gloomberb 0.18.1 → 0.18.2
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/commodity-balances.ts +6 -0
- package/src/api-client/company-kpis.ts +9 -0
- package/src/api-client/crypto-markets.ts +9 -0
- package/src/api-client/data.ts +6 -5
- package/src/api-client/equity-screener.ts +2 -0
- package/src/api-client/types.ts +5 -0
- package/src/app/pane-runtime/plugin-bindings.ts +3 -1
- package/src/app/pane-runtime/ticker-open-runtime.ts +17 -5
- package/src/app/runtime/plugin-bindings.ts +6 -2
- package/src/brokers/logos.ts +66 -0
- package/src/brokers/profile-form.ts +3 -0
- package/src/brokers/remote-broker-adapter.ts +13 -3
- package/src/capabilities/index.ts +1 -1
- package/src/capabilities/service-capabilities.ts +7 -1
- package/src/cli/base-converter.ts +60 -18
- package/src/cli/commands/market.ts +281 -154
- package/src/cli/commands/news-search.ts +35 -8
- package/src/cli/commands/system.ts +35 -1
- package/src/cli/commands/ticker.ts +100 -50
- package/src/cli/currency-hints.ts +27 -0
- package/src/cli/fx-pair.ts +41 -4
- package/src/cli/glossary.ts +57 -4
- package/src/cli/helpers.ts +36 -3
- package/src/cli/index.ts +4 -1
- package/src/cli/known-gaps.ts +37 -1
- package/src/cli/listing-arg.ts +37 -2
- package/src/cli/pane-functions/catalog.ts +3 -1
- package/src/cli/pane-functions/discovery.ts +15 -1
- package/src/cli/pane-functions/dom.ts +9 -4
- package/src/cli/pane-functions/freshness.ts +4 -4
- package/src/cli/pane-functions/function-help.ts +27 -3
- package/src/cli/pane-functions/headless.ts +6 -5
- package/src/cli/pane-functions/index.ts +3 -2
- package/src/cli/pane-functions/lock-note.ts +7 -4
- package/src/cli/pane-functions/resolver.ts +2 -1
- package/src/cli/quote-answer.ts +179 -0
- package/src/cli/series-search.ts +2 -1
- package/src/cli/types.ts +4 -0
- package/src/components/chart/composite/composite-chart.tsx +11 -2
- package/src/components/chart/composite/format.ts +13 -1
- package/src/components/chart/composite/level-labels.ts +30 -0
- package/src/components/chart/composite/levels.ts +17 -6
- package/src/components/chart/composite/panel-surface.tsx +58 -8
- package/src/components/command-bar/routes/ticker-search/actions.ts +6 -0
- package/src/components/index.ts +2 -0
- package/src/components/layout/pane/content.tsx +16 -14
- package/src/components/layout/pane/footer/index.tsx +22 -3
- package/src/components/layout/pane/footer/model.ts +99 -4
- package/src/components/layout/pane/header-tabs.tsx +15 -1
- package/src/components/onboarding/onboarding-frame.tsx +9 -11
- package/src/components/onboarding/use-onboarding-broker-form.ts +5 -2
- package/src/components/onboarding/wizard-model.ts +10 -3
- package/src/components/ui/button.tsx +11 -4
- package/src/components/ui/choice-dialog.tsx +8 -5
- package/src/components/ui/display.tsx +11 -6
- package/src/components/ui/fields.tsx +100 -20
- package/src/components/ui/icon.tsx +6 -1
- package/src/components/ui/list-row-lines.tsx +87 -0
- package/src/components/ui/list-view.tsx +14 -0
- package/src/components/ui/logo-mark.tsx +64 -0
- package/src/components/ui/number-step.ts +54 -0
- package/src/components/ui/select-button.tsx +5 -4
- package/src/components/ui/select-field.tsx +3 -3
- package/src/components/ui/status.tsx +17 -3
- package/src/components/ui/tabs.tsx +9 -1
- package/src/components/ui/toggle.tsx +17 -3
- package/src/core/state/app/instrument.ts +8 -3
- package/src/data/config/store/normalize.ts +4 -1
- package/src/data/plugin-cache.ts +32 -6
- package/src/docs/snapshot.json +5 -5
- package/src/i18n/es.ts +9 -3
- package/src/i18n/ja.ts +9 -3
- package/src/i18n/ko.ts +9 -3
- package/src/i18n/zh-cn.ts +10 -4
- package/src/i18n/zh-tw.ts +10 -4
- package/src/market-data/commodity-names.ts +80 -0
- package/src/market-data/coordinator/auxiliary.ts +3 -1
- package/src/market-data/coordinator/chart.ts +3 -1
- package/src/market-data/futures-board.ts +38 -1
- package/src/market-data/futures-contract.ts +91 -0
- package/src/market-data/fx-closes.ts +78 -1
- package/src/market-data/market/change-basis.ts +219 -0
- package/src/market-data/market/format.ts +28 -12
- package/src/market-data/market/freshness.ts +38 -0
- package/src/market-data/market/status.ts +55 -9
- package/src/market-data/market/year-to-date.ts +46 -0
- package/src/market-data/quotes/contributions.ts +12 -0
- package/src/market-data/quotes/delisting.ts +34 -0
- package/src/market-data/quotes/live-valuation.ts +23 -0
- package/src/market-data/quotes/resolution.ts +8 -2
- package/src/market-data/tax-equivalent.ts +10 -2
- package/src/notifications/app-notifier.ts +2 -2
- package/src/notifications/notification-log.ts +8 -6
- package/src/plugins/builtin/alerts/cli.ts +26 -18
- package/src/plugins/builtin/alerts/headless.ts +1 -1
- package/src/plugins/builtin/alerts/index.tsx +13 -4
- package/src/plugins/builtin/attention/trending.tsx +8 -6
- package/src/plugins/builtin/backtest/client.ts +5 -2
- package/src/plugins/builtin/backtest/window.ts +16 -3
- package/src/plugins/builtin/broker-manager/add-flow.tsx +40 -8
- package/src/plugins/builtin/broker-manager/detail.tsx +26 -1
- package/src/plugins/builtin/broker-manager/index.tsx +20 -9
- package/src/plugins/builtin/broker-manager/model.ts +5 -0
- package/src/plugins/builtin/broker-manager/pane-actions.tsx +8 -1
- package/src/plugins/builtin/broker-manager/table.ts +5 -1
- package/src/plugins/builtin/broker-trading/account-picker.tsx +91 -0
- package/src/plugins/builtin/broker-trading/chart-levels.ts +54 -0
- package/src/plugins/builtin/broker-trading/command.ts +103 -0
- package/src/plugins/builtin/broker-trading/controller.ts +688 -0
- package/src/plugins/builtin/broker-trading/dock.tsx +182 -0
- package/src/plugins/builtin/broker-trading/headless.ts +76 -0
- package/src/plugins/builtin/broker-trading/index.tsx +77 -0
- package/src/plugins/builtin/broker-trading/order-feed.ts +383 -0
- package/src/plugins/builtin/broker-trading/orders-data.ts +133 -0
- package/src/plugins/builtin/broker-trading/orders.tsx +437 -0
- package/src/plugins/builtin/broker-trading/overview-orders.tsx +121 -0
- package/src/plugins/builtin/broker-trading/pane-model.ts +207 -0
- package/src/plugins/builtin/broker-trading/pane.tsx +505 -0
- package/src/plugins/builtin/broker-trading/strategy.ts +153 -0
- package/src/plugins/builtin/broker-trading/ticket-parts.tsx +117 -0
- package/src/plugins/builtin/broker-trading/ticket.tsx +573 -0
- package/src/plugins/builtin/buildout/data-runtime.ts +1 -1
- package/src/plugins/builtin/buildout/format.ts +1 -0
- package/src/plugins/builtin/buildout/model/index.ts +16 -7
- package/src/plugins/builtin/buildout/pane/index.tsx +3 -2
- package/src/plugins/builtin/buildout/pane/status.tsx +3 -3
- package/src/plugins/builtin/chart-composer/cli-options.ts +19 -0
- package/src/plugins/builtin/chart-composer/data-catalog-pane.tsx +2 -1
- package/src/plugins/builtin/chart-composer/headless-schema.ts +3 -3
- package/src/plugins/builtin/chart-composer/headless.ts +2 -1
- package/src/plugins/builtin/chart-composer/option-range.ts +39 -0
- package/src/plugins/builtin/chart-composer/pane.tsx +31 -11
- package/src/plugins/builtin/chart-composer/presets.ts +2 -2
- package/src/plugins/builtin/chart-composer/series-catalog.ts +4 -2
- package/src/plugins/builtin/chart-composer/shortcuts.ts +3 -3
- package/src/plugins/builtin/chart-composer/viewport-labels.ts +5 -2
- package/src/plugins/builtin/chat/index.tsx +1 -1
- package/src/plugins/builtin/chat/status-widget.tsx +1 -1
- package/src/plugins/builtin/chat/unread-inbox-pane.tsx +6 -1
- package/src/plugins/builtin/chat/unread-inbox.ts +1 -1
- package/src/plugins/builtin/commodity-balances/client.ts +6 -4
- package/src/plugins/builtin/commodity-balances/headless.ts +41 -7
- package/src/plugins/builtin/commodity-balances/index.tsx +19 -4
- package/src/plugins/builtin/commodity-balances/model.ts +22 -0
- package/src/plugins/builtin/commodity-balances/pane.tsx +36 -5
- package/src/plugins/builtin/company-kpis/client.ts +3 -2
- package/src/plugins/builtin/company-kpis/headless.ts +5 -7
- package/src/plugins/builtin/company-kpis/model.ts +85 -2
- package/src/plugins/builtin/company-kpis/pane.tsx +26 -12
- package/src/plugins/builtin/composite-plugins.ts +3 -2
- package/src/plugins/builtin/cot/headless.ts +1 -1
- package/src/plugins/builtin/cot/index.tsx +1 -1
- package/src/plugins/builtin/cot/model.ts +35 -12
- package/src/plugins/builtin/credit-boards/cdx-pane.tsx +2 -1
- package/src/plugins/builtin/credit-boards/sovr-pane.tsx +2 -1
- package/src/plugins/builtin/credit-conditions/index.tsx +2 -1
- package/src/plugins/builtin/crypto-board/client.ts +33 -2
- package/src/plugins/builtin/crypto-board/headless.ts +5 -2
- package/src/plugins/builtin/crypto-board/model.ts +28 -8
- package/src/plugins/builtin/dividend-yield/headless.ts +30 -8
- package/src/plugins/builtin/dividend-yield/provider-client.ts +3 -1
- package/src/plugins/builtin/earnings-calls/headless.ts +4 -4
- package/src/plugins/builtin/earnings-calls/lock-note.ts +12 -3
- package/src/plugins/builtin/econ/calendar-model.ts +35 -0
- package/src/plugins/builtin/econ/index.tsx +31 -4
- package/src/plugins/builtin/econ/print-watch.ts +279 -0
- package/src/plugins/builtin/equity-screener/headless.ts +20 -5
- package/src/plugins/builtin/equity-screener/model.ts +40 -2
- package/src/plugins/builtin/equity-screener/pane.tsx +24 -21
- package/src/plugins/builtin/estimate-revisions/headless.ts +3 -2
- package/src/plugins/builtin/estimate-revisions/lock-note.ts +29 -7
- package/src/plugins/builtin/exposure/client.ts +4 -1
- package/src/plugins/builtin/exposure/editor.tsx +3 -3
- package/src/plugins/builtin/exposure/evidence.ts +4 -1
- package/src/plugins/builtin/exposure/holdings.ts +3 -1
- package/src/plugins/builtin/exposure/pane.tsx +37 -16
- package/src/plugins/builtin/filing-events/index.tsx +9 -3
- package/src/plugins/builtin/futures/table.tsx +6 -3
- package/src/plugins/builtin/futures-curve/pane.tsx +2 -2
- package/src/plugins/builtin/insider/base-values.ts +72 -0
- package/src/plugins/builtin/insider/headless.ts +29 -4
- package/src/plugins/builtin/jobs/headless.ts +2 -2
- package/src/plugins/builtin/jobs/lock-note.ts +14 -3
- package/src/plugins/builtin/market-halts/model.ts +31 -6
- package/src/plugins/builtin/market-halts/pane.tsx +3 -0
- package/src/plugins/builtin/market-valuation/pane.tsx +2 -1
- package/src/plugins/builtin/members/pane.tsx +8 -3
- package/src/plugins/builtin/news/wire/feed-config.ts +29 -12
- package/src/plugins/builtin/news/wire/index.tsx +7 -2
- package/src/plugins/builtin/news/wire/news/preset-pane.tsx +11 -3
- package/src/plugins/builtin/news/wire/rss/cli.ts +3 -2
- package/src/plugins/builtin/news/wire/rss/source.ts +13 -4
- package/src/plugins/builtin/options/footer.ts +25 -20
- package/src/plugins/builtin/options/live-quotes.ts +33 -18
- package/src/plugins/builtin/options/view.tsx +77 -22
- package/src/plugins/builtin/options-calculator/pane.tsx +4 -2
- package/src/plugins/builtin/options-scenario/pane.tsx +41 -6
- package/src/plugins/builtin/portfolio-list/account-metrics.ts +3 -0
- package/src/plugins/builtin/portfolio-list/cli/render.ts +34 -10
- package/src/plugins/builtin/portfolio-list/headless.ts +1 -1
- package/src/plugins/builtin/portfolio-list/pane/data.ts +23 -1
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +33 -14
- package/src/plugins/builtin/portfolio-list/settings.ts +20 -2
- package/src/plugins/builtin/portfolio-list/summary/index.tsx +12 -0
- package/src/plugins/builtin/portfolio-list/summary/totals.ts +24 -3
- package/src/plugins/builtin/primary-dealers/client.ts +137 -62
- package/src/plugins/builtin/primary-dealers/headless.ts +30 -5
- package/src/plugins/builtin/primary-dealers/index.tsx +3 -2
- package/src/plugins/builtin/primary-dealers/model.ts +179 -129
- package/src/plugins/builtin/primary-dealers/pane.tsx +135 -15
- package/src/plugins/builtin/research/analyst-model.ts +5 -1
- package/src/plugins/builtin/risk-factors/index.tsx +12 -3
- package/src/plugins/builtin/scanner/feed.ts +6 -1
- package/src/plugins/builtin/scanner/headless.ts +1 -1
- package/src/plugins/builtin/scanner/lock-note.ts +3 -2
- package/src/plugins/builtin/series-inventory.ts +1 -0
- package/src/plugins/builtin/shared/cloud-denied.ts +7 -6
- package/src/plugins/builtin/shared/cloud-upgrade.ts +10 -11
- package/src/plugins/builtin/shared/free-routes.ts +24 -0
- package/src/plugins/builtin/shared/headless-options.ts +11 -0
- package/src/plugins/builtin/shared/pro-lock-note.ts +89 -7
- package/src/plugins/builtin/shared/research-trading.tsx +62 -0
- package/src/plugins/builtin/social-mentions/headless.ts +1 -1
- package/src/plugins/builtin/social-mentions/lock-note.ts +1 -1
- package/src/plugins/builtin/themes/headless.ts +8 -2
- package/src/plugins/builtin/themes/model.ts +43 -10
- package/src/plugins/builtin/themes/pane.tsx +8 -6
- package/src/plugins/builtin/ticker-detail/overview/model.ts +44 -10
- package/src/plugins/builtin/ticker-detail/overview/option-contract.ts +64 -0
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +82 -21
- package/src/plugins/builtin/ticker-detail/pane.tsx +21 -14
- package/src/plugins/builtin/ticker-detail/quote-footer.ts +5 -1
- package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +37 -8
- package/src/plugins/builtin/time-sales/pane.tsx +2 -2
- package/src/plugins/builtin/trace-bonds/client.ts +14 -3
- package/src/plugins/builtin/trace-bonds/headless.ts +1 -1
- package/src/plugins/builtin/vol-surface/pane.tsx +2 -2
- package/src/plugins/builtin-editorial.ts +1 -1
- package/src/plugins/compat.ts +3 -0
- package/src/plugins/external-runtime.ts +5 -0
- package/src/plugins/loader.ts +6 -0
- package/src/plugins/plugin-icon.ts +61 -0
- package/src/plugins/registry/host-actions.ts +2 -1
- package/src/plugins/registry/index.ts +9 -1
- package/src/plugins/runtime/context.tsx +5 -0
- package/src/plugins/runtime/index.tsx +3 -0
- package/src/portfolio/column-values.ts +13 -4
- package/src/portfolio/cost-fx.ts +5 -28
- package/src/public/__snapshots__/public-api.test.ts.snap +5 -0
- package/src/public/broker.ts +8 -0
- package/src/public/market-data.ts +5 -0
- package/src/renderers/dom/desktop/control-styles.ts +7 -1
- package/src/renderers/dom/desktop/controls.tsx +39 -7
- package/src/renderers/dom/desktop/icons.tsx +14 -0
- package/src/renderers/dom/desktop/list-view.tsx +51 -19
- package/src/renderers/dom/desktop/menu.tsx +10 -1
- package/src/renderers/dom/desktop/select-field.tsx +8 -6
- package/src/renderers/dom/host/style.ts +6 -1
- package/src/renderers/dom/host/tabs.tsx +6 -0
- package/src/renderers/dom/styles.css +12 -5
- package/src/renderers/electrobun/bun/core-capabilities.ts +12 -0
- package/src/renderers/electrobun/bun/external-plugins.ts +3 -0
- package/src/renderers/electrobun/shared/protocol.ts +2 -0
- package/src/renderers/electrobun/view/broker-remote-client.ts +16 -0
- package/src/renderers/electrobun/view/external-plugins.ts +1 -0
- package/src/series/validate.ts +2 -2
- package/src/sources/gloomberb-cloud/index.ts +50 -17
- package/src/sources/gloomberb-cloud/normalizers.ts +4 -0
- package/src/sources/history-result.ts +3 -0
- package/src/sources/provider-errors.ts +3 -1
- package/src/sources/provider-router/history.ts +49 -6
- package/src/sources/provider-router/index.ts +2 -1
- package/src/state/app/activity.ts +13 -0
- package/src/sync/controller.ts +4 -0
- package/src/sync/core-contributors.ts +62 -11
- package/src/sync/local-only.ts +121 -0
- package/src/sync/react.ts +6 -0
- package/src/sync/types.ts +3 -0
- package/src/tickers/listing-fate.ts +27 -8
- package/src/tickers/search/index.ts +24 -3
- package/src/time-series/price-comparison.ts +35 -0
- package/src/time-series/resolve.ts +52 -11
- package/src/time-series/spec.ts +1 -1
- package/src/time-series/types.ts +5 -0
- package/src/types/broker.ts +56 -1
- package/src/types/config.ts +6 -0
- package/src/types/data-provider.ts +5 -1
- package/src/types/financials.ts +23 -0
- package/src/types/headless.ts +12 -2
- package/src/types/instrument.ts +4 -0
- package/src/types/plugin.ts +24 -6
- package/src/types/price-history.ts +6 -1
- package/src/types/trading.ts +67 -0
- package/src/ui/host.tsx +4 -0
- package/src/utils/base-currency.ts +108 -0
- package/src/utils/currency-names.ts +128 -0
- package/src/utils/currency-pair.ts +2 -1
- package/src/utils/currency-units.ts +32 -1
- package/src/utils/dom-screenshot.ts +22 -9
- package/src/utils/exchange-notation.generated.ts +360 -0
- package/src/utils/exchanges.ts +15 -322
- package/src/utils/format.ts +31 -1
- package/src/utils/instrument-identity.ts +38 -1
- package/src/utils/options.ts +46 -1
- package/src/utils/price-history.ts +4 -0
- package/src/utils/reported-money.ts +8 -4
|
@@ -2,12 +2,13 @@ import { useCallback, useMemo, useRef } from "react";
|
|
|
2
2
|
import { isAccessDenied } from "../../../api-client/errors";
|
|
3
3
|
import { usePlanAccess } from "../../../api-client/plan-access";
|
|
4
4
|
import type { ExposureEvidence, ExposurePayload } from "../../../api-client/exposure";
|
|
5
|
-
import { ActionRow, ChartTableHeader, DataTableView, EmptyState, KeyValueRow, PageStackView, PaneStatusBody, QueryBar, SectionHeading, usePaneNoticeFooter, usePaneStatusFooter, usePaneTabs, type DataTableCell, type DataTableColumn, type PaneHint, type StatItem } from "../../../components";
|
|
5
|
+
import { ActionRow, Button, ChartTableHeader, DataTableView, EmptyState, KeyValueRow, PageStackView, PaneStatusBody, QueryBar, SectionHeading, usePaneNoticeFooter, usePaneStatusFooter, usePaneTabs, type DataTableCell, type DataTableColumn, type PaneHint, type StatItem } from "../../../components";
|
|
6
6
|
import { usePaneRefreshKey } from "../../../components/data-table/table-pane";
|
|
7
7
|
import { useAppSelector, useAsyncResource, useAutoRefresh, useMarketData, usePaneInstance, usePaneSettingValue, usePluginAppActions, usePluginPaneState } from "../../../public/react";
|
|
8
8
|
import { useThemeColors } from "../../../theme/theme-context";
|
|
9
9
|
import type { PaneProps } from "../../../types/plugin";
|
|
10
10
|
import { Box, ScrollBox, Text, useRendererHost, useUiCapabilities } from "../../../ui";
|
|
11
|
+
import { logFailure } from "../../../utils/log-failure";
|
|
11
12
|
import { SignInWall } from "../cloud/auth-actions";
|
|
12
13
|
import { CLOUD_PLAN_KEY, useCloudUpgradeAction } from "../shared/cloud-upgrade";
|
|
13
14
|
import { useOpenDescription } from "../shared/open-description";
|
|
@@ -82,14 +83,32 @@ export function ExposurePane({ width, height, focused }: PaneProps) {
|
|
|
82
83
|
const { createPaneFromTemplate } = usePluginAppActions();
|
|
83
84
|
const openDescription = useOpenDescription();
|
|
84
85
|
const [openingScenario] = usePaneSettingValue("scenario", "taiwan-disruption");
|
|
85
|
-
const [customSetting] = usePaneSettingValue("customScenario", "");
|
|
86
|
+
const [customSetting, setCustomSetting] = usePaneSettingValue("customScenario", "");
|
|
86
87
|
const [openingDepth] = usePaneSettingValue("depth", 2);
|
|
87
88
|
const [openingNav] = usePaneSettingValue("nav", "");
|
|
88
89
|
const [openingCash] = usePaneSettingValue("cash", "");
|
|
89
|
-
const [snapshotSetting] = usePaneSettingValue<ExposurePayload | null>("exposureSnapshot", null);
|
|
90
|
-
const
|
|
90
|
+
const [snapshotSetting, setSnapshotSetting] = usePaneSettingValue<ExposurePayload | null>("exposureSnapshot", null);
|
|
91
|
+
const savedSnapshot = useMemo(() => {
|
|
92
|
+
try { return { data: snapshotSetting === null ? null : validateExposure(snapshotSetting), error: null }; } catch (error) {
|
|
93
|
+
return logFailure("exposure.pane", "Read saved exposure snapshot failed", {
|
|
94
|
+
data: null, error: "Cannot read the saved exposure snapshot. Repair or reset it before analyzing.",
|
|
95
|
+
})(error);
|
|
96
|
+
}
|
|
97
|
+
}, [snapshotSetting]);
|
|
98
|
+
const snapshot = savedSnapshot.data;
|
|
91
99
|
const initialInput = snapshot ? snapshot.holdings.map(h => `${h.symbol}=${h.weight}`).join(" ") : instance?.params?.input ?? instance?.params?.arg ?? "";
|
|
92
|
-
const
|
|
100
|
+
const savedScenario = useMemo(() => {
|
|
101
|
+
try { return { data: snapshot?.scenario ?? (customSetting ? parseCustomScenario(customSetting) : DEFAULT_SCENARIO), error: null }; } catch (error) {
|
|
102
|
+
return logFailure("exposure.pane", "Read custom exposure scenario failed", {
|
|
103
|
+
data: null, error: "Cannot read the custom scenario. Repair or reset it before analyzing.",
|
|
104
|
+
})(error);
|
|
105
|
+
}
|
|
106
|
+
}, [snapshot, customSetting]);
|
|
107
|
+
const inputError = savedSnapshot.error ?? savedScenario.error;
|
|
108
|
+
// A repaired setting must replace any stale scenario in persisted editor state.
|
|
109
|
+
const repairingCustomSetting = useRef(false);
|
|
110
|
+
if (savedScenario.error) repairingCustomSetting.current = true;
|
|
111
|
+
const initialScenario = savedScenario.data ?? DEFAULT_SCENARIO;
|
|
93
112
|
const [inputs, setInputs] = usePluginPaneState<ExposureInputs>("exposure:inputs", { input: initialInput, source: /^(PORT|WATCH):/.test(initialInput) ? initialInput : "typed", nav: String(openingNav), cash: String(openingCash), depth: snapshot?.depth ?? Number(openingDepth), scenario: initialScenario });
|
|
94
113
|
const [scenarioId, setScenarioId] = usePluginPaneState("exposure:scenario", snapshot ? snapshot.scenario.id ?? "captured" : customSetting ? "custom" : openingScenario);
|
|
95
114
|
const [openingTab] = usePaneSettingValue("tab", "table");
|
|
@@ -98,11 +117,11 @@ export function ExposurePane({ width, height, focused }: PaneProps) {
|
|
|
98
117
|
const [view, setView] = usePluginPaneState("exposure:view", openingView);
|
|
99
118
|
const [selectedId, setSelected] = usePluginPaneState<string | null>("exposure:selected", null);
|
|
100
119
|
const [detailId, setDetail] = usePluginPaneState<string | null>("exposure:detail", null);
|
|
101
|
-
const [editing, setEditing] = usePluginPaneState("exposure:editing", !initialInput && !snapshot);
|
|
120
|
+
const [editing, setEditing] = usePluginPaneState("exposure:editing", !initialInput && !snapshot && !inputError);
|
|
102
121
|
const [sort, setSort] = usePluginPaneState("exposure:sort", { column: "exposure", direction: "desc" as "asc" | "desc" });
|
|
103
122
|
const libraryLoader = useCallback(() => snapshot ? Promise.resolve([snapshot.scenario]) : fetchScenarios(), [snapshot, accessKey]);
|
|
104
|
-
const library = useAsyncResource(libraryLoader, { clearOnError: isAccessDenied });
|
|
105
|
-
const scenario = snapshot?.scenario ?? (scenarioId === "custom" ? inputs.scenario : library.data?.find(s => s.id === scenarioId) ?? (scenarioId === DEFAULT_SCENARIO.id ? DEFAULT_SCENARIO : null));
|
|
123
|
+
const library = useAsyncResource(inputError ? null : libraryLoader, { clearOnError: isAccessDenied });
|
|
124
|
+
const scenario = snapshot?.scenario ?? (scenarioId === "custom" ? repairingCustomSetting.current && customSetting ? savedScenario.data : inputs.scenario : library.data?.find(s => s.id === scenarioId) ?? (scenarioId === DEFAULT_SCENARIO.id ? DEFAULT_SCENARIO : null));
|
|
106
125
|
const portfolio = config.portfolios.find(p => inputs.source === `PORT:${p.id}`);
|
|
107
126
|
const localTickers = useMemo(() => [...tickers.values()], [tickers]);
|
|
108
127
|
// Freeze a local selection until the user explicitly refreshes. Tick-by-tick marks must not rerun scenarios.
|
|
@@ -126,10 +145,10 @@ export function ExposurePane({ width, height, focused }: PaneProps) {
|
|
|
126
145
|
return fetchExposure({ holdings, scenario, depth: inputs.depth, ...(cash === undefined ? {} : { cashWeight: cash }) });
|
|
127
146
|
// Explicit request state, including the account, owns recalculation. Quote changes do not.
|
|
128
147
|
}, [sourceKey, snapshot]);
|
|
129
|
-
const resource = useAsyncResource(inputs.input || inputs.source !== "typed" || snapshot ? loader : null, { clearOnError: isAccessDenied });
|
|
148
|
+
const resource = useAsyncResource(!inputError && (inputs.input || inputs.source !== "typed" || snapshot) ? loader : null, { clearOnError: isAccessDenied });
|
|
130
149
|
const data = resource.data;
|
|
131
150
|
useAutoRefresh(resource.updatedAt, resource.load);
|
|
132
|
-
usePaneRefreshKey(() => void resource.reload(), { focused: focused && !editing });
|
|
151
|
+
usePaneRefreshKey(() => void resource.reload(), { focused: focused && !editing && !inputError });
|
|
133
152
|
const rawRows = useMemo(() => !data ? [] : tab === "drivers" ? driverRows(data) : tab === "paths" ? pathRows(data) : tab === "portfolio" ? portfolioRows(data, view) : tableRows(data), [data, tab, view]);
|
|
134
153
|
const rows = useMemo(() => [...rawRows].sort((a, b) => {
|
|
135
154
|
const get = (r: ExposureRow) => sort.column === "value" ? r.driver?.value ?? r.driver?.range?.high ?? -Infinity : sort.column === "units" ? r.driver ? driverUnits(r.driver) : "" : sort.column === "scope" ? r.driver ? driverScope(r.driver) : "" : sort.column === "classification" ? r.classification : sort.column === "label" ? r.label : sort.column === "period" ? r.period ?? "" : sort.column === "exposure" ? r.exposure?.high ?? -Infinity : sort.column === "impact" ? r.impact?.low ?? -Infinity : sort.column === "weight" ? r.weight ?? -Infinity : sort.column === "basis" ? r.basis ?? "" : r.symbol;
|
|
@@ -141,12 +160,12 @@ export function ExposurePane({ width, height, focused }: PaneProps) {
|
|
|
141
160
|
const activeRow = detail ?? selected;
|
|
142
161
|
const source = activeRow?.components.flatMap(c => [...c.evidence, ...c.path.flatMap(h => h.evidence)]).find(e => e.url)?.url;
|
|
143
162
|
const symbol = activeRow?.symbol.split(",")[0]?.trim();
|
|
144
|
-
const detailOpen = editing || !!detail || detailId === "unknowns";
|
|
163
|
+
const detailOpen = !savedSnapshot.error && (editing || (!inputError && (!!detail || detailId === "unknowns")));
|
|
145
164
|
const { strip, rows: tabRows } = usePaneTabs({ tabs: TABS, activeValue: tab, onSelect: value => { setTab(value); setDetail(null); }, focused: focused && !editing, dense: true });
|
|
146
165
|
useExposureEvidence(data, tab, view, rows.map(r => r.id));
|
|
147
|
-
const hints: PaneHint[] = editing ? [] : [
|
|
166
|
+
const hints: PaneHint[] = editing || savedSnapshot.error ? [] : [
|
|
148
167
|
{ id: "edit", key: "s", label: "cenario", title: "Edit scenario and holdings", onPress: () => setEditing(true) },
|
|
149
|
-
{ id: "unknowns", key: "v", label: "isibility", title: "What we could not see", onPress: () => setDetail("unknowns") },
|
|
168
|
+
...(!inputError ? [{ id: "unknowns", key: "v", label: "isibility", title: "What we could not see", onPress: () => setDetail("unknowns") }] : []),
|
|
150
169
|
...(selected ? [{ id: "evidence", key: "e", label: "vidence", onPress: () => setDetail(selected.id) }] : []),
|
|
151
170
|
...(source ? [{ id: "source", key: "o", label: "pen source", onPress: () => void host.openExternal(source) }] : []),
|
|
152
171
|
...(symbol ? [
|
|
@@ -158,7 +177,7 @@ export function ExposurePane({ width, height, focused }: PaneProps) {
|
|
|
158
177
|
] : []),
|
|
159
178
|
...(data?.access === "preview" ? [{ id: "upgrade", key: CLOUD_PLAN_KEY, label: "upgrade", title: "Upgrade to Pro", onPress: upgrade }] : []),
|
|
160
179
|
];
|
|
161
|
-
usePaneStatusFooter({ registrationId: "exposure", loading: resource.loading, error: data ? resource.error : null, hints,
|
|
180
|
+
usePaneStatusFooter({ registrationId: "exposure", loading: resource.loading, error: inputError ?? (data ? resource.error : null), hints,
|
|
162
181
|
info: data ? [{ id: "asof", parts: [{ text: `analyzed ${data.evaluatedAt.slice(0, 10)}`, tone: "muted" as const }] }] : [] });
|
|
163
182
|
usePaneNoticeFooter({ registrationId: "exposure:notices", focused, notices: [...(library.error ? [library.error] : []), ...(data?.unknowns ?? []), ...(data?.holdings.flatMap(h => h.unknowns.map(u => `${h.symbol}: ${u}`)) ?? [])] });
|
|
164
183
|
const sources = [{ value: "typed", label: "Typed holdings" }, ...config.portfolios.map(p => ({ value: `PORT:${p.id}`, label: `PORT · ${p.name}` })), ...config.watchlists.map(w => ({ value: `WATCH:${w.id}`, label: `Watchlist · ${w.name}` }))];
|
|
@@ -231,8 +250,10 @@ export function ExposurePane({ width, height, focused }: PaneProps) {
|
|
|
231
250
|
{strip}
|
|
232
251
|
<PageStackView focused={focused && !editing} detailOpen={detailOpen} onBack={() => { setDetail(null); setEditing(false); }}
|
|
233
252
|
detailTitle={editing ? "Scenario and holdings" : detail ? `${detail.symbol} · ${detail.label}` : "What we could not see"}
|
|
234
|
-
detailContent={editing ? <ExposureEditor initial={{ ...inputs, scenario: scenario ?? inputs.scenario }} sources={sources} focused={focused} width={width} onCancel={() => setEditing(false)} onSave={value => { setInputs(value); setScenarioId("custom"); setEditing(false); setDetail(null); }} /> : data ? <ExposureDetail row={detail} data={data} width={width} /> : null}
|
|
235
|
-
rootContent={<PaneStatusBody loading={!data && resource.loading} error={!data ? resource.error : null} subject="exposure analysis"
|
|
253
|
+
detailContent={editing && !savedSnapshot.error ? <ExposureEditor initial={{ ...inputs, scenario: scenario ?? inputs.scenario }} initialCustom={savedScenario.error ? customSetting : undefined} sources={sources} focused={focused} width={width} onCancel={() => setEditing(false)} onSave={value => { if (repairingCustomSetting.current && customSetting) setCustomSetting(JSON.stringify(value.scenario)); setInputs(value); setScenarioId("custom"); setEditing(false); setDetail(null); }} /> : data ? <ExposureDetail row={detail} data={data} width={width} /> : null}
|
|
254
|
+
rootContent={<PaneStatusBody loading={!data && resource.loading} error={inputError ?? (!data ? resource.error : null)} subject="exposure analysis"
|
|
255
|
+
actions={savedSnapshot.error ? <Button label="Reset saved snapshot" onPress={() => { setSnapshotSetting(null); setEditing(false); setDetail(null); }} />
|
|
256
|
+
: savedScenario.error ? <><Button label="Edit scenario" onPress={() => setEditing(true)} /><Button label="Reset custom scenario" onPress={() => { setCustomSetting(""); setInputs({ ...inputs, scenario: DEFAULT_SCENARIO }); setScenarioId(DEFAULT_SCENARIO.id!); setEditing(false); setDetail(null); }} /></> : undefined}>
|
|
236
257
|
{data ? <DataTableView<ExposureRow> columns={columns} items={items} getItemKey={r => r.id} rootWidth={width} rootHeight={bodyHeight} focused={focused && !detailOpen}
|
|
237
258
|
selection={{ kind: "id", selectedId: selected?.id ?? null, getId: r => r.id, onChange: (id) => { if (!id.startsWith("locked:")) setSelected(id); } }}
|
|
238
259
|
onActivate={row => row.id.startsWith("locked:") ? upgrade() : setDetail(row.id)} selectedTextOverridesCellColor showHorizontalScrollbar
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { PluginModule } from "../plugin-module";
|
|
2
|
+
import type { PaneLock } from "../../../types/plugin";
|
|
3
|
+
import { filesForm } from "../shared/free-routes";
|
|
2
4
|
import { proLockNote } from "../shared/pro-lock-note";
|
|
3
5
|
import { createTickerSurfacePaneTemplate } from "../shared/ticker-surface";
|
|
4
6
|
import { FILING_EVENTS_PANE_ID, FilingEventsPane } from "./pane";
|
|
@@ -11,14 +13,18 @@ const description =
|
|
|
11
13
|
* EK is Pro whole (`useProFeatureWall`): signed out it shows the sign-in wall
|
|
12
14
|
* and on a free account the Pro wall, never a partial feed. The 8-Ks themselves
|
|
13
15
|
* are in SEC, which needs no account: its rows carry each 8-K's filing date,
|
|
14
|
-
* acceptance time and items, by number and in words.
|
|
16
|
+
* acceptance time and items, by number and in words. A company filing no 8-Ks
|
|
17
|
+
* (ASML files 6-Ks) gets no route.
|
|
15
18
|
*/
|
|
16
|
-
function filingEventsLockNote(lock:
|
|
19
|
+
function filingEventsLockNote(lock: PaneLock): Promise<string> {
|
|
17
20
|
return proLockNote(lock, {
|
|
18
21
|
token: "EK",
|
|
19
22
|
does: "it reads each 8-K and sums up what it said and who it names",
|
|
20
23
|
shows: "them",
|
|
21
|
-
free:
|
|
24
|
+
free: [{
|
|
25
|
+
text: (ticker, company) => `gloomberb fn SEC ${ticker} lists ${company}'s recent filings, 8-Ks among them, each with its filing date and its items by number and name.`,
|
|
26
|
+
answers: (symbol, context) => filesForm(symbol, "8-K", context),
|
|
27
|
+
}],
|
|
22
28
|
});
|
|
23
29
|
}
|
|
24
30
|
|
|
@@ -3,6 +3,7 @@ import { colors, priceColor } from "../../../theme/colors";
|
|
|
3
3
|
import type { Quote } from "../../../types/financials";
|
|
4
4
|
import { TextAttributes } from "../../../ui";
|
|
5
5
|
import { formatCompact, formatNumber, formatPercentRaw } from "../../../utils/format";
|
|
6
|
+
import { minorUnitSign } from "../../../utils/currency-units";
|
|
6
7
|
import { isBoardRowLoading, renderQuoteBoardCell, type BoardQuoteMap } from "../shared/use-quote-board";
|
|
7
8
|
import { tickDecimals, type FuturesContract } from "../../../market-data/futures-board";
|
|
8
9
|
import { FUTURES_RETURN_COLUMNS, futuresContractHeadline, futuresUnitLabel, type FuturesColumnId, type FuturesTableRow } from "./model";
|
|
@@ -164,13 +165,15 @@ const currencySymbols = new Map<string, string>();
|
|
|
164
165
|
/**
|
|
165
166
|
* The board prints dollar prices bare. Anything else carries its unit on the
|
|
166
167
|
* price and on the change, so Dutch TTF gas at 72.071 euros is not read as
|
|
167
|
-
* dollars beside Henry Hub:
|
|
168
|
-
* currencies as their
|
|
168
|
+
* dollars beside Henry Hub: a minor unit as its sign after the amount (US
|
|
169
|
+
* cents 312.35¢, as every price in cents reads), other currencies as their
|
|
170
|
+
* symbol.
|
|
169
171
|
*/
|
|
170
172
|
function currencyMarks(currency: string | undefined): { prefix: string; suffix: string } {
|
|
171
173
|
const code = currency?.trim().toUpperCase() ?? "";
|
|
172
174
|
if (!code || code === "USD") return { prefix: "", suffix: "" };
|
|
173
|
-
|
|
175
|
+
const minor = minorUnitSign(currency);
|
|
176
|
+
if (minor) return { prefix: "", suffix: minor };
|
|
174
177
|
let symbol = currencySymbols.get(code);
|
|
175
178
|
if (symbol === undefined) {
|
|
176
179
|
try {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
-
import { chartTableChromeRows, ChartTableHeader, CurveSurface, curveGhostColors, DataTableView, Notice, PaneStatusBody, QueryBar, useChartTableSelection, usePaneNoticeFooter, usePaneStatusFooter, usePaneTabs, type DataTableColumn, type PaneFooterSegment, type StatItem } from "../../../components";
|
|
2
|
+
import { splitDelayedStatus, chartTableChromeRows, ChartTableHeader, CurveSurface, curveGhostColors, DataTableView, Notice, PaneStatusBody, QueryBar, useChartTableSelection, usePaneNoticeFooter, usePaneStatusFooter, usePaneTabs, type DataTableColumn, type PaneFooterSegment, type StatItem } from "../../../components";
|
|
3
3
|
import { fitChartTableColumns } from "../../../components/chart-table";
|
|
4
4
|
import { curveStrip, curveSurfaceMinRows } from "../../../components/chart/curve";
|
|
5
5
|
import { isAccessDenied } from "../../../api-client/errors";
|
|
@@ -225,7 +225,7 @@ function FuturesCurveView({ width, height, focused, root, ownsTitle }: PaneProps
|
|
|
225
225
|
...requestedDate ? [{ id: "latest", key: "c", label: "urrent", title: "Current Curve", onPress: () => selectDate("") }] : [],
|
|
226
226
|
],
|
|
227
227
|
info: data ? [
|
|
228
|
-
{ id: "source", parts: [{ text: `${requestedDate ? "daily archive" : data.source === "cboe" ? "settlement" : delay > 0 ? `${delay}m delayed` : "dated quotes"} · ${unit ?? "units unavailable"} · ${curveTimestamp(newest)}${newest?.includes("T") ? " UTC" : ""}`, tone: "muted" }] },
|
|
228
|
+
...splitDelayedStatus({ id: "source", parts: [{ text: `${requestedDate ? "daily archive" : data.source === "cboe" ? "settlement" : delay > 0 ? `${delay}m delayed` : "dated quotes"} · ${unit ?? "units unavailable"} · ${curveTimestamp(newest)}${newest?.includes("T") ? " UTC" : ""}`, tone: "muted" }] }),
|
|
229
229
|
...(staleCount ? [{ id: "stale", parts: [{ text: `${staleCount} of ${data.contracts.length} stale`, tone: "warning" as const }] }] : []),
|
|
230
230
|
...(spot ? [spot.status === "ok" ? spotSegment(spot, root, clock)
|
|
231
231
|
: { id: "spot", parts: [{ text: `Basis blank: ${spot.reason}`, tone: "warning" as const }] }] : []),
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { HeadlessPaneColumn, HeadlessPaneRow } from "../../../types/headless";
|
|
2
|
+
import { fxCloseOnOrBefore, loadDailyFxCloses, type FxCloses, type FxHistoryClient } from "../../../market-data/fx-closes";
|
|
3
|
+
import { logFailure } from "../../../utils/log-failure";
|
|
4
|
+
import { formatCompact, formatNumber } from "../../../utils/format";
|
|
5
|
+
|
|
6
|
+
/** A Form 4 states its prices and values in US dollars. */
|
|
7
|
+
export const FORM4_CURRENCY = "USD";
|
|
8
|
+
|
|
9
|
+
/** The rows with their currency, and for a base currency other than USD each value in it, with the columns and note that say so. */
|
|
10
|
+
export interface InsiderBaseValues {
|
|
11
|
+
rows: HeadlessPaneRow[];
|
|
12
|
+
/** The money columns labelled with their currency, and the converted value and its rate; null for a USD base, whose columns stay as they are. */
|
|
13
|
+
columns: HeadlessPaneColumn[] | null;
|
|
14
|
+
note: string | null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const dash = (value: unknown, format: (value: number) => string) => (typeof value === "number" && Number.isFinite(value) ? format(value) : "-");
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Every row carries the currency its price and value are in (USD, as a Form 4
|
|
21
|
+
* states them). With another base currency (CAD), each value is also given
|
|
22
|
+
* in it at the daily close of USD/CAD on its transaction day, or the last
|
|
23
|
+
* close before a weekend or holiday (`fxCloseOnOrBefore` over the history
|
|
24
|
+
* PORT and PEB read), with the rate and that close's date beside it. When the
|
|
25
|
+
* closes cannot be read the values stay in USD and the note says why.
|
|
26
|
+
*/
|
|
27
|
+
export async function withInsiderBaseValues(
|
|
28
|
+
rows: readonly HeadlessPaneRow[],
|
|
29
|
+
baseCurrency: string,
|
|
30
|
+
client: FxHistoryClient,
|
|
31
|
+
now = new Date(),
|
|
32
|
+
): Promise<InsiderBaseValues> {
|
|
33
|
+
const labelled: HeadlessPaneRow[] = rows.map((row) => ({ ...row, currency: FORM4_CURRENCY }));
|
|
34
|
+
const base = baseCurrency.trim().toUpperCase();
|
|
35
|
+
if (!base || base === FORM4_CURRENCY) return { rows: labelled, columns: null, note: null };
|
|
36
|
+
const pair = `${FORM4_CURRENCY}/${base}`;
|
|
37
|
+
const days = labelled.flatMap((row) => typeof row.transactionDate === "string" && typeof row.totalValue === "number"
|
|
38
|
+
? [row.transactionDate.slice(0, 10)] : []).sort();
|
|
39
|
+
let closes: FxCloses | null = null;
|
|
40
|
+
if (days.length > 0) {
|
|
41
|
+
try {
|
|
42
|
+
closes = await loadDailyFxCloses(base, days[0]!, client, now);
|
|
43
|
+
} catch (error) {
|
|
44
|
+
logFailure("insider.base-values", `Daily ${pair} closes failed`)(error);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const converted = labelled.map((row) => {
|
|
48
|
+
const day = typeof row.transactionDate === "string" ? row.transactionDate.slice(0, 10) : null;
|
|
49
|
+
const reading = closes && day && typeof row.totalValue === "number" ? fxCloseOnOrBefore(closes, day) : null;
|
|
50
|
+
// The close is USD per unit of the base currency; the rate is the base currency per dollar.
|
|
51
|
+
const fxRate = reading ? 1 / reading.rate : null;
|
|
52
|
+
return {
|
|
53
|
+
...row,
|
|
54
|
+
baseCurrency: base,
|
|
55
|
+
valueBase: fxRate != null ? (row.totalValue as number) * fxRate : null,
|
|
56
|
+
fxRate,
|
|
57
|
+
fxDate: reading?.date ?? null,
|
|
58
|
+
};
|
|
59
|
+
});
|
|
60
|
+
const columns: HeadlessPaneColumn[] = [
|
|
61
|
+
{ key: "pricePerShare", header: `Price (${FORM4_CURRENCY})`, align: "right", format: (value) => dash(value, (price) => formatNumber(price, 2)) },
|
|
62
|
+
{ key: "totalValue", header: `Value (${FORM4_CURRENCY})`, align: "right", format: (value) => dash(value, formatCompact) },
|
|
63
|
+
...closes ? [
|
|
64
|
+
{ key: "valueBase", header: `Value (${base})`, align: "right" as const, format: (value: unknown) => dash(value, formatCompact) },
|
|
65
|
+
{ key: "fxRate", header: pair, align: "right" as const, stackPriority: 1, format: (value: unknown) => dash(value, (rate) => formatNumber(rate, 4)) },
|
|
66
|
+
] : [],
|
|
67
|
+
];
|
|
68
|
+
const note = days.length === 0 ? null
|
|
69
|
+
: closes ? `Value (${base}) is each value at the ${pair} daily close of its transaction day, or of the trading day before it; ${pair} is that rate.`
|
|
70
|
+
: `Daily ${pair} closes are unavailable, so values stay in ${FORM4_CURRENCY}.`;
|
|
71
|
+
return { rows: closes ? converted : labelled, columns, note };
|
|
72
|
+
}
|
|
@@ -8,6 +8,7 @@ import type {
|
|
|
8
8
|
import { formatCompact, formatCurrency } from "../../../utils/format";
|
|
9
9
|
import { loadParsedInsiderFilings, type InsiderFilingsLoad } from "./client";
|
|
10
10
|
import { insiderEmptyNotice } from "./issuer";
|
|
11
|
+
import { FORM4_CURRENCY, withInsiderBaseValues } from "./base-values";
|
|
11
12
|
import {
|
|
12
13
|
buildInsiderRows,
|
|
13
14
|
buildInsiderSummary,
|
|
@@ -58,6 +59,7 @@ import {
|
|
|
58
59
|
insiderUsualRatio,
|
|
59
60
|
} from "./signals-model";
|
|
60
61
|
import { REPORT_ROWS } from "../shared/headless-options";
|
|
62
|
+
import type { FxHistoryClient } from "../../../market-data/fx-closes";
|
|
61
63
|
|
|
62
64
|
const INSIDER_COLUMNS: HeadlessPaneColumn[] = [
|
|
63
65
|
{ key: "form", header: "Form" },
|
|
@@ -106,6 +108,20 @@ const INSIDER_COLUMNS: HeadlessPaneColumn[] = [
|
|
|
106
108
|
{ key: "note", header: "Note", underRow: true, format: (value) => typeof value === "string" ? value : "" },
|
|
107
109
|
];
|
|
108
110
|
|
|
111
|
+
/** With a base currency other than USD, the dollar columns say they are in USD ("Bought (USD)"), as $ alone is several currencies' sign. */
|
|
112
|
+
function labelDollarColumns(columns: HeadlessPaneColumn[], keys: readonly string[], baseCurrency: string): HeadlessPaneColumn[] {
|
|
113
|
+
if (baseCurrency.trim().toUpperCase() === FORM4_CURRENCY) return columns;
|
|
114
|
+
return columns.map((column) => keys.includes(column.key) ? { ...column, header: `${column.header} (${FORM4_CURRENCY})` } : column);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** The trades' columns with the money ones labelled by currency and the converted value and rate after them (`withInsiderBaseValues`). */
|
|
118
|
+
function withMoneyColumns(money: readonly HeadlessPaneColumn[]): HeadlessPaneColumn[] {
|
|
119
|
+
const byKey = new Map(money.map((column) => [column.key, column]));
|
|
120
|
+
return INSIDER_COLUMNS.flatMap((column) => column.key === "totalValue"
|
|
121
|
+
? money.filter((entry) => entry.key !== "pricePerShare")
|
|
122
|
+
: [byKey.get(column.key) ?? column]);
|
|
123
|
+
}
|
|
124
|
+
|
|
109
125
|
const figureOrDash = (value: unknown) => (typeof value === "number" ? value : null);
|
|
110
126
|
|
|
111
127
|
/**
|
|
@@ -183,6 +199,7 @@ function insidersRows(activity: InsiderCompanyActivity, side: InsiderSignalSide)
|
|
|
183
199
|
stakeBothWays: line.buyer.twoWay === true,
|
|
184
200
|
bought: line.bought,
|
|
185
201
|
sold: line.sold,
|
|
202
|
+
currency: FORM4_CURRENCY,
|
|
186
203
|
shares: line.buyer.shares ?? null,
|
|
187
204
|
last: line.buyer.lastBuy,
|
|
188
205
|
}));
|
|
@@ -211,6 +228,7 @@ function signalRows(payload: InsiderSignalsPayload): HeadlessPaneRow[] {
|
|
|
211
228
|
insiders: row.insiders,
|
|
212
229
|
buys: row.buys,
|
|
213
230
|
value: row.value,
|
|
231
|
+
currency: FORM4_CURRENCY,
|
|
214
232
|
shares: row.shares,
|
|
215
233
|
firstBuy: row.firstBuy,
|
|
216
234
|
lastBuy: row.lastBuy,
|
|
@@ -251,6 +269,8 @@ function amendmentNote(entry: ParsedInsiderFiling): string {
|
|
|
251
269
|
}
|
|
252
270
|
|
|
253
271
|
export interface InsiderHeadlessDependencies {
|
|
272
|
+
/** Daily FX closes for values in a base currency other than USD; the report's API client unless a test stands in. */
|
|
273
|
+
fxHistory?: FxHistoryClient;
|
|
254
274
|
loadSignals?(query: InsiderSignalsQuery, limit: number, ctx: HeadlessPaneContext): Promise<InsiderSignalsPayload>;
|
|
255
275
|
loadCompany?(query: InsiderCompanyQuery, ctx: HeadlessPaneContext): Promise<InsiderCompanyActivity>;
|
|
256
276
|
loadParsed(
|
|
@@ -441,9 +461,9 @@ export function createInsiderHeadless(
|
|
|
441
461
|
const payload = await (dependencies.loadSignals ?? defaultDependencies.loadSignals!)(query, Number(args.options.limit), ctx);
|
|
442
462
|
const side = payload.side ?? "buy";
|
|
443
463
|
return {
|
|
444
|
-
columns: signalColumns(side),
|
|
464
|
+
columns: labelDollarColumns(signalColumns(side), ["value"], ctx.config.baseCurrency),
|
|
445
465
|
// The exports carry each row's insiders; the text leaves them to fn INS <ticker> --view insiders.
|
|
446
|
-
textColumns: signalColumns(side).filter((column) => column.key !== "buyers"),
|
|
466
|
+
textColumns: labelDollarColumns(signalColumns(side).filter((column) => column.key !== "buyers"), ["value"], ctx.config.baseCurrency),
|
|
447
467
|
rows: signalRows(payload),
|
|
448
468
|
...(payload.rows.length === 0 ? { empty: insiderSignalsEmptyTitle(payload.window, side, insiderSignalsFiltered(query)) } : {}),
|
|
449
469
|
metadata: {
|
|
@@ -472,7 +492,7 @@ export function createInsiderHeadless(
|
|
|
472
492
|
.filter((notice): notice is string => !!notice))];
|
|
473
493
|
const rows = insidersRows(activity, side);
|
|
474
494
|
return {
|
|
475
|
-
columns: INSIDERS_COLUMNS,
|
|
495
|
+
columns: labelDollarColumns(INSIDERS_COLUMNS, ["bought", "sold"], ctx.config.baseCurrency),
|
|
476
496
|
rows,
|
|
477
497
|
...(rows.length === 0 ? { empty: insiderActivityEmptyTitle(window) } : {}),
|
|
478
498
|
metadata: {
|
|
@@ -503,12 +523,17 @@ export function createInsiderHeadless(
|
|
|
503
523
|
.filter((row) => row.status !== "parsed" && row.status !== "disclosure")
|
|
504
524
|
.map((row) => row.accessionNumber))];
|
|
505
525
|
// A filing's lines sit together, so its note goes once, under the last of them.
|
|
506
|
-
const
|
|
526
|
+
const filed: HeadlessPaneRow[] = buildInsiderRows(filtered, parsed).map((row, index) => {
|
|
507
527
|
const entry = filtered[index]!;
|
|
508
528
|
const last = filtered[index + 1]?.filing.accessionNumber !== entry.filing.accessionNumber;
|
|
509
529
|
return isAmendedInsiderFiling(entry) && last ? { ...row, note: amendmentNote(entry) || null } : row;
|
|
510
530
|
});
|
|
531
|
+
// Every value says its currency; a base currency other than USD gets each value converted beside it.
|
|
532
|
+
const based = await withInsiderBaseValues(filed, ctx.config.baseCurrency, dependencies.fxHistory ?? ctx.apiClient);
|
|
533
|
+
const rows = based.rows;
|
|
511
534
|
return {
|
|
535
|
+
...(based.columns ? { columns: withMoneyColumns(based.columns) } : {}),
|
|
536
|
+
...(based.note ? { notes: [based.note] } : {}),
|
|
512
537
|
rows,
|
|
513
538
|
// Nothing at all: the company has no Form 4 of its own, which is the whole answer.
|
|
514
539
|
...(empty ? { empty, unavailableSymbols: [] } : {}),
|
|
@@ -183,11 +183,11 @@ export const jobsHeadless: HeadlessPaneDefinition<"bundle"> = {
|
|
|
183
183
|
const symbol = args.symbols[0];
|
|
184
184
|
if (symbol) {
|
|
185
185
|
const state = await fetchJobs(symbol, { force: true, client: ctx.apiClient as JobsCloudClient });
|
|
186
|
-
if (state.kind === "denied") throw explainRefusal(new ApiRequestError(state.message, state.status), jobsLockNote, { symbol });
|
|
186
|
+
if (state.kind === "denied") throw await explainRefusal(new ApiRequestError(state.message, state.status), jobsLockNote, { symbol, context: ctx });
|
|
187
187
|
return companyBundle(symbol, state, limit);
|
|
188
188
|
}
|
|
189
189
|
const payload = await fetchJobsMovers(ctx.apiClient as JobsCloudClient)
|
|
190
|
-
.catch((error: unknown) => { throw explainRefusal(error, jobsLockNote, { symbol: null }); });
|
|
190
|
+
.catch(async (error: unknown) => { throw await explainRefusal(error, jobsLockNote, { symbol: null }); });
|
|
191
191
|
const rows = buildMoverRows(payload.movers).slice(0, Math.max(limit, 15)).map((row) => ({
|
|
192
192
|
ticker: row.ticker,
|
|
193
193
|
company: row.company,
|
|
@@ -1,16 +1,27 @@
|
|
|
1
1
|
import type { PaneLock } from "../../../types/plugin";
|
|
2
|
-
import {
|
|
2
|
+
import type { HiringPayload } from "../../../api-client/hiring";
|
|
3
|
+
import { fetchAttention } from "../company-attention/client";
|
|
4
|
+
import { proLockNote, type FreeRoute } from "../shared/pro-lock-note";
|
|
5
|
+
|
|
6
|
+
/** HIRE's preview, for a company whose careers system it reads: Coffee Holding (JVA) is mapped but uncovered. */
|
|
7
|
+
const HIRE_ROUTE: FreeRoute = {
|
|
8
|
+
text: (ticker, company) => `gloomberb fn HIRE ${ticker} previews ${company}'s hiring with its latest roles, without the full history.`,
|
|
9
|
+
answers: async (symbol, context) => {
|
|
10
|
+
const data = await fetchAttention("hiring", symbol, { limit: 1 }, context.apiClient, context.signal) as HiringPayload;
|
|
11
|
+
return data.evidence.length + data.series.length + data.functions.length > 0;
|
|
12
|
+
},
|
|
13
|
+
};
|
|
3
14
|
|
|
4
15
|
/**
|
|
5
16
|
* JOBS is Pro whole. HIRE reads the same careers systems and needs no
|
|
6
17
|
* account: a company's latest roles, without the full history.
|
|
7
18
|
*/
|
|
8
|
-
export function jobsLockNote(lock: PaneLock): string {
|
|
19
|
+
export function jobsLockNote(lock: PaneLock): Promise<string> {
|
|
9
20
|
return proLockNote(lock, {
|
|
10
21
|
token: "JOBS",
|
|
11
22
|
argument: "optional",
|
|
12
23
|
does: "it reads each company's careers system into open roles, hiring pace and the mix by function and country",
|
|
13
24
|
shows: "it",
|
|
14
|
-
free:
|
|
25
|
+
free: [HIRE_ROUTE],
|
|
15
26
|
});
|
|
16
27
|
}
|
|
@@ -180,14 +180,33 @@ const DAY_MS = 86_400_000;
|
|
|
180
180
|
/** Unresolved halts move out of the recent views at 30 elapsed days. */
|
|
181
181
|
const LONG_TERM_MS = 30 * DAY_MS;
|
|
182
182
|
|
|
183
|
-
|
|
183
|
+
/** How long a halt has kept trading stopped: to the trade resumption, or to now while it lasts. Quotes resuming do not end it. */
|
|
184
|
+
function haltDurationMs(record: HaltRecord, now: number): number {
|
|
184
185
|
const end = record.tradeResumeAt == null ? now : Math.min(now, record.tradeResumeAt);
|
|
185
|
-
|
|
186
|
+
return Math.max(0, end - record.haltedAt);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** How long since the halt began, whether or not trading has resumed. */
|
|
190
|
+
function haltAgeMs(record: HaltRecord, now: number): number {
|
|
191
|
+
return Math.max(0, now - record.haltedAt);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function formatElapsed(elapsed: number): string {
|
|
186
195
|
if (elapsed >= DAY_MS) return `${Math.floor(elapsed / DAY_MS)}d`;
|
|
187
196
|
if (elapsed >= 3_600_000) return `${Math.floor(elapsed / 3_600_000)}h`;
|
|
188
197
|
return `${Math.floor(elapsed / 60_000)}m`;
|
|
189
198
|
}
|
|
190
199
|
|
|
200
|
+
/** HALTED FOR: RAIL halted 16:40 and resumed 17:15 was halted for 35m, read at any later time. */
|
|
201
|
+
export function formatHaltDuration(record: HaltRecord, now: number): string {
|
|
202
|
+
return formatElapsed(haltDurationMs(record, now));
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** AGE: RAIL's 16:40 halt is 1h old at 17:45, though it lasted 35m. */
|
|
206
|
+
export function formatHaltAge(record: HaltRecord, now: number): string {
|
|
207
|
+
return formatElapsed(haltAgeMs(record, now));
|
|
208
|
+
}
|
|
209
|
+
|
|
191
210
|
export const HALT_FILTERS: ReadonlyArray<{ label: string; value: HaltFilter }> = [
|
|
192
211
|
{ label: "All", value: "all" },
|
|
193
212
|
{ label: "Active", value: "active" },
|
|
@@ -221,6 +240,7 @@ export type HaltColumnId =
|
|
|
221
240
|
| "halted"
|
|
222
241
|
| "quote"
|
|
223
242
|
| "trade"
|
|
243
|
+
| "duration"
|
|
224
244
|
| "status";
|
|
225
245
|
|
|
226
246
|
export type HaltColumn = DataTableColumn & { id: HaltColumnId };
|
|
@@ -235,6 +255,7 @@ export const HALT_SORT_COLUMN_IDS: readonly HaltColumnId[] = [
|
|
|
235
255
|
"halted",
|
|
236
256
|
"quote",
|
|
237
257
|
"trade",
|
|
258
|
+
"duration",
|
|
238
259
|
"status",
|
|
239
260
|
];
|
|
240
261
|
|
|
@@ -262,16 +283,18 @@ const HALT_COLUMNS: readonly HaltColumn[] = [
|
|
|
262
283
|
{ id: "halted", label: "HALT ET", width: 9, align: "left" },
|
|
263
284
|
{ id: "quote", label: "QUOTE ET", width: 11, align: "left" },
|
|
264
285
|
{ id: "trade", label: "TRADE ET", width: 11, align: "left" },
|
|
286
|
+
{ id: "duration", label: "HALTED FOR", width: 12, align: "right" },
|
|
265
287
|
{ id: "status", label: "STATUS", width: 8, align: "left" },
|
|
266
288
|
];
|
|
267
289
|
|
|
268
290
|
/**
|
|
269
291
|
* What a narrow pane gives up first, so STATUS never scrolls out of view. MKT
|
|
270
292
|
* is only the venue, STATUS already reads QUOTE once quoting resumes, and
|
|
271
|
-
* REASON spells out CODE. Trade resumption
|
|
272
|
-
* the
|
|
293
|
+
* REASON spells out CODE. Trade resumption, halt time and then the age, which
|
|
294
|
+
* the date and halt time also give, go only in the narrowest panes; symbol,
|
|
295
|
+
* reason, halt date, how long it was halted for and status always stay.
|
|
273
296
|
*/
|
|
274
|
-
const NARROW_DROP_ORDER: readonly HaltColumnId[] = ["market", "quote", "code", "company", "trade", "halted"];
|
|
297
|
+
const NARROW_DROP_ORDER: readonly HaltColumnId[] = ["market", "quote", "code", "company", "trade", "halted", "age"];
|
|
275
298
|
|
|
276
299
|
export function buildHaltColumns(width: number): HaltColumn[] {
|
|
277
300
|
const columns = fitChartTableColumns(HALT_COLUMNS, width, NARROW_DROP_ORDER);
|
|
@@ -304,7 +327,9 @@ function haltSortValue(columnId: HaltColumnId, record: HaltRecord, now: number):
|
|
|
304
327
|
case "halted":
|
|
305
328
|
return record.haltedAt;
|
|
306
329
|
case "age":
|
|
307
|
-
return
|
|
330
|
+
return haltAgeMs(record, now);
|
|
331
|
+
case "duration":
|
|
332
|
+
return haltDurationMs(record, now);
|
|
308
333
|
case "quote":
|
|
309
334
|
return record.quoteResumeAt;
|
|
310
335
|
case "trade":
|
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
formatEtResumption,
|
|
30
30
|
formatEtTime,
|
|
31
31
|
formatHaltAge,
|
|
32
|
+
formatHaltDuration,
|
|
32
33
|
haltStatusColor,
|
|
33
34
|
haltStatusLabel,
|
|
34
35
|
nextHaltFilter,
|
|
@@ -179,6 +180,8 @@ export function MarketHaltsPane({ focused, width, height }: PaneProps) {
|
|
|
179
180
|
text: formatEtResumption(row.tradeResumeAt, row.haltedAt),
|
|
180
181
|
color: selectedColor ?? colors.textDim,
|
|
181
182
|
};
|
|
183
|
+
case "duration":
|
|
184
|
+
return { text: formatHaltDuration(row, now), color: selectedColor ?? colors.textMuted };
|
|
182
185
|
case "status": {
|
|
183
186
|
const rowStatus = resolveHaltStatus(row, now);
|
|
184
187
|
return {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useMemo } from "react";
|
|
2
2
|
import {
|
|
3
|
+
DELAYED_SEGMENT,
|
|
3
4
|
PaneStatusBody, type DataTableCell,
|
|
4
5
|
type DataTableColumn,
|
|
5
6
|
type PaneFooterSegment
|
|
@@ -140,7 +141,7 @@ export function MarketValuationPane({ focused, width, height }: PaneProps) {
|
|
|
140
141
|
if (!selectedView) return [];
|
|
141
142
|
const info: PaneFooterSegment[] = [
|
|
142
143
|
{ id: "as-of", parts: [{ text: `as of ${selectedView.asOf}`, tone: "muted" }] },
|
|
143
|
-
...(width >= 80 ? [
|
|
144
|
+
...(width >= 80 ? [DELAYED_SEGMENT] : []),
|
|
144
145
|
];
|
|
145
146
|
if (selectedView.observationStale) {
|
|
146
147
|
info.push({ id: "stale", parts: [{ text: "STALE", tone: "warning", bold: true }] });
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useCallback, useMemo, useRef, useState } from "react";
|
|
2
2
|
import type { FundChange, FundMember } from "../../../api-client/members";
|
|
3
|
-
import { DataTableStackView, DataTableView, DetailScrollBody, EmptyState, KeyValueRow, PaneFooterScope, PaneStatusBody, Prose, QueryBar, StatGrid,
|
|
3
|
+
import { delayedFooterSegment, DataTableStackView, DataTableView, DetailScrollBody, EmptyState, KeyValueRow, PaneFooterScope, PaneStatusBody, Prose, QueryBar, StatGrid,
|
|
4
4
|
isSectionedItemRow, renderSectionedRowHeader, useExternalLinkFooter, usePaneNoticeFooter, usePaneStatusFooter, usePaneTabs, useQueryBarSearch,
|
|
5
5
|
type DataTableCell, type DataTableColumn, type PaneFooterSegment, type SectionedRow } from "../../../components";
|
|
6
6
|
import { usePaneRefreshKey } from "../../../components/data-table/table-pane";
|
|
@@ -49,8 +49,13 @@ function Holdings({ fund, tab, width, height, focused, active }: ViewProps & { t
|
|
|
49
49
|
return [
|
|
50
50
|
{ id: "coverage", parts: [{ text: coverageText(data.aggregate, width < 60), tone: partial ? "warning" : "muted" }] },
|
|
51
51
|
// A capture's status line says the quotes' date and delay too; it does not say returns are unavailable.
|
|
52
|
-
|
|
53
|
-
|
|
52
|
+
...(data.quotesUnavailable
|
|
53
|
+
? [{ id: "quote-date", title: `${date ?? "Quote date unavailable"} · 15m delayed · holdings ${data.asOf}`, parts: [{ text: "returns unavailable", tone: "muted" as const }] }]
|
|
54
|
+
: [
|
|
55
|
+
delayedFooterSegment({ id: "quote-delay", detail: "15m delayed", delay: "15m", quoteStatus: true }),
|
|
56
|
+
...(date ? [{ id: "quote-date", title: `${date} · 15m delayed · holdings ${data.asOf}`, quoteStatus: true,
|
|
57
|
+
parts: [{ text: width < 60 && dates.length === 1 ? date.slice(5) : date, tone: "muted" as const }] }] : []),
|
|
58
|
+
]),
|
|
54
59
|
...(width >= 60 ? [{ id: "holdings-date", asOf: true, parts: [{ text: `holdings ${data.asOf}`, tone: "muted" as const }] }] : []),
|
|
55
60
|
];
|
|
56
61
|
}, [data, width]);
|