gloomberb 0.9.7 → 0.9.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/api-client/auth.ts +20 -0
- package/src/api-client/index.test.ts +3 -3
- package/src/api-client/index.ts +48 -6
- package/src/api-client/request.test.ts +63 -0
- package/src/api-client/request.ts +53 -5
- package/src/api-client/types.ts +45 -2
- package/src/app/pane-runtime/layout-placement.ts +0 -1
- package/src/app/runtime/desktop-deeplink.test.ts +87 -0
- package/src/app/runtime/desktop-deeplink.ts +496 -0
- package/src/app.tsx +34 -0
- package/src/brokers/account-cache.ts +2 -10
- package/src/brokers/broker-portfolio-sync.ts +0 -1
- package/src/brokers/broker-ticker-sync.ts +20 -16
- package/src/brokers/remote-broker-adapter.ts +3 -0
- package/src/capabilities/registry.test.ts +25 -0
- package/src/capabilities/registry.ts +22 -5
- package/src/cli/commands/automation.ts +1 -3
- package/src/cli/commands/command-utils.ts +0 -7
- package/src/cli/commands/overview.ts +2 -2
- package/src/cli/commands/plugins.ts +34 -17
- package/src/cli/index.ts +2 -2
- package/src/cli.test.ts +0 -157
- package/src/components/chart/bar-chart-renderer.test.ts +87 -12
- package/src/components/chart/bar-chart-renderer.ts +111 -55
- package/src/components/chart/comparison/renderer.ts +0 -1
- package/src/components/chart/comparison-stock-chart/render-output.ts +3 -8
- package/src/components/chart/comparison-stock-chart/view.tsx +0 -2
- package/src/components/chart/core/index.test.ts +0 -24
- package/src/components/chart/core/renderer.ts +0 -4
- package/src/components/chart/core/resolution.test.ts +0 -17
- package/src/components/chart/core/resolution.ts +0 -26
- package/src/components/chart/core/scroll.test.ts +0 -6
- package/src/components/chart/core/scroll.ts +0 -22
- package/src/components/chart/core/viewport.test.ts +2 -23
- package/src/components/chart/core/viewport.ts +0 -27
- package/src/components/chart/market-session.ts +1 -1
- package/src/components/chart/native/surface/hooks.ts +10 -3
- package/src/components/chart/static/bar-chart-surface.tsx +104 -20
- package/src/components/chart/stock-chart/data-runtime.ts +0 -1
- package/src/components/chart/stock-chart/footer.test.ts +65 -0
- package/src/components/chart/stock-chart/footer.ts +68 -13
- package/src/components/chart/stock-chart/resolved.tsx +18 -4
- package/src/components/chart/stock-chart/viewport/effects.ts +0 -3
- package/src/components/command-bar/helpers.test.ts +1 -80
- package/src/components/command-bar/helpers.ts +1 -36
- package/src/components/command-bar/pane-settings.ts +1 -1
- package/src/components/command-bar/pane-templates/items.ts +11 -3
- package/src/components/command-bar/panel/runtime.ts +0 -2
- package/src/components/command-bar/routes/root/results.ts +1 -1
- package/src/components/command-bar/routes/ticker-search/results.ts +0 -11
- package/src/components/command-bar/surface/index.test.tsx +52 -16
- package/src/components/command-bar/surface/index.tsx +0 -1
- package/src/components/command-bar/workflow/ops.ts +81 -20
- package/src/components/feed-data-table/stack-view.test.tsx +36 -130
- package/src/components/feed-data-table/stack-view.tsx +0 -1
- package/src/components/index.ts +1 -0
- package/src/components/layout/shell/index.test.tsx +2 -113
- package/src/components/layout/shell/index.tsx +0 -1
- package/src/components/layout/shell/menu.ts +0 -1
- package/src/components/layout/shell/terminal-pointer-runtime.ts +0 -1
- package/src/components/onboarding/onboarding-wizard.tsx +3 -3
- package/src/components/onboarding/wizard-keyboard.ts +2 -23
- package/src/components/price-selector-dialog.tsx +3 -3
- package/src/components/ticker/badge/text.tsx +0 -1
- package/src/components/toggle-list.tsx +29 -73
- package/src/components/ui/button.tsx +4 -1
- package/src/components/ui/checkbox.tsx +76 -0
- package/src/components/ui/choice-dialog.tsx +15 -3
- package/src/components/ui/frame.tsx +1 -1
- package/src/components/ui/index.ts +2 -0
- package/src/components/ui/message-composer.tsx +4 -0
- package/src/components/ui/native-select.tsx +4 -2
- package/src/components/ui/page-stack-view.tsx +1 -1
- package/src/components/ui/ui.test.tsx +0 -78
- package/src/core/state/app/layout-reducer.ts +0 -1
- package/src/data/config/layout.ts +3 -3
- package/src/data/resource-store.ts +6 -0
- package/src/data/sqlite/cache.test.ts +5 -0
- package/src/data/ticker-store.ts +0 -7
- package/src/market-data/coordinator/auxiliary.ts +1 -44
- package/src/market-data/coordinator/entries.ts +0 -1
- package/src/market-data/coordinator/events.test.ts +49 -0
- package/src/market-data/coordinator/events.ts +25 -4
- package/src/market-data/coordinator/index.test.ts +223 -10
- package/src/market-data/coordinator/index.ts +41 -21
- package/src/market-data/coordinator/quotes.ts +13 -11
- package/src/market-data/coordinator/subscriptions.test.ts +1 -0
- package/src/market-data/hooks.tsx +17 -13
- package/src/market-data/market/freshness.ts +58 -1
- package/src/market-data/market/status.ts +19 -1
- package/src/market-data/query-store.ts +0 -4
- package/src/market-data/quotes/freshness.test.ts +49 -1
- package/src/market-data/quotes/freshness.ts +8 -0
- package/src/market-data/quotes/resolution.test.ts +185 -0
- package/src/market-data/quotes/resolution.ts +96 -10
- package/src/market-data/request-types.test.ts +61 -1
- package/src/market-data/request-types.ts +42 -8
- package/src/market-data/result-types.ts +0 -11
- package/src/market-data/selectors.ts +1 -5
- package/src/news/aggregator.ts +5 -1
- package/src/pane-settings.test.ts +0 -18
- package/src/plugins/builtin/account-management/footer.ts +2 -5
- package/src/plugins/builtin/account-management/form-components.tsx +175 -59
- package/src/plugins/builtin/account-management/keyboard.ts +32 -2
- package/src/plugins/builtin/account-management/model.test.ts +99 -4
- package/src/plugins/builtin/account-management/model.ts +259 -23
- package/src/plugins/builtin/account-management/pane.tsx +784 -155
- package/src/plugins/builtin/broker-manager/index.test.tsx +0 -32
- package/src/plugins/builtin/buildout/cells.tsx +1 -1
- package/src/plugins/builtin/buildout/detail/satellite-images.tsx +1 -1
- package/src/plugins/builtin/buildout/detail/ui.tsx +0 -1
- package/src/plugins/builtin/buildout/format.ts +2 -11
- package/src/plugins/builtin/chat/chat.test.tsx +127 -1
- package/src/plugins/builtin/chat/content/channel-navigation.ts +1 -21
- package/src/plugins/builtin/chat/content/composer-cursor.ts +29 -0
- package/src/plugins/builtin/chat/content/composer-runtime.ts +22 -24
- package/src/plugins/builtin/chat/content/composer.tsx +84 -1
- package/src/plugins/builtin/chat/content/index.tsx +153 -16
- package/src/plugins/builtin/chat/content/layout-metrics.ts +10 -1
- package/src/plugins/builtin/chat/content/mentions.test.ts +74 -0
- package/src/plugins/builtin/chat/content/mentions.ts +88 -0
- package/src/plugins/builtin/chat/content/new-dm-dialog.tsx +264 -0
- package/src/plugins/builtin/chat/content/shortcuts.ts +47 -1
- package/src/plugins/builtin/chat/content/snapshot.ts +7 -0
- package/src/plugins/builtin/chat/content/transcript.tsx +0 -5
- package/src/plugins/builtin/chat/controller/index.ts +0 -10
- package/src/plugins/builtin/chat/controller.test.ts +5 -5
- package/src/plugins/builtin/chat/message/profile-popover.test.tsx +36 -0
- package/src/plugins/builtin/chat/message/profile-popover.tsx +124 -20
- package/src/plugins/builtin/chat/sidebar.test.tsx +83 -6
- package/src/plugins/builtin/chat/sidebar.tsx +26 -2
- package/src/plugins/builtin/chat/test-harness.tsx +22 -12
- package/src/plugins/builtin/cloud/plugin.tsx +8 -1
- package/src/plugins/builtin/cloud-tweets/pane.tsx +0 -1
- package/src/plugins/builtin/congress-trades/model.ts +2 -7
- package/src/plugins/builtin/congress-trades/pane.tsx +0 -1
- package/src/plugins/builtin/correlation/index.tsx +4 -1
- package/src/plugins/builtin/correlation/relationship/controls.tsx +8 -2
- package/src/plugins/builtin/correlation/relationship/model.ts +22 -2
- package/src/plugins/builtin/correlation/relationship/pane.tsx +9 -3
- package/src/plugins/builtin/earnings/index.tsx +0 -9
- package/src/plugins/builtin/econ/index.tsx +1 -5
- package/src/plugins/builtin/help/index.tsx +2 -2
- package/src/plugins/builtin/holders/pane.tsx +0 -5
- package/src/plugins/builtin/insider/index.tsx +1 -8
- package/src/plugins/builtin/kelly-sizer/index.test.tsx +0 -26
- package/src/plugins/builtin/kelly-sizer/index.tsx +0 -2
- package/src/plugins/builtin/market-movers/index.tsx +0 -2
- package/src/plugins/builtin/news/index.tsx +0 -1
- package/src/plugins/builtin/news/wire/news/table.tsx +0 -11
- package/src/plugins/builtin/options/view.test.tsx +1 -38
- package/src/plugins/builtin/plugin-groups.ts +17 -3
- package/src/plugins/builtin/portfolio-list/column-values.ts +15 -8
- package/src/plugins/builtin/portfolio-list/index.test.ts +52 -25
- package/src/plugins/builtin/portfolio-list/metrics.test.ts +44 -7
- package/src/plugins/builtin/portfolio-list/pane/data.test.ts +54 -0
- package/src/plugins/builtin/portfolio-list/pane/data.ts +50 -3
- package/src/plugins/builtin/portfolio-list/pane/index.test.tsx +314 -16
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +11 -4
- package/src/plugins/builtin/portfolio-list/pane/streaming.ts +83 -24
- package/src/plugins/builtin/sec/filing-documents.ts +0 -11
- package/src/plugins/builtin/sec/index.tsx +0 -2
- package/src/plugins/builtin/substack/article-detail.tsx +1 -1
- package/src/plugins/builtin/thirteenf/api.ts +0 -15
- package/src/plugins/builtin/thirteenf/pane.tsx +1 -3
- package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/content.tsx +42 -6
- package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/model.ts +415 -64
- package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/settings.ts +23 -2
- package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/types.ts +8 -1
- package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph.tsx +3 -1
- package/src/plugins/builtin/ticker-detail/data-panes.test.ts +95 -1
- package/src/plugins/builtin/ticker-detail/financials/tab.tsx +0 -7
- package/src/plugins/builtin/ticker-detail/index.tsx +2 -0
- package/src/plugins/builtin/ticker-detail/quote-monitor/index.test.tsx +5 -34
- package/src/plugins/builtin/world-indices/index.tsx +0 -4
- package/src/plugins/builtin/world-indices/model.ts +0 -7
- package/src/plugins/ibkr/account-cache.ts +1 -1
- package/src/plugins/ibkr/gateway/market-data.ts +12 -5
- package/src/plugins/ibkr/gateway/service/index.test.ts +31 -0
- package/src/plugins/ibkr/portfolio-performance.ts +1 -1
- package/src/plugins/ibkr/trade/utils.ts +1 -7
- package/src/plugins/loader.ts +2 -1
- package/src/plugins/prediction-markets/detail.test.tsx +0 -34
- package/src/plugins/registry/context.ts +15 -0
- package/src/plugins/registry/contributions.ts +22 -11
- package/src/plugins/registry/index.test.ts +44 -0
- package/src/plugins/registry/index.ts +75 -26
- package/src/remote/controller.ts +1 -7
- package/src/remote/json-patch.ts +15 -1
- package/src/remote/server.ts +16 -6
- package/src/renderers/electrobun/bun/application-menu/index.ts +1 -1
- package/src/renderers/electrobun/bun/core-capabilities.ts +2 -0
- package/src/renderers/electrobun/bun/desktop/http-fetch.test.ts +14 -0
- package/src/renderers/electrobun/bun/desktop/http-fetch.ts +8 -0
- package/src/renderers/electrobun/bun/desktop/initialization.ts +2 -1
- package/src/renderers/electrobun/bun/index.ts +42 -0
- package/src/renderers/electrobun/shared/protocol.ts +5 -0
- package/src/renderers/electrobun/view/backend-rpc.ts +29 -0
- package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +1 -0
- package/src/renderers/electrobun/view/data-table/dom.ts +0 -1
- package/src/renderers/electrobun/view/desktop/controls.tsx +135 -4
- package/src/renderers/electrobun/view/desktop/list-view.tsx +38 -25
- package/src/renderers/electrobun/view/desktop-deeplink-bridge.ts +10 -0
- package/src/renderers/electrobun/view/host/box.tsx +0 -2
- package/src/renderers/electrobun/view/host/mouse.ts +0 -11
- package/src/renderers/electrobun/view/host/scroll-box.tsx +1 -2
- package/src/renderers/electrobun/view/host/text.tsx +1 -2
- package/src/renderers/electrobun/view/http-fetch.ts +15 -2
- package/src/renderers/electrobun/view/main.tsx +3 -0
- package/src/renderers/electrobun/view/remote/asset-data-client.ts +9 -18
- package/src/renderers/electrobun/view/remote/capability-invoker.test.ts +50 -0
- package/src/renderers/electrobun/view/remote/capability-invoker.ts +36 -0
- package/src/renderers/electrobun/view/ui-host.tsx +2 -0
- package/src/renderers/opentui/host.tsx +11 -3
- package/src/renderers/opentui/input-host.tsx +5 -1
- package/src/sources/gloomberb-cloud/index.test.ts +0 -72
- package/src/sources/gloomberb-cloud/index.ts +1 -30
- package/src/sources/gloomberb-cloud/news.ts +0 -12
- package/src/sources/provider-router/batches.ts +11 -3
- package/src/sources/provider-router/cached-financials.test.ts +123 -0
- package/src/sources/provider-router/financial-routes.ts +77 -5
- package/src/sources/provider-router/financials.test.ts +49 -0
- package/src/sources/provider-router/financials.ts +40 -3
- package/src/sources/provider-router/index.test.ts +314 -0
- package/src/sources/provider-router/primary.ts +17 -3
- package/src/sources/provider-router/search.ts +5 -0
- package/src/sources/provider-router/streaming.ts +6 -1
- package/src/sources/provider-router/supplemental.ts +4 -3
- package/src/sources/sec-edgar.test.ts +42 -0
- package/src/sources/sec-edgar.ts +1 -2
- package/src/sources/yahoo-finance/history.ts +5 -1
- package/src/sources/yahoo-finance/mappers.test.ts +29 -0
- package/src/sources/yahoo-finance/mappers.ts +2 -1
- package/src/sources/yahoo-finance/options.ts +59 -14
- package/src/sources/yahoo-finance/quote-summary.ts +4 -1
- package/src/sources/yahoo-finance/requests.ts +2 -1
- package/src/sources/yahoo-finance/snapshots.ts +13 -2
- package/src/sources/yahoo-finance.test.ts +40 -21
- package/src/sources/yahoo-finance.ts +19 -10
- package/src/state/app/context/index.test.tsx +0 -81
- package/src/state/app/context/selector.test.tsx +6 -2
- package/src/state/hooks/quote-streaming.test.ts +14 -1
- package/src/state/hooks/quote-streaming.ts +5 -4
- package/src/sync/controller.test.ts +31 -0
- package/src/sync/controller.ts +300 -0
- package/src/sync/core-contributors.test.ts +251 -0
- package/src/sync/core-contributors.ts +400 -0
- package/src/sync/profile-analytics.ts +39 -0
- package/src/sync/react.ts +53 -0
- package/src/sync/types.ts +77 -0
- package/src/test-support/data-provider.ts +0 -1
- package/src/types/desktop-deeplink.ts +7 -0
- package/src/types/plugin.ts +3 -0
- package/src/ui/host.tsx +15 -1
- package/src/ui/index.tsx +2 -0
- package/src/updater.test.ts +0 -21
- package/src/updater.ts +20 -16
- package/src/utils/async-deadline.ts +26 -0
- package/src/utils/exchanges.test.ts +11 -1
- package/src/utils/exchanges.ts +5 -1
- package/src/utils/selection-clipboard.ts +2 -2
- package/src/version.ts +1 -1
- package/src/components/layout/pane/header.test.tsx +0 -39
- package/src/plugins/builtin/chat/channels.test.ts +0 -28
- package/src/plugins/builtin/chat/index.tsx +0 -4
- package/src/plugins/builtin/cloud-tweets/index.ts +0 -1
- package/src/plugins/builtin/earnings/index.test.ts +0 -12
- package/src/plugins/builtin/help/index.test.tsx +0 -242
- package/src/plugins/builtin/substack/article-detail.test.tsx +0 -10
- package/src/renderers/electrobun/bun/application-menu/index.test.ts +0 -21
- package/src/renderers/electrobun/bun/window/focus.test.ts +0 -35
- package/src/renderers/electrobun/view/host/mouse.test.ts +0 -37
- package/src/renderers/electrobun/view/host/text.test.ts +0 -22
- package/src/renderers/opentui/dialog-host.test.tsx +0 -44
- /package/src/{plugins/ibkr → utils}/hash.ts +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { BarChartSeries } from "../../../../../components/chart/bar-chart-renderer";
|
|
2
2
|
import { colors } from "../../../../../theme/colors";
|
|
3
|
-
import type { FinancialStatement, TickerFinancials } from "../../../../../types/financials";
|
|
3
|
+
import type { FinancialStatement, PricePoint, TickerFinancials } from "../../../../../types/financials";
|
|
4
4
|
import { formatCompact, formatNumber, formatPercent } from "../../../../../utils/format";
|
|
5
5
|
import type {
|
|
6
6
|
FundamentalColumn,
|
|
@@ -13,13 +13,62 @@ import type {
|
|
|
13
13
|
ValuationMetricKey,
|
|
14
14
|
} from "./types";
|
|
15
15
|
|
|
16
|
+
function finiteNumber(value: unknown): value is number {
|
|
17
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function financialRatio(
|
|
21
|
+
numerator: number | null | undefined,
|
|
22
|
+
denominator: number | null | undefined,
|
|
23
|
+
): number | null {
|
|
24
|
+
return finiteNumber(numerator) && finiteNumber(denominator) && denominator !== 0
|
|
25
|
+
? numerator / denominator
|
|
26
|
+
: null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function freeCashFlowValue(statement: FinancialStatement): number | null {
|
|
30
|
+
if (finiteNumber(statement.freeCashFlow)) return statement.freeCashFlow;
|
|
31
|
+
if (finiteNumber(statement.operatingCashFlow) && finiteNumber(statement.capitalExpenditure)) {
|
|
32
|
+
return statement.operatingCashFlow + statement.capitalExpenditure;
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function formatMargin(value: number): string {
|
|
38
|
+
return `${formatNumber(value * 100, 1)}%`;
|
|
39
|
+
}
|
|
40
|
+
|
|
16
41
|
const FUNDAMENTAL_METRICS: ReadonlyArray<MetricDefinition<FundamentalMetricKey>> = [
|
|
17
42
|
{ key: "totalRevenue", label: "Revenue", format: formatCompact },
|
|
18
43
|
{ key: "grossProfit", label: "Gross Profit", format: formatCompact },
|
|
44
|
+
{
|
|
45
|
+
key: "grossMargin",
|
|
46
|
+
label: "Gross Margin",
|
|
47
|
+
format: formatMargin,
|
|
48
|
+
value: (statement) => financialRatio(statement.grossProfit, statement.totalRevenue),
|
|
49
|
+
},
|
|
19
50
|
{ key: "operatingIncome", label: "Operating Income", format: formatCompact },
|
|
51
|
+
{
|
|
52
|
+
key: "operatingMargin",
|
|
53
|
+
label: "Operating Margin",
|
|
54
|
+
format: formatMargin,
|
|
55
|
+
value: (statement) => financialRatio(statement.operatingIncome, statement.totalRevenue),
|
|
56
|
+
},
|
|
20
57
|
{ key: "netIncome", label: "Net Income", format: formatCompact },
|
|
58
|
+
{
|
|
59
|
+
key: "netMargin",
|
|
60
|
+
label: "Net Margin",
|
|
61
|
+
format: formatMargin,
|
|
62
|
+
value: (statement) => financialRatio(statement.netIncome, statement.totalRevenue),
|
|
63
|
+
},
|
|
21
64
|
{ key: "operatingCashFlow", label: "Operating Cash Flow", format: formatCompact },
|
|
22
|
-
{ key: "freeCashFlow", label: "Free Cash Flow", format: formatCompact },
|
|
65
|
+
{ key: "freeCashFlow", label: "Free Cash Flow", format: formatCompact, value: freeCashFlowValue },
|
|
66
|
+
{
|
|
67
|
+
key: "freeCashFlowMargin",
|
|
68
|
+
label: "FCF Margin",
|
|
69
|
+
format: formatMargin,
|
|
70
|
+
value: (statement) => financialRatio(freeCashFlowValue(statement), statement.totalRevenue),
|
|
71
|
+
},
|
|
23
72
|
{ key: "totalAssets", label: "Total Assets", format: formatCompact },
|
|
24
73
|
{ key: "totalDebt", label: "Total Debt", format: formatCompact },
|
|
25
74
|
{ key: "totalEquity", label: "Equity", format: formatCompact },
|
|
@@ -52,52 +101,221 @@ export function formatMaybePercent(value: number | null): string {
|
|
|
52
101
|
}
|
|
53
102
|
|
|
54
103
|
function selectedStatements(financials: TickerFinancials | null, period: FundamentalPeriod): FinancialStatement[] {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
: financials?.quarterlyStatements ?? [];
|
|
104
|
+
if (period === "annual") return financials?.annualStatements ?? [];
|
|
105
|
+
return deriveQuarterlyStatements(financials?.quarterlyStatements ?? [], financials?.annualStatements ?? []);
|
|
58
106
|
}
|
|
59
107
|
|
|
60
108
|
function periodCategory(date: string, period: FundamentalPeriod): string {
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
109
|
+
const parsedYear = Number(date.slice(0, 4));
|
|
110
|
+
let year = Number.isFinite(parsedYear) ? parsedYear : 0;
|
|
111
|
+
if (period === "annual") return year > 0 ? `FY${year}` : date;
|
|
112
|
+
let month = Number(date.slice(5, 7));
|
|
113
|
+
const day = Number(date.slice(8, 10));
|
|
114
|
+
if (Number.isFinite(month) && Number.isFinite(day) && day <= 7 && (month - 1) % 3 === 0) {
|
|
115
|
+
month -= 1;
|
|
116
|
+
if (month === 0) {
|
|
117
|
+
month = 12;
|
|
118
|
+
year -= 1;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
64
121
|
const quarter = Number.isFinite(month) && month > 0 ? Math.ceil(month / 3) : 0;
|
|
65
|
-
return quarter > 0 ? `${year} Q${quarter}` : date;
|
|
122
|
+
return quarter > 0 && year > 0 ? `${year} Q${quarter}` : date;
|
|
66
123
|
}
|
|
67
124
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
125
|
+
type NumericStatementField = Exclude<keyof FinancialStatement, "date">;
|
|
126
|
+
|
|
127
|
+
const QUARTERLY_FLOW_FIELDS: NumericStatementField[] = [
|
|
128
|
+
"totalRevenue",
|
|
129
|
+
"grossProfit",
|
|
130
|
+
"operatingIncome",
|
|
131
|
+
"netIncome",
|
|
132
|
+
"ebitda",
|
|
133
|
+
"operatingCashFlow",
|
|
134
|
+
"capitalExpenditure",
|
|
135
|
+
"freeCashFlow",
|
|
136
|
+
"eps",
|
|
137
|
+
];
|
|
138
|
+
|
|
139
|
+
const QUARTERLY_SNAPSHOT_FIELDS: NumericStatementField[] = [
|
|
140
|
+
"totalAssets",
|
|
141
|
+
"cashAndCashEquivalents",
|
|
142
|
+
"cashCashEquivalentsAndShortTermInvestments",
|
|
143
|
+
"totalDebt",
|
|
144
|
+
"totalEquity",
|
|
145
|
+
"basicShares",
|
|
146
|
+
"dilutedShares",
|
|
147
|
+
"shareIssued",
|
|
148
|
+
"ordinarySharesNumber",
|
|
149
|
+
];
|
|
150
|
+
|
|
151
|
+
function statementNumber(statement: FinancialStatement, field: NumericStatementField): number | null {
|
|
152
|
+
const value = statement[field];
|
|
153
|
+
return finiteNumber(value) ? value : null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function setStatementNumber(statement: FinancialStatement, field: NumericStatementField, value: number): void {
|
|
157
|
+
(statement as Record<NumericStatementField, number | undefined>)[field] = value;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function statementTime(statement: FinancialStatement): number {
|
|
161
|
+
const time = Date.parse(statement.date);
|
|
162
|
+
return Number.isFinite(time) ? time : Number.NaN;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function precedingQuarterValues(
|
|
166
|
+
quarterlyStatements: FinancialStatement[],
|
|
167
|
+
annualStatement: FinancialStatement,
|
|
168
|
+
field: NumericStatementField,
|
|
169
|
+
): number[] {
|
|
170
|
+
const annualTime = statementTime(annualStatement);
|
|
171
|
+
if (!Number.isFinite(annualTime)) return [];
|
|
172
|
+
|
|
173
|
+
const annualCategory = periodCategory(annualStatement.date, "quarterly");
|
|
174
|
+
const byCategory = new Map<string, { date: string; time: number; value: number }>();
|
|
175
|
+
for (const statement of quarterlyStatements) {
|
|
176
|
+
const time = statementTime(statement);
|
|
177
|
+
if (!Number.isFinite(time) || time >= annualTime || annualTime - time > 370 * 24 * 60 * 60 * 1000) {
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
const category = periodCategory(statement.date, "quarterly");
|
|
181
|
+
if (category === annualCategory) continue;
|
|
182
|
+
const value = statementNumber(statement, field);
|
|
183
|
+
if (value === null) continue;
|
|
184
|
+
const previous = byCategory.get(category);
|
|
185
|
+
if (!previous || statement.date.localeCompare(previous.date) > 0) {
|
|
186
|
+
byCategory.set(category, { date: statement.date, time, value });
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return [...byCategory.values()]
|
|
191
|
+
.sort((left, right) => right.time - left.time)
|
|
192
|
+
.slice(0, 3)
|
|
193
|
+
.sort((left, right) => left.time - right.time)
|
|
194
|
+
.map((entry) => entry.value);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function mergeStatementsByDate(statements: FinancialStatement[]): FinancialStatement[] {
|
|
198
|
+
const byDate = new Map<string, FinancialStatement>();
|
|
199
|
+
for (const statement of statements) {
|
|
200
|
+
const existing = byDate.get(statement.date);
|
|
201
|
+
if (!existing) {
|
|
202
|
+
byDate.set(statement.date, { ...statement });
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
for (const [key, value] of Object.entries(statement) as Array<[keyof FinancialStatement, unknown]>) {
|
|
206
|
+
if (key === "date") continue;
|
|
207
|
+
if (value !== undefined && existing[key] === undefined) {
|
|
208
|
+
(existing as unknown as Record<string, unknown>)[key] = value;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return [...byDate.values()];
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function deriveQuarterlyStatements(
|
|
216
|
+
quarterlyStatements: FinancialStatement[],
|
|
217
|
+
annualStatements: FinancialStatement[],
|
|
218
|
+
): FinancialStatement[] {
|
|
219
|
+
const mergedQuarterly = mergeStatementsByDate(quarterlyStatements);
|
|
220
|
+
const byDate = new Map(mergedQuarterly.map((statement) => [statement.date, { ...statement }]));
|
|
221
|
+
|
|
222
|
+
for (const annualStatement of annualStatements) {
|
|
223
|
+
let target = byDate.get(annualStatement.date);
|
|
224
|
+
let changed = false;
|
|
225
|
+
if (!target) {
|
|
226
|
+
target = { date: annualStatement.date };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
for (const field of QUARTERLY_FLOW_FIELDS) {
|
|
230
|
+
if (statementNumber(target, field) !== null) continue;
|
|
231
|
+
const annualValue = statementNumber(annualStatement, field);
|
|
232
|
+
if (annualValue === null) continue;
|
|
233
|
+
const previousValues = precedingQuarterValues(mergedQuarterly, annualStatement, field);
|
|
234
|
+
if (previousValues.length !== 3) continue;
|
|
235
|
+
const derived = annualValue - previousValues.reduce((sum, value) => sum + value, 0);
|
|
236
|
+
if (!Number.isFinite(derived)) continue;
|
|
237
|
+
setStatementNumber(target, field, derived);
|
|
238
|
+
changed = true;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
for (const field of QUARTERLY_SNAPSHOT_FIELDS) {
|
|
242
|
+
if (statementNumber(target, field) !== null) continue;
|
|
243
|
+
const annualValue = statementNumber(annualStatement, field);
|
|
244
|
+
if (annualValue === null) continue;
|
|
245
|
+
setStatementNumber(target, field, annualValue);
|
|
246
|
+
changed = true;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (changed) byDate.set(target.date, target);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return [...byDate.values()].sort((left, right) => left.date.localeCompare(right.date));
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function fundamentalMetricDef(metric: FundamentalMetricKey): MetricDefinition<FundamentalMetricKey> {
|
|
256
|
+
return FUNDAMENTAL_METRICS.find((definition) => definition.key === metric) ?? FUNDAMENTAL_METRICS[0]!;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function fundamentalMetricValue(statement: FinancialStatement, metric: FundamentalMetricKey): number | null {
|
|
260
|
+
const definition = fundamentalMetricDef(metric);
|
|
261
|
+
const value = definition.value
|
|
262
|
+
? definition.value(statement)
|
|
263
|
+
: statement[metric as keyof FinancialStatement];
|
|
264
|
+
return finiteNumber(value) ? value : null;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
interface GraphRowEntry {
|
|
268
|
+
date: string;
|
|
269
|
+
category: string;
|
|
270
|
+
value: number;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function buildRowsFromEntries(
|
|
274
|
+
entries: GraphRowEntry[],
|
|
275
|
+
metric: GraphMetricKey,
|
|
276
|
+
symbol: string,
|
|
73
277
|
): FundamentalGraphRow[] {
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
const category = periodCategory(statement.date, period);
|
|
80
|
-
const key = `${symbol}\u0000${category}`;
|
|
81
|
-
const previous = byCategory.get(key);
|
|
82
|
-
if (!previous || statement.date.localeCompare(previous.date) >= 0) {
|
|
83
|
-
byCategory.set(key, statement);
|
|
278
|
+
const byCategory = new Map<string, GraphRowEntry>();
|
|
279
|
+
for (const entry of [...entries].sort((left, right) => left.date.localeCompare(right.date))) {
|
|
280
|
+
const previous = byCategory.get(entry.category);
|
|
281
|
+
if (!previous || entry.date.localeCompare(previous.date) >= 0) {
|
|
282
|
+
byCategory.set(entry.category, entry);
|
|
84
283
|
}
|
|
85
284
|
}
|
|
86
285
|
const sorted = [...byCategory.values()].sort((left, right) => left.date.localeCompare(right.date));
|
|
87
|
-
const maxAbs = Math.max(1, ...sorted.map((
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
const previous = sorted[index - 1]?.
|
|
286
|
+
const maxAbs = Math.max(1, ...sorted.map((entry) => Math.abs(entry.value)));
|
|
287
|
+
|
|
288
|
+
return sorted.map((entry, index) => {
|
|
289
|
+
const previous = sorted[index - 1]?.value;
|
|
91
290
|
return {
|
|
92
|
-
key: [symbol,
|
|
291
|
+
key: [symbol, entry.date, metric].filter(Boolean).join(":"),
|
|
93
292
|
symbol,
|
|
293
|
+
date: entry.date,
|
|
294
|
+
category: entry.category,
|
|
295
|
+
value: entry.value,
|
|
296
|
+
growth: typeof previous === "number" && previous !== 0
|
|
297
|
+
? (entry.value - previous) / Math.abs(previous)
|
|
298
|
+
: null,
|
|
299
|
+
barWidth: Math.max(1, Math.round((Math.abs(entry.value) / maxAbs) * 24)),
|
|
300
|
+
};
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export function buildFundamentalGraphRows(
|
|
305
|
+
statements: FinancialStatement[],
|
|
306
|
+
metric: FundamentalMetricKey,
|
|
307
|
+
symbol = "",
|
|
308
|
+
period: FundamentalPeriod = "annual",
|
|
309
|
+
): FundamentalGraphRow[] {
|
|
310
|
+
const entries = statements
|
|
311
|
+
.map((statement) => ({ statement, value: fundamentalMetricValue(statement, metric) }))
|
|
312
|
+
.filter((entry): entry is { statement: FinancialStatement; value: number } => entry.value !== null)
|
|
313
|
+
.map(({ statement, value }) => ({
|
|
94
314
|
date: statement.date,
|
|
95
315
|
category: periodCategory(statement.date, period),
|
|
96
316
|
value,
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
};
|
|
100
|
-
});
|
|
317
|
+
}));
|
|
318
|
+
return buildRowsFromEntries(entries, metric, symbol);
|
|
101
319
|
}
|
|
102
320
|
|
|
103
321
|
function resolveMarketCap(financials: TickerFinancials | null): number | null {
|
|
@@ -111,21 +329,141 @@ function resolveMarketCap(financials: TickerFinancials | null): number | null {
|
|
|
111
329
|
: null;
|
|
112
330
|
}
|
|
113
331
|
|
|
114
|
-
function
|
|
332
|
+
function timeFromDate(value: Date | string): number {
|
|
333
|
+
return value instanceof Date ? value.getTime() : Date.parse(value);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function priceAtOrBefore(priceHistory: PricePoint[], date: string): number | null {
|
|
337
|
+
const target = Date.parse(date);
|
|
338
|
+
if (!Number.isFinite(target)) return null;
|
|
339
|
+
|
|
340
|
+
let closest: { time: number; close: number } | null = null;
|
|
341
|
+
for (const point of priceHistory) {
|
|
342
|
+
if (!finiteNumber(point.close) || point.close <= 0) continue;
|
|
343
|
+
const time = timeFromDate(point.date);
|
|
344
|
+
if (!Number.isFinite(time) || time > target) continue;
|
|
345
|
+
if (!closest || time > closest.time) {
|
|
346
|
+
closest = { time, close: point.close };
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return closest?.close ?? null;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function statementShares(statement: FinancialStatement): number | null {
|
|
353
|
+
const shares = statement.dilutedShares
|
|
354
|
+
?? statement.basicShares
|
|
355
|
+
?? statement.ordinarySharesNumber
|
|
356
|
+
?? statement.shareIssued;
|
|
357
|
+
return finiteNumber(shares) && shares > 0 ? shares : null;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function statementEpsValue(statement: FinancialStatement): number | null {
|
|
361
|
+
if (finiteNumber(statement.eps) && statement.eps !== 0) return statement.eps;
|
|
362
|
+
const shares = statementShares(statement);
|
|
363
|
+
return finiteNumber(statement.netIncome) && shares ? statement.netIncome / shares : null;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function aggregateEps(statements: FinancialStatement[]): number | null {
|
|
367
|
+
if (statements.length === 0) return null;
|
|
368
|
+
const epsValues = statements.map(statementEpsValue);
|
|
369
|
+
if (epsValues.every((value): value is number => finiteNumber(value))) {
|
|
370
|
+
return epsValues.reduce((sum, value) => sum + value, 0);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const latestShares = statementShares(statements.at(-1)!);
|
|
374
|
+
const incomeValues = statements.map((statement) => statement.netIncome);
|
|
375
|
+
if (!latestShares || !incomeValues.every((value): value is number => finiteNumber(value))) {
|
|
376
|
+
return null;
|
|
377
|
+
}
|
|
378
|
+
return incomeValues.reduce((sum, value) => sum + value, 0) / latestShares;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function trailingEpsValue(
|
|
382
|
+
statements: FinancialStatement[],
|
|
383
|
+
index: number,
|
|
384
|
+
period: FundamentalPeriod,
|
|
385
|
+
): number | null {
|
|
386
|
+
if (period === "annual") return statementEpsValue(statements[index]!);
|
|
387
|
+
if (index < 3) return null;
|
|
388
|
+
return aggregateEps(statements.slice(index - 3, index + 1));
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function forwardEpsValue(
|
|
392
|
+
statements: FinancialStatement[],
|
|
393
|
+
index: number,
|
|
394
|
+
period: FundamentalPeriod,
|
|
395
|
+
): number | null {
|
|
396
|
+
if (period === "annual") {
|
|
397
|
+
const nextStatement = statements[index + 1];
|
|
398
|
+
return nextStatement ? statementEpsValue(nextStatement) : null;
|
|
399
|
+
}
|
|
400
|
+
if (index + 4 >= statements.length) return null;
|
|
401
|
+
return aggregateEps(statements.slice(index + 1, index + 5));
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function historicalMarketCap(
|
|
405
|
+
financials: TickerFinancials | null,
|
|
406
|
+
statement: FinancialStatement,
|
|
407
|
+
): number | null {
|
|
408
|
+
const price = financials ? priceAtOrBefore(financials.priceHistory, statement.date) : null;
|
|
409
|
+
const shares = statementShares(statement);
|
|
410
|
+
if (price && shares) return price * shares;
|
|
411
|
+
return resolveMarketCap(financials);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function historicalEnterpriseValue(
|
|
415
|
+
financials: TickerFinancials | null,
|
|
416
|
+
statement: FinancialStatement,
|
|
417
|
+
): number | null {
|
|
418
|
+
const marketCap = historicalMarketCap(financials, statement);
|
|
419
|
+
if (!marketCap) return financials?.fundamentals?.enterpriseValue ?? null;
|
|
420
|
+
const debt = finiteNumber(statement.totalDebt) ? statement.totalDebt : 0;
|
|
421
|
+
const cash = statement.cashCashEquivalentsAndShortTermInvestments
|
|
422
|
+
?? statement.cashAndCashEquivalents
|
|
423
|
+
?? 0;
|
|
424
|
+
return marketCap + debt - (finiteNumber(cash) ? cash : 0);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function currentValuationMetricValue(
|
|
115
428
|
financials: TickerFinancials | null,
|
|
116
429
|
metric: ValuationMetricKey,
|
|
117
|
-
statement: FinancialStatement | null,
|
|
118
430
|
): number | null {
|
|
119
431
|
const fundamentals = financials?.fundamentals;
|
|
120
432
|
if (metric === "trailingPE") return fundamentals?.trailingPE ?? null;
|
|
121
433
|
if (metric === "forwardPE") return fundamentals?.forwardPE ?? null;
|
|
122
434
|
if (metric === "pegRatio") return fundamentals?.pegRatio ?? null;
|
|
435
|
+
return null;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function valuationMetricValue(
|
|
439
|
+
financials: TickerFinancials | null,
|
|
440
|
+
metric: ValuationMetricKey,
|
|
441
|
+
statement: FinancialStatement | null,
|
|
442
|
+
context?: {
|
|
443
|
+
statements: FinancialStatement[];
|
|
444
|
+
statementIndex: number;
|
|
445
|
+
period: FundamentalPeriod;
|
|
446
|
+
},
|
|
447
|
+
): number | null {
|
|
448
|
+
if (metric === "pegRatio" || statement === null) {
|
|
449
|
+
return currentValuationMetricValue(financials, metric);
|
|
450
|
+
}
|
|
123
451
|
|
|
124
|
-
|
|
125
|
-
|
|
452
|
+
if (metric === "trailingPE" || metric === "forwardPE") {
|
|
453
|
+
const price = financials ? priceAtOrBefore(financials.priceHistory, statement.date) : null;
|
|
454
|
+
const eps = context
|
|
455
|
+
? metric === "trailingPE"
|
|
456
|
+
? trailingEpsValue(context.statements, context.statementIndex, context.period)
|
|
457
|
+
: forwardEpsValue(context.statements, context.statementIndex, context.period)
|
|
458
|
+
: statementEpsValue(statement);
|
|
459
|
+
return price && eps && eps > 0 ? price / eps : null;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
const marketCap = historicalMarketCap(financials, statement);
|
|
463
|
+
const enterpriseValue = historicalEnterpriseValue(financials, statement);
|
|
126
464
|
const revenue = statement?.totalRevenue;
|
|
127
465
|
const ebitda = statement?.ebitda;
|
|
128
|
-
const freeCashFlow = statement
|
|
466
|
+
const freeCashFlow = statement ? freeCashFlowValue(statement) : null;
|
|
129
467
|
|
|
130
468
|
if (metric === "priceSales" && marketCap && revenue) return marketCap / revenue;
|
|
131
469
|
if (metric === "evSales" && enterpriseValue && revenue) return enterpriseValue / revenue;
|
|
@@ -140,33 +478,32 @@ export function buildValuationGraphRows(
|
|
|
140
478
|
symbol = "",
|
|
141
479
|
period: FundamentalPeriod = "annual",
|
|
142
480
|
): FundamentalGraphRow[] {
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
.map((statement) => ({
|
|
149
|
-
statement,
|
|
150
|
-
|
|
481
|
+
const sourceStatements = metric === "pegRatio"
|
|
482
|
+
? [{ date: "Current" } as FinancialStatement]
|
|
483
|
+
: selectedStatements(financials, period);
|
|
484
|
+
const entries = [...sourceStatements]
|
|
485
|
+
.sort((left, right) => left.date.localeCompare(right.date))
|
|
486
|
+
.map((statement, statementIndex, statements) => ({
|
|
487
|
+
date: statement.date,
|
|
488
|
+
category: statement.date === "Current" ? "Current" : periodCategory(statement.date, period),
|
|
489
|
+
value: valuationMetricValue(
|
|
490
|
+
financials,
|
|
491
|
+
metric,
|
|
492
|
+
statement.date === "Current" ? null : statement,
|
|
493
|
+
{ statements, statementIndex, period },
|
|
494
|
+
),
|
|
151
495
|
}))
|
|
152
|
-
.filter((entry): entry is
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
496
|
+
.filter((entry): entry is GraphRowEntry => finiteNumber(entry.value));
|
|
497
|
+
const currentValue = currentValuationMetricValue(financials, metric);
|
|
498
|
+
if (finiteNumber(currentValue) && !entries.some((entry) => entry.date === "Current") && (
|
|
499
|
+
metric === "pegRatio"
|
|
500
|
+
|| metric === "forwardPE"
|
|
501
|
+
|| (metric === "trailingPE" && entries.length === 0)
|
|
502
|
+
)) {
|
|
503
|
+
entries.push({ date: "Current", category: "Current", value: currentValue });
|
|
504
|
+
}
|
|
157
505
|
|
|
158
|
-
return
|
|
159
|
-
const previous = sorted[index - 1]?.value;
|
|
160
|
-
return {
|
|
161
|
-
key: [symbol, entry.statement.date, metric].filter(Boolean).join(":"),
|
|
162
|
-
symbol,
|
|
163
|
-
date: entry.statement.date,
|
|
164
|
-
category: entry.statement.date === "Current" ? "Current" : periodCategory(entry.statement.date, period),
|
|
165
|
-
value: entry.value,
|
|
166
|
-
growth: typeof previous === "number" && previous !== 0 ? (entry.value - previous) / Math.abs(previous) : null,
|
|
167
|
-
barWidth: Math.max(1, Math.round((Math.abs(entry.value) / maxAbs) * 24)),
|
|
168
|
-
};
|
|
169
|
-
});
|
|
506
|
+
return buildRowsFromEntries(entries, metric, symbol);
|
|
170
507
|
}
|
|
171
508
|
|
|
172
509
|
export function metricDefs(kind: GraphKind): ReadonlyArray<MetricDefinition> {
|
|
@@ -242,9 +579,23 @@ export function buildFundamentalColumns(width: number, multiSymbol: boolean): Fu
|
|
|
242
579
|
return columns;
|
|
243
580
|
}
|
|
244
581
|
|
|
582
|
+
function categorySortKey(row: FundamentalGraphRow): string {
|
|
583
|
+
return row.date === "Current" ? "9999-12-31" : row.date;
|
|
584
|
+
}
|
|
585
|
+
|
|
245
586
|
export function buildGraphBarSeries(rows: FundamentalGraphRow[]): BarChartSeries[] {
|
|
246
587
|
const symbols = [...new Set(rows.map((row) => row.symbol || ""))];
|
|
247
|
-
const
|
|
588
|
+
const categoryOrder = new Map<string, string>();
|
|
589
|
+
for (const row of rows) {
|
|
590
|
+
const sortKey = categorySortKey(row);
|
|
591
|
+
const previous = categoryOrder.get(row.category);
|
|
592
|
+
if (!previous || sortKey.localeCompare(previous) < 0) {
|
|
593
|
+
categoryOrder.set(row.category, sortKey);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
const categories = [...categoryOrder.entries()]
|
|
597
|
+
.sort((left, right) => left[1].localeCompare(right[1]) || left[0].localeCompare(right[0]))
|
|
598
|
+
.map(([category]) => category);
|
|
248
599
|
return symbols.map((symbol, index) => ({
|
|
249
600
|
id: symbol || "value",
|
|
250
601
|
label: symbol || "Value",
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import type { PaneTemplateCreateOptions } from "../../../../../types/plugin";
|
|
2
|
-
import { formatTickerListInput, parseTickerListInput } from "../../../../../tickers/list";
|
|
1
|
+
import type { PaneSettingsDef, PaneTemplateCreateOptions } from "../../../../../types/plugin";
|
|
2
|
+
import { formatTickerListInput, MAX_TICKER_LIST_SIZE, parseTickerListInput } from "../../../../../tickers/list";
|
|
3
3
|
import type { GraphKind } from "./types";
|
|
4
4
|
|
|
5
|
+
export const FUNDAMENTAL_GRAPH_PANE_ID = "fundamental-graph";
|
|
6
|
+
|
|
5
7
|
export function symbolsFromPaneSettings(settings: Record<string, unknown> | undefined, fallbackSymbol: string | null): string[] {
|
|
6
8
|
const symbols = settings?.symbols;
|
|
7
9
|
if (Array.isArray(symbols)) {
|
|
@@ -26,6 +28,10 @@ export function graphKindFromSettings(settings: Record<string, unknown> | undefi
|
|
|
26
28
|
return settings?.chartKind === "valuation" ? "valuation" : fallback;
|
|
27
29
|
}
|
|
28
30
|
|
|
31
|
+
export function graphShortcutForKind(kind: GraphKind): "GF" | "GE" {
|
|
32
|
+
return kind === "valuation" ? "GE" : "GF";
|
|
33
|
+
}
|
|
34
|
+
|
|
29
35
|
export function graphTemplateSymbols(
|
|
30
36
|
activeTicker: string | null,
|
|
31
37
|
options: Pick<PaneTemplateCreateOptions, "arg" | "values" | "symbols"> | undefined,
|
|
@@ -42,3 +48,18 @@ export function graphTemplateSymbols(
|
|
|
42
48
|
export function graphTemplateTitle(shortcut: "GF" | "GE", symbols: string[]): string {
|
|
43
49
|
return `${shortcut} ${formatTickerListInput(symbols)}`;
|
|
44
50
|
}
|
|
51
|
+
|
|
52
|
+
export function buildGraphPaneSettingsDef(): PaneSettingsDef {
|
|
53
|
+
return {
|
|
54
|
+
title: "Graph Pane Settings",
|
|
55
|
+
fields: [
|
|
56
|
+
{
|
|
57
|
+
key: "symbolsText",
|
|
58
|
+
label: "Tickers",
|
|
59
|
+
description: `Enter up to ${MAX_TICKER_LIST_SIZE} tickers separated by commas.`,
|
|
60
|
+
type: "text",
|
|
61
|
+
placeholder: "AMD, NVDA",
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
};
|
|
65
|
+
}
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import type { DataTableColumn } from "../../../../../components";
|
|
2
|
-
import type { TickerFinancials } from "../../../../../types/financials";
|
|
2
|
+
import type { FinancialStatement, TickerFinancials } from "../../../../../types/financials";
|
|
3
3
|
|
|
4
4
|
export type FundamentalMetricKey =
|
|
5
5
|
| "totalRevenue"
|
|
6
6
|
| "grossProfit"
|
|
7
|
+
| "grossMargin"
|
|
7
8
|
| "operatingIncome"
|
|
9
|
+
| "operatingMargin"
|
|
8
10
|
| "netIncome"
|
|
11
|
+
| "netMargin"
|
|
9
12
|
| "operatingCashFlow"
|
|
10
13
|
| "freeCashFlow"
|
|
14
|
+
| "freeCashFlowMargin"
|
|
11
15
|
| "totalAssets"
|
|
12
16
|
| "totalDebt"
|
|
13
17
|
| "totalEquity"
|
|
@@ -42,6 +46,9 @@ export type MetricDefinition<Key extends GraphMetricKey = GraphMetricKey> = {
|
|
|
42
46
|
key: Key;
|
|
43
47
|
label: string;
|
|
44
48
|
format: (value: number) => string;
|
|
49
|
+
value?: Key extends FundamentalMetricKey
|
|
50
|
+
? (statement: FinancialStatement) => number | null | undefined
|
|
51
|
+
: never;
|
|
45
52
|
};
|
|
46
53
|
|
|
47
54
|
export type SymbolFinancials = {
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
isMetricForKind,
|
|
13
13
|
} from "./fundamental-graph/model";
|
|
14
14
|
import {
|
|
15
|
+
FUNDAMENTAL_GRAPH_PANE_ID,
|
|
15
16
|
graphKindFromSettings,
|
|
16
17
|
graphTemplateSymbols,
|
|
17
18
|
graphTemplateTitle,
|
|
@@ -20,6 +21,7 @@ import {
|
|
|
20
21
|
import type { FundamentalPeriod, GraphKind, GraphMetricKey } from "./fundamental-graph/types";
|
|
21
22
|
|
|
22
23
|
export { buildFundamentalGraphRows, buildValuationGraphRows } from "./fundamental-graph/model";
|
|
24
|
+
export { buildGraphPaneSettingsDef, FUNDAMENTAL_GRAPH_PANE_ID } from "./fundamental-graph/settings";
|
|
23
25
|
|
|
24
26
|
export function FundamentalGraphPane({ focused, width, height }: PaneProps) {
|
|
25
27
|
const pane = usePaneInstance();
|
|
@@ -99,7 +101,7 @@ export function createGraphPaneTemplate({
|
|
|
99
101
|
}): PaneTemplateDef {
|
|
100
102
|
return {
|
|
101
103
|
id,
|
|
102
|
-
paneId:
|
|
104
|
+
paneId: FUNDAMENTAL_GRAPH_PANE_ID,
|
|
103
105
|
label,
|
|
104
106
|
description,
|
|
105
107
|
keywords: chartKind === "valuation"
|