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.
Files changed (307) hide show
  1. package/package.json +1 -1
  2. package/src/api-client/commodity-balances.ts +6 -0
  3. package/src/api-client/company-kpis.ts +9 -0
  4. package/src/api-client/crypto-markets.ts +9 -0
  5. package/src/api-client/data.ts +6 -5
  6. package/src/api-client/equity-screener.ts +2 -0
  7. package/src/api-client/types.ts +5 -0
  8. package/src/app/pane-runtime/plugin-bindings.ts +3 -1
  9. package/src/app/pane-runtime/ticker-open-runtime.ts +17 -5
  10. package/src/app/runtime/plugin-bindings.ts +6 -2
  11. package/src/brokers/logos.ts +66 -0
  12. package/src/brokers/profile-form.ts +3 -0
  13. package/src/brokers/remote-broker-adapter.ts +13 -3
  14. package/src/capabilities/index.ts +1 -1
  15. package/src/capabilities/service-capabilities.ts +7 -1
  16. package/src/cli/base-converter.ts +60 -18
  17. package/src/cli/commands/market.ts +281 -154
  18. package/src/cli/commands/news-search.ts +35 -8
  19. package/src/cli/commands/system.ts +35 -1
  20. package/src/cli/commands/ticker.ts +100 -50
  21. package/src/cli/currency-hints.ts +27 -0
  22. package/src/cli/fx-pair.ts +41 -4
  23. package/src/cli/glossary.ts +57 -4
  24. package/src/cli/helpers.ts +36 -3
  25. package/src/cli/index.ts +4 -1
  26. package/src/cli/known-gaps.ts +37 -1
  27. package/src/cli/listing-arg.ts +37 -2
  28. package/src/cli/pane-functions/catalog.ts +3 -1
  29. package/src/cli/pane-functions/discovery.ts +15 -1
  30. package/src/cli/pane-functions/dom.ts +9 -4
  31. package/src/cli/pane-functions/freshness.ts +4 -4
  32. package/src/cli/pane-functions/function-help.ts +27 -3
  33. package/src/cli/pane-functions/headless.ts +6 -5
  34. package/src/cli/pane-functions/index.ts +3 -2
  35. package/src/cli/pane-functions/lock-note.ts +7 -4
  36. package/src/cli/pane-functions/resolver.ts +2 -1
  37. package/src/cli/quote-answer.ts +179 -0
  38. package/src/cli/series-search.ts +2 -1
  39. package/src/cli/types.ts +4 -0
  40. package/src/components/chart/composite/composite-chart.tsx +11 -2
  41. package/src/components/chart/composite/format.ts +13 -1
  42. package/src/components/chart/composite/level-labels.ts +30 -0
  43. package/src/components/chart/composite/levels.ts +17 -6
  44. package/src/components/chart/composite/panel-surface.tsx +58 -8
  45. package/src/components/command-bar/routes/ticker-search/actions.ts +6 -0
  46. package/src/components/index.ts +2 -0
  47. package/src/components/layout/pane/content.tsx +16 -14
  48. package/src/components/layout/pane/footer/index.tsx +22 -3
  49. package/src/components/layout/pane/footer/model.ts +99 -4
  50. package/src/components/layout/pane/header-tabs.tsx +15 -1
  51. package/src/components/onboarding/onboarding-frame.tsx +9 -11
  52. package/src/components/onboarding/use-onboarding-broker-form.ts +5 -2
  53. package/src/components/onboarding/wizard-model.ts +10 -3
  54. package/src/components/ui/button.tsx +11 -4
  55. package/src/components/ui/choice-dialog.tsx +8 -5
  56. package/src/components/ui/display.tsx +11 -6
  57. package/src/components/ui/fields.tsx +100 -20
  58. package/src/components/ui/icon.tsx +6 -1
  59. package/src/components/ui/list-row-lines.tsx +87 -0
  60. package/src/components/ui/list-view.tsx +14 -0
  61. package/src/components/ui/logo-mark.tsx +64 -0
  62. package/src/components/ui/number-step.ts +54 -0
  63. package/src/components/ui/select-button.tsx +5 -4
  64. package/src/components/ui/select-field.tsx +3 -3
  65. package/src/components/ui/status.tsx +17 -3
  66. package/src/components/ui/tabs.tsx +9 -1
  67. package/src/components/ui/toggle.tsx +17 -3
  68. package/src/core/state/app/instrument.ts +8 -3
  69. package/src/data/config/store/normalize.ts +4 -1
  70. package/src/data/plugin-cache.ts +32 -6
  71. package/src/docs/snapshot.json +5 -5
  72. package/src/i18n/es.ts +9 -3
  73. package/src/i18n/ja.ts +9 -3
  74. package/src/i18n/ko.ts +9 -3
  75. package/src/i18n/zh-cn.ts +10 -4
  76. package/src/i18n/zh-tw.ts +10 -4
  77. package/src/market-data/commodity-names.ts +80 -0
  78. package/src/market-data/coordinator/auxiliary.ts +3 -1
  79. package/src/market-data/coordinator/chart.ts +3 -1
  80. package/src/market-data/futures-board.ts +38 -1
  81. package/src/market-data/futures-contract.ts +91 -0
  82. package/src/market-data/fx-closes.ts +78 -1
  83. package/src/market-data/market/change-basis.ts +219 -0
  84. package/src/market-data/market/format.ts +28 -12
  85. package/src/market-data/market/freshness.ts +38 -0
  86. package/src/market-data/market/status.ts +55 -9
  87. package/src/market-data/market/year-to-date.ts +46 -0
  88. package/src/market-data/quotes/contributions.ts +12 -0
  89. package/src/market-data/quotes/delisting.ts +34 -0
  90. package/src/market-data/quotes/live-valuation.ts +23 -0
  91. package/src/market-data/quotes/resolution.ts +8 -2
  92. package/src/market-data/tax-equivalent.ts +10 -2
  93. package/src/notifications/app-notifier.ts +2 -2
  94. package/src/notifications/notification-log.ts +8 -6
  95. package/src/plugins/builtin/alerts/cli.ts +26 -18
  96. package/src/plugins/builtin/alerts/headless.ts +1 -1
  97. package/src/plugins/builtin/alerts/index.tsx +13 -4
  98. package/src/plugins/builtin/attention/trending.tsx +8 -6
  99. package/src/plugins/builtin/backtest/client.ts +5 -2
  100. package/src/plugins/builtin/backtest/window.ts +16 -3
  101. package/src/plugins/builtin/broker-manager/add-flow.tsx +40 -8
  102. package/src/plugins/builtin/broker-manager/detail.tsx +26 -1
  103. package/src/plugins/builtin/broker-manager/index.tsx +20 -9
  104. package/src/plugins/builtin/broker-manager/model.ts +5 -0
  105. package/src/plugins/builtin/broker-manager/pane-actions.tsx +8 -1
  106. package/src/plugins/builtin/broker-manager/table.ts +5 -1
  107. package/src/plugins/builtin/broker-trading/account-picker.tsx +91 -0
  108. package/src/plugins/builtin/broker-trading/chart-levels.ts +54 -0
  109. package/src/plugins/builtin/broker-trading/command.ts +103 -0
  110. package/src/plugins/builtin/broker-trading/controller.ts +688 -0
  111. package/src/plugins/builtin/broker-trading/dock.tsx +182 -0
  112. package/src/plugins/builtin/broker-trading/headless.ts +76 -0
  113. package/src/plugins/builtin/broker-trading/index.tsx +77 -0
  114. package/src/plugins/builtin/broker-trading/order-feed.ts +383 -0
  115. package/src/plugins/builtin/broker-trading/orders-data.ts +133 -0
  116. package/src/plugins/builtin/broker-trading/orders.tsx +437 -0
  117. package/src/plugins/builtin/broker-trading/overview-orders.tsx +121 -0
  118. package/src/plugins/builtin/broker-trading/pane-model.ts +207 -0
  119. package/src/plugins/builtin/broker-trading/pane.tsx +505 -0
  120. package/src/plugins/builtin/broker-trading/strategy.ts +153 -0
  121. package/src/plugins/builtin/broker-trading/ticket-parts.tsx +117 -0
  122. package/src/plugins/builtin/broker-trading/ticket.tsx +573 -0
  123. package/src/plugins/builtin/buildout/data-runtime.ts +1 -1
  124. package/src/plugins/builtin/buildout/format.ts +1 -0
  125. package/src/plugins/builtin/buildout/model/index.ts +16 -7
  126. package/src/plugins/builtin/buildout/pane/index.tsx +3 -2
  127. package/src/plugins/builtin/buildout/pane/status.tsx +3 -3
  128. package/src/plugins/builtin/chart-composer/cli-options.ts +19 -0
  129. package/src/plugins/builtin/chart-composer/data-catalog-pane.tsx +2 -1
  130. package/src/plugins/builtin/chart-composer/headless-schema.ts +3 -3
  131. package/src/plugins/builtin/chart-composer/headless.ts +2 -1
  132. package/src/plugins/builtin/chart-composer/option-range.ts +39 -0
  133. package/src/plugins/builtin/chart-composer/pane.tsx +31 -11
  134. package/src/plugins/builtin/chart-composer/presets.ts +2 -2
  135. package/src/plugins/builtin/chart-composer/series-catalog.ts +4 -2
  136. package/src/plugins/builtin/chart-composer/shortcuts.ts +3 -3
  137. package/src/plugins/builtin/chart-composer/viewport-labels.ts +5 -2
  138. package/src/plugins/builtin/chat/index.tsx +1 -1
  139. package/src/plugins/builtin/chat/status-widget.tsx +1 -1
  140. package/src/plugins/builtin/chat/unread-inbox-pane.tsx +6 -1
  141. package/src/plugins/builtin/chat/unread-inbox.ts +1 -1
  142. package/src/plugins/builtin/commodity-balances/client.ts +6 -4
  143. package/src/plugins/builtin/commodity-balances/headless.ts +41 -7
  144. package/src/plugins/builtin/commodity-balances/index.tsx +19 -4
  145. package/src/plugins/builtin/commodity-balances/model.ts +22 -0
  146. package/src/plugins/builtin/commodity-balances/pane.tsx +36 -5
  147. package/src/plugins/builtin/company-kpis/client.ts +3 -2
  148. package/src/plugins/builtin/company-kpis/headless.ts +5 -7
  149. package/src/plugins/builtin/company-kpis/model.ts +85 -2
  150. package/src/plugins/builtin/company-kpis/pane.tsx +26 -12
  151. package/src/plugins/builtin/composite-plugins.ts +3 -2
  152. package/src/plugins/builtin/cot/headless.ts +1 -1
  153. package/src/plugins/builtin/cot/index.tsx +1 -1
  154. package/src/plugins/builtin/cot/model.ts +35 -12
  155. package/src/plugins/builtin/credit-boards/cdx-pane.tsx +2 -1
  156. package/src/plugins/builtin/credit-boards/sovr-pane.tsx +2 -1
  157. package/src/plugins/builtin/credit-conditions/index.tsx +2 -1
  158. package/src/plugins/builtin/crypto-board/client.ts +33 -2
  159. package/src/plugins/builtin/crypto-board/headless.ts +5 -2
  160. package/src/plugins/builtin/crypto-board/model.ts +28 -8
  161. package/src/plugins/builtin/dividend-yield/headless.ts +30 -8
  162. package/src/plugins/builtin/dividend-yield/provider-client.ts +3 -1
  163. package/src/plugins/builtin/earnings-calls/headless.ts +4 -4
  164. package/src/plugins/builtin/earnings-calls/lock-note.ts +12 -3
  165. package/src/plugins/builtin/econ/calendar-model.ts +35 -0
  166. package/src/plugins/builtin/econ/index.tsx +31 -4
  167. package/src/plugins/builtin/econ/print-watch.ts +279 -0
  168. package/src/plugins/builtin/equity-screener/headless.ts +20 -5
  169. package/src/plugins/builtin/equity-screener/model.ts +40 -2
  170. package/src/plugins/builtin/equity-screener/pane.tsx +24 -21
  171. package/src/plugins/builtin/estimate-revisions/headless.ts +3 -2
  172. package/src/plugins/builtin/estimate-revisions/lock-note.ts +29 -7
  173. package/src/plugins/builtin/exposure/client.ts +4 -1
  174. package/src/plugins/builtin/exposure/editor.tsx +3 -3
  175. package/src/plugins/builtin/exposure/evidence.ts +4 -1
  176. package/src/plugins/builtin/exposure/holdings.ts +3 -1
  177. package/src/plugins/builtin/exposure/pane.tsx +37 -16
  178. package/src/plugins/builtin/filing-events/index.tsx +9 -3
  179. package/src/plugins/builtin/futures/table.tsx +6 -3
  180. package/src/plugins/builtin/futures-curve/pane.tsx +2 -2
  181. package/src/plugins/builtin/insider/base-values.ts +72 -0
  182. package/src/plugins/builtin/insider/headless.ts +29 -4
  183. package/src/plugins/builtin/jobs/headless.ts +2 -2
  184. package/src/plugins/builtin/jobs/lock-note.ts +14 -3
  185. package/src/plugins/builtin/market-halts/model.ts +31 -6
  186. package/src/plugins/builtin/market-halts/pane.tsx +3 -0
  187. package/src/plugins/builtin/market-valuation/pane.tsx +2 -1
  188. package/src/plugins/builtin/members/pane.tsx +8 -3
  189. package/src/plugins/builtin/news/wire/feed-config.ts +29 -12
  190. package/src/plugins/builtin/news/wire/index.tsx +7 -2
  191. package/src/plugins/builtin/news/wire/news/preset-pane.tsx +11 -3
  192. package/src/plugins/builtin/news/wire/rss/cli.ts +3 -2
  193. package/src/plugins/builtin/news/wire/rss/source.ts +13 -4
  194. package/src/plugins/builtin/options/footer.ts +25 -20
  195. package/src/plugins/builtin/options/live-quotes.ts +33 -18
  196. package/src/plugins/builtin/options/view.tsx +77 -22
  197. package/src/plugins/builtin/options-calculator/pane.tsx +4 -2
  198. package/src/plugins/builtin/options-scenario/pane.tsx +41 -6
  199. package/src/plugins/builtin/portfolio-list/account-metrics.ts +3 -0
  200. package/src/plugins/builtin/portfolio-list/cli/render.ts +34 -10
  201. package/src/plugins/builtin/portfolio-list/headless.ts +1 -1
  202. package/src/plugins/builtin/portfolio-list/pane/data.ts +23 -1
  203. package/src/plugins/builtin/portfolio-list/pane/index.tsx +33 -14
  204. package/src/plugins/builtin/portfolio-list/settings.ts +20 -2
  205. package/src/plugins/builtin/portfolio-list/summary/index.tsx +12 -0
  206. package/src/plugins/builtin/portfolio-list/summary/totals.ts +24 -3
  207. package/src/plugins/builtin/primary-dealers/client.ts +137 -62
  208. package/src/plugins/builtin/primary-dealers/headless.ts +30 -5
  209. package/src/plugins/builtin/primary-dealers/index.tsx +3 -2
  210. package/src/plugins/builtin/primary-dealers/model.ts +179 -129
  211. package/src/plugins/builtin/primary-dealers/pane.tsx +135 -15
  212. package/src/plugins/builtin/research/analyst-model.ts +5 -1
  213. package/src/plugins/builtin/risk-factors/index.tsx +12 -3
  214. package/src/plugins/builtin/scanner/feed.ts +6 -1
  215. package/src/plugins/builtin/scanner/headless.ts +1 -1
  216. package/src/plugins/builtin/scanner/lock-note.ts +3 -2
  217. package/src/plugins/builtin/series-inventory.ts +1 -0
  218. package/src/plugins/builtin/shared/cloud-denied.ts +7 -6
  219. package/src/plugins/builtin/shared/cloud-upgrade.ts +10 -11
  220. package/src/plugins/builtin/shared/free-routes.ts +24 -0
  221. package/src/plugins/builtin/shared/headless-options.ts +11 -0
  222. package/src/plugins/builtin/shared/pro-lock-note.ts +89 -7
  223. package/src/plugins/builtin/shared/research-trading.tsx +62 -0
  224. package/src/plugins/builtin/social-mentions/headless.ts +1 -1
  225. package/src/plugins/builtin/social-mentions/lock-note.ts +1 -1
  226. package/src/plugins/builtin/themes/headless.ts +8 -2
  227. package/src/plugins/builtin/themes/model.ts +43 -10
  228. package/src/plugins/builtin/themes/pane.tsx +8 -6
  229. package/src/plugins/builtin/ticker-detail/overview/model.ts +44 -10
  230. package/src/plugins/builtin/ticker-detail/overview/option-contract.ts +64 -0
  231. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +82 -21
  232. package/src/plugins/builtin/ticker-detail/pane.tsx +21 -14
  233. package/src/plugins/builtin/ticker-detail/quote-footer.ts +5 -1
  234. package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +37 -8
  235. package/src/plugins/builtin/time-sales/pane.tsx +2 -2
  236. package/src/plugins/builtin/trace-bonds/client.ts +14 -3
  237. package/src/plugins/builtin/trace-bonds/headless.ts +1 -1
  238. package/src/plugins/builtin/vol-surface/pane.tsx +2 -2
  239. package/src/plugins/builtin-editorial.ts +1 -1
  240. package/src/plugins/compat.ts +3 -0
  241. package/src/plugins/external-runtime.ts +5 -0
  242. package/src/plugins/loader.ts +6 -0
  243. package/src/plugins/plugin-icon.ts +61 -0
  244. package/src/plugins/registry/host-actions.ts +2 -1
  245. package/src/plugins/registry/index.ts +9 -1
  246. package/src/plugins/runtime/context.tsx +5 -0
  247. package/src/plugins/runtime/index.tsx +3 -0
  248. package/src/portfolio/column-values.ts +13 -4
  249. package/src/portfolio/cost-fx.ts +5 -28
  250. package/src/public/__snapshots__/public-api.test.ts.snap +5 -0
  251. package/src/public/broker.ts +8 -0
  252. package/src/public/market-data.ts +5 -0
  253. package/src/renderers/dom/desktop/control-styles.ts +7 -1
  254. package/src/renderers/dom/desktop/controls.tsx +39 -7
  255. package/src/renderers/dom/desktop/icons.tsx +14 -0
  256. package/src/renderers/dom/desktop/list-view.tsx +51 -19
  257. package/src/renderers/dom/desktop/menu.tsx +10 -1
  258. package/src/renderers/dom/desktop/select-field.tsx +8 -6
  259. package/src/renderers/dom/host/style.ts +6 -1
  260. package/src/renderers/dom/host/tabs.tsx +6 -0
  261. package/src/renderers/dom/styles.css +12 -5
  262. package/src/renderers/electrobun/bun/core-capabilities.ts +12 -0
  263. package/src/renderers/electrobun/bun/external-plugins.ts +3 -0
  264. package/src/renderers/electrobun/shared/protocol.ts +2 -0
  265. package/src/renderers/electrobun/view/broker-remote-client.ts +16 -0
  266. package/src/renderers/electrobun/view/external-plugins.ts +1 -0
  267. package/src/series/validate.ts +2 -2
  268. package/src/sources/gloomberb-cloud/index.ts +50 -17
  269. package/src/sources/gloomberb-cloud/normalizers.ts +4 -0
  270. package/src/sources/history-result.ts +3 -0
  271. package/src/sources/provider-errors.ts +3 -1
  272. package/src/sources/provider-router/history.ts +49 -6
  273. package/src/sources/provider-router/index.ts +2 -1
  274. package/src/state/app/activity.ts +13 -0
  275. package/src/sync/controller.ts +4 -0
  276. package/src/sync/core-contributors.ts +62 -11
  277. package/src/sync/local-only.ts +121 -0
  278. package/src/sync/react.ts +6 -0
  279. package/src/sync/types.ts +3 -0
  280. package/src/tickers/listing-fate.ts +27 -8
  281. package/src/tickers/search/index.ts +24 -3
  282. package/src/time-series/price-comparison.ts +35 -0
  283. package/src/time-series/resolve.ts +52 -11
  284. package/src/time-series/spec.ts +1 -1
  285. package/src/time-series/types.ts +5 -0
  286. package/src/types/broker.ts +56 -1
  287. package/src/types/config.ts +6 -0
  288. package/src/types/data-provider.ts +5 -1
  289. package/src/types/financials.ts +23 -0
  290. package/src/types/headless.ts +12 -2
  291. package/src/types/instrument.ts +4 -0
  292. package/src/types/plugin.ts +24 -6
  293. package/src/types/price-history.ts +6 -1
  294. package/src/types/trading.ts +67 -0
  295. package/src/ui/host.tsx +4 -0
  296. package/src/utils/base-currency.ts +108 -0
  297. package/src/utils/currency-names.ts +128 -0
  298. package/src/utils/currency-pair.ts +2 -1
  299. package/src/utils/currency-units.ts +32 -1
  300. package/src/utils/dom-screenshot.ts +22 -9
  301. package/src/utils/exchange-notation.generated.ts +360 -0
  302. package/src/utils/exchanges.ts +15 -322
  303. package/src/utils/format.ts +31 -1
  304. package/src/utils/instrument-identity.ts +38 -1
  305. package/src/utils/options.ts +46 -1
  306. package/src/utils/price-history.ts +4 -0
  307. 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 snapshot = useMemo(() => { try { return snapshotSetting ? validateExposure(snapshotSetting) : null; } catch { return null; } }, [snapshotSetting]);
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 initialScenario = useMemo(() => { if (snapshot) return snapshot.scenario; try { return customSetting ? parseCustomScenario(customSetting) : DEFAULT_SCENARIO; } catch { return DEFAULT_SCENARIO; } }, [snapshot, customSetting]);
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: { gate: "sign-in" | "pro"; symbol: string | null }): string {
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: (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.`,
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: US cents (`USX`) as a `c` suffix, other
168
- * currencies as their symbol.
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
- if (code === "USX") return { prefix: "", suffix: "c" };
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 rows: HeadlessPaneRow[] = buildInsiderRows(filtered, parsed).map((row, index) => {
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 { proLockNote } from "../shared/pro-lock-note";
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: (ticker, company) => `gloomberb fn HIRE ${ticker} previews ${company}'s hiring with its latest roles, without the full history.`,
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
- export function formatHaltAge(record: HaltRecord, now: number): string {
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
- const elapsed = Math.max(0, end - record.haltedAt);
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 and halt time give way only in
272
- * the narrowest panes; symbol, reason, halt date, age and status always stay.
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 Math.max(0, Math.min(now, record.tradeResumeAt ?? now) - record.haltedAt);
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 ? [{ id: "delayed", parts: [{ text: "delayed", tone: "muted" as const }] }] : []),
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
- { id: "quote-date", title: `${date ?? "Quote date unavailable"} · 15m delayed · holdings ${data.asOf}`, quoteStatus: !data.quotesUnavailable,
53
- parts: [{ text: data.quotesUnavailable ? "returns unavailable" : width < 60 && dates.length === 1 ? `${date!.slice(5)} delayed` : `${date ? `${date} · ` : ""}15m delayed`, tone: "muted" }] },
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]);