gloomberb 0.18.1 → 0.18.3

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 (416) hide show
  1. package/package.json +1 -1
  2. package/patches/electrobun@1.18.1.patch +106 -5
  3. package/src/api-client/commodity-balances.ts +6 -0
  4. package/src/api-client/company-kpis.ts +9 -0
  5. package/src/api-client/crypto-markets.ts +9 -0
  6. package/src/api-client/data.ts +21 -5
  7. package/src/api-client/equity-screener.ts +2 -0
  8. package/src/api-client/index.ts +2 -0
  9. package/src/api-client/request.ts +5 -3
  10. package/src/api-client/telemetry.ts +17 -0
  11. package/src/api-client/types.ts +5 -0
  12. package/src/app/global-shortcuts.ts +1 -16
  13. package/src/app/pane-runtime/plugin-bindings.ts +3 -1
  14. package/src/app/pane-runtime/ticker-open-runtime.ts +17 -5
  15. package/src/app/runtime/plugin-bindings.ts +6 -2
  16. package/src/app/runtime/update.ts +28 -8
  17. package/src/architecture/pane-tabs-allowlist.json +65 -0
  18. package/src/brokers/logos.ts +66 -0
  19. package/src/brokers/profile-form.ts +3 -0
  20. package/src/brokers/remote-broker-adapter.ts +13 -3
  21. package/src/capabilities/index.ts +1 -1
  22. package/src/capabilities/service-capabilities.ts +7 -1
  23. package/src/cli/base-converter.ts +60 -18
  24. package/src/cli/commands/market.ts +354 -187
  25. package/src/cli/commands/news-search.ts +35 -8
  26. package/src/cli/commands/system.ts +35 -1
  27. package/src/cli/commands/ticker.ts +167 -60
  28. package/src/cli/currency-hints.ts +27 -0
  29. package/src/cli/fx-pair.ts +41 -4
  30. package/src/cli/glossary.ts +57 -4
  31. package/src/cli/help.ts +1 -1
  32. package/src/cli/helpers.ts +36 -3
  33. package/src/cli/index.ts +12 -1
  34. package/src/cli/issuer-membership.ts +103 -0
  35. package/src/cli/known-gaps.ts +37 -1
  36. package/src/cli/listing-arg.ts +51 -3
  37. package/src/cli/name-entries.ts +51 -12
  38. package/src/cli/pane-functions/capabilities.ts +2 -0
  39. package/src/cli/pane-functions/catalog.ts +19 -1
  40. package/src/cli/pane-functions/discovery.ts +15 -1
  41. package/src/cli/pane-functions/dom.ts +11 -5
  42. package/src/cli/pane-functions/freshness.ts +4 -4
  43. package/src/cli/pane-functions/function-help.ts +27 -3
  44. package/src/cli/pane-functions/headless.ts +31 -11
  45. package/src/cli/pane-functions/index.ts +67 -7
  46. package/src/cli/pane-functions/lock-note.ts +7 -4
  47. package/src/cli/pane-functions/resolver.ts +21 -6
  48. package/src/cli/quote-answer.ts +179 -0
  49. package/src/cli/report-tables.ts +136 -18
  50. package/src/cli/result.ts +12 -4
  51. package/src/cli/series-search.ts +2 -1
  52. package/src/cli/symbol-list.ts +47 -11
  53. package/src/cli/types.ts +4 -0
  54. package/src/components/chart/composite/composite-chart.tsx +25 -7
  55. package/src/components/chart/composite/drawing-store.ts +26 -6
  56. package/src/components/chart/composite/format.ts +13 -1
  57. package/src/components/chart/composite/interactions.ts +2 -1
  58. package/src/components/chart/composite/level-labels.ts +30 -0
  59. package/src/components/chart/composite/levels.ts +17 -6
  60. package/src/components/chart/composite/panel-surface.tsx +69 -8
  61. package/src/components/chart/composite/types.ts +6 -0
  62. package/src/components/chart/composite/use-composite-chart-keyboard.ts +1 -1
  63. package/src/components/command-bar/routes/root/command-items.ts +32 -6
  64. package/src/components/command-bar/routes/ticker-search/actions.ts +6 -0
  65. package/src/components/data-table/view.tsx +23 -3
  66. package/src/components/index.ts +3 -0
  67. package/src/components/layout/detached-pane-shell.tsx +1 -0
  68. package/src/components/layout/floating-pane.tsx +1 -0
  69. package/src/components/layout/header.tsx +228 -55
  70. package/src/components/layout/pane/content.tsx +16 -14
  71. package/src/components/layout/pane/footer/index.tsx +22 -3
  72. package/src/components/layout/pane/footer/model.ts +99 -4
  73. package/src/components/layout/pane/header-tabs.tsx +15 -1
  74. package/src/components/layout/pane/header.tsx +10 -3
  75. package/src/components/layout/pane/index.tsx +1 -0
  76. package/src/components/layout/pane/pane-tabs.tsx +9 -1
  77. package/src/components/onboarding/onboarding-frame.tsx +9 -11
  78. package/src/components/onboarding/use-onboarding-broker-form.ts +5 -2
  79. package/src/components/onboarding/wizard-model.ts +10 -3
  80. package/src/components/ui/button.tsx +11 -4
  81. package/src/components/ui/choice-dialog.tsx +8 -5
  82. package/src/components/ui/display.tsx +11 -6
  83. package/src/components/ui/fields.tsx +100 -20
  84. package/src/components/ui/icon.tsx +6 -1
  85. package/src/components/ui/list-row-lines.tsx +87 -0
  86. package/src/components/ui/list-view.tsx +14 -0
  87. package/src/components/ui/logo-mark.tsx +64 -0
  88. package/src/components/ui/number-step.ts +54 -0
  89. package/src/components/ui/select-button.tsx +5 -4
  90. package/src/components/ui/select-field.tsx +3 -3
  91. package/src/components/ui/stat-grid.tsx +28 -3
  92. package/src/components/ui/status.tsx +20 -4
  93. package/src/components/ui/tabs.tsx +9 -1
  94. package/src/components/ui/toggle.tsx +17 -3
  95. package/src/core/app-services.ts +4 -0
  96. package/src/core/state/app/instrument.ts +8 -3
  97. package/src/data/config/store/normalize.ts +4 -1
  98. package/src/data/plugin-cache.ts +32 -6
  99. package/src/docs/snapshot.json +5 -5
  100. package/src/i18n/es.ts +31 -3
  101. package/src/i18n/ja.ts +31 -3
  102. package/src/i18n/ko.ts +31 -3
  103. package/src/i18n/zh-cn.ts +32 -4
  104. package/src/i18n/zh-tw.ts +32 -4
  105. package/src/market-data/commodity-names.ts +80 -0
  106. package/src/market-data/coordinator/auxiliary.ts +3 -1
  107. package/src/market-data/coordinator/chart.ts +3 -1
  108. package/src/market-data/futures-board.ts +38 -1
  109. package/src/market-data/futures-contract.ts +91 -0
  110. package/src/market-data/fx-closes.ts +78 -1
  111. package/src/market-data/market/change-basis.ts +219 -0
  112. package/src/market-data/market/format.ts +28 -12
  113. package/src/market-data/market/freshness.ts +38 -0
  114. package/src/market-data/market/status.ts +55 -9
  115. package/src/market-data/market/year-to-date.ts +46 -0
  116. package/src/market-data/quotes/contributions.ts +12 -0
  117. package/src/market-data/quotes/delisting.ts +34 -0
  118. package/src/market-data/quotes/live-valuation.ts +23 -0
  119. package/src/market-data/quotes/resolution.ts +8 -2
  120. package/src/market-data/tax-equivalent.ts +10 -2
  121. package/src/notifications/app-notifier.ts +2 -2
  122. package/src/notifications/notification-log.ts +8 -6
  123. package/src/plugins/builtin/alerts/alert-engine.ts +3 -1
  124. package/src/plugins/builtin/alerts/cli.ts +26 -18
  125. package/src/plugins/builtin/alerts/events.ts +1 -0
  126. package/src/plugins/builtin/alerts/headless.ts +1 -1
  127. package/src/plugins/builtin/alerts/index.tsx +13 -4
  128. package/src/plugins/builtin/alerts/research-rules.ts +1 -0
  129. package/src/plugins/builtin/analytics/risk-pane.tsx +10 -7
  130. package/src/plugins/builtin/attention/pane.tsx +16 -14
  131. package/src/plugins/builtin/attention/trending.tsx +8 -6
  132. package/src/plugins/builtin/awards/pane.tsx +6 -6
  133. package/src/plugins/builtin/backtest/client.ts +5 -2
  134. package/src/plugins/builtin/backtest/export.ts +1 -1
  135. package/src/plugins/builtin/backtest/window.ts +16 -3
  136. package/src/plugins/builtin/broker-manager/add-flow.tsx +40 -8
  137. package/src/plugins/builtin/broker-manager/detail.tsx +26 -1
  138. package/src/plugins/builtin/broker-manager/index.tsx +20 -9
  139. package/src/plugins/builtin/broker-manager/model.ts +5 -0
  140. package/src/plugins/builtin/broker-manager/pane-actions.tsx +8 -1
  141. package/src/plugins/builtin/broker-manager/table.ts +5 -1
  142. package/src/plugins/builtin/broker-trading/account-picker.tsx +91 -0
  143. package/src/plugins/builtin/broker-trading/chart-levels.ts +54 -0
  144. package/src/plugins/builtin/broker-trading/command.ts +103 -0
  145. package/src/plugins/builtin/broker-trading/controller.ts +688 -0
  146. package/src/plugins/builtin/broker-trading/dock.tsx +184 -0
  147. package/src/plugins/builtin/broker-trading/headless.ts +76 -0
  148. package/src/plugins/builtin/broker-trading/index.tsx +77 -0
  149. package/src/plugins/builtin/broker-trading/order-feed.ts +383 -0
  150. package/src/plugins/builtin/broker-trading/orders-data.ts +133 -0
  151. package/src/plugins/builtin/broker-trading/orders.tsx +437 -0
  152. package/src/plugins/builtin/broker-trading/overview-orders.tsx +121 -0
  153. package/src/plugins/builtin/broker-trading/pane-model.ts +207 -0
  154. package/src/plugins/builtin/broker-trading/pane.tsx +507 -0
  155. package/src/plugins/builtin/broker-trading/strategy.ts +153 -0
  156. package/src/plugins/builtin/broker-trading/ticket-parts.tsx +117 -0
  157. package/src/plugins/builtin/broker-trading/ticket.tsx +578 -0
  158. package/src/plugins/builtin/buildout/data-runtime.ts +1 -1
  159. package/src/plugins/builtin/buildout/format.ts +1 -0
  160. package/src/plugins/builtin/buildout/model/index.ts +16 -7
  161. package/src/plugins/builtin/buildout/pane/index.tsx +11 -7
  162. package/src/plugins/builtin/buildout/pane/status.tsx +3 -3
  163. package/src/plugins/builtin/catalysts/client.ts +2 -2
  164. package/src/plugins/builtin/catalysts/pane.tsx +3 -3
  165. package/src/plugins/builtin/chart-composer/cli-options.ts +19 -0
  166. package/src/plugins/builtin/chart-composer/data-catalog-pane.tsx +2 -1
  167. package/src/plugins/builtin/chart-composer/headless-schema.ts +3 -3
  168. package/src/plugins/builtin/chart-composer/headless.ts +2 -1
  169. package/src/plugins/builtin/chart-composer/option-range.ts +39 -0
  170. package/src/plugins/builtin/chart-composer/pane.tsx +32 -11
  171. package/src/plugins/builtin/chart-composer/presets.ts +2 -2
  172. package/src/plugins/builtin/chart-composer/series-catalog.ts +4 -2
  173. package/src/plugins/builtin/chart-composer/shortcuts.ts +3 -3
  174. package/src/plugins/builtin/chart-composer/viewport-labels.ts +5 -2
  175. package/src/plugins/builtin/chat/index.tsx +1 -1
  176. package/src/plugins/builtin/chat/status-widget.tsx +1 -1
  177. package/src/plugins/builtin/chat/unread-inbox-pane.tsx +6 -1
  178. package/src/plugins/builtin/chat/unread-inbox.ts +1 -1
  179. package/src/plugins/builtin/commodity-balances/client.ts +6 -4
  180. package/src/plugins/builtin/commodity-balances/headless.ts +41 -7
  181. package/src/plugins/builtin/commodity-balances/index.tsx +19 -4
  182. package/src/plugins/builtin/commodity-balances/model.ts +22 -0
  183. package/src/plugins/builtin/commodity-balances/pane.tsx +39 -9
  184. package/src/plugins/builtin/company-attention/app-rank.tsx +4 -5
  185. package/src/plugins/builtin/company-attention/pane.tsx +13 -11
  186. package/src/plugins/builtin/company-kpis/client.ts +3 -2
  187. package/src/plugins/builtin/company-kpis/headless.ts +5 -7
  188. package/src/plugins/builtin/company-kpis/model.ts +85 -2
  189. package/src/plugins/builtin/company-kpis/pane.tsx +30 -15
  190. package/src/plugins/builtin/composite-plugins.ts +3 -2
  191. package/src/plugins/builtin/correlation/headless.ts +2 -1
  192. package/src/plugins/builtin/correlation/relationship/model.ts +1 -0
  193. package/src/plugins/builtin/correlation/settings.ts +1 -0
  194. package/src/plugins/builtin/cot/headless.ts +1 -1
  195. package/src/plugins/builtin/cot/index.tsx +1 -1
  196. package/src/plugins/builtin/cot/model.ts +35 -12
  197. package/src/plugins/builtin/cot/pane.tsx +4 -3
  198. package/src/plugins/builtin/credit-boards/cdx-pane.tsx +2 -1
  199. package/src/plugins/builtin/credit-boards/sovr-pane.tsx +2 -1
  200. package/src/plugins/builtin/credit-conditions/index.tsx +2 -1
  201. package/src/plugins/builtin/credit-documents/pane.tsx +8 -7
  202. package/src/plugins/builtin/crypto-board/client.ts +33 -2
  203. package/src/plugins/builtin/crypto-board/headless.ts +5 -2
  204. package/src/plugins/builtin/crypto-board/model.ts +28 -8
  205. package/src/plugins/builtin/debt-maturities/pane.tsx +4 -3
  206. package/src/plugins/builtin/dividend-yield/headless.ts +30 -8
  207. package/src/plugins/builtin/dividend-yield/provider-client.ts +3 -1
  208. package/src/plugins/builtin/doe/pane.tsx +1 -1
  209. package/src/plugins/builtin/earnings-calls/headless.ts +4 -4
  210. package/src/plugins/builtin/earnings-calls/lock-note.ts +12 -3
  211. package/src/plugins/builtin/econ/calendar-model.ts +35 -0
  212. package/src/plugins/builtin/econ/index.tsx +31 -4
  213. package/src/plugins/builtin/econ/print-watch.ts +279 -0
  214. package/src/plugins/builtin/equity-screener/headless.ts +30 -5
  215. package/src/plugins/builtin/equity-screener/model.ts +68 -2
  216. package/src/plugins/builtin/equity-screener/pane.tsx +129 -119
  217. package/src/plugins/builtin/estimate-revisions/headless.ts +3 -2
  218. package/src/plugins/builtin/estimate-revisions/lock-note.ts +29 -7
  219. package/src/plugins/builtin/exposure/client.ts +4 -1
  220. package/src/plugins/builtin/exposure/editor.tsx +3 -3
  221. package/src/plugins/builtin/exposure/evidence.ts +4 -1
  222. package/src/plugins/builtin/exposure/holdings.ts +3 -1
  223. package/src/plugins/builtin/exposure/pane.tsx +43 -21
  224. package/src/plugins/builtin/filing-events/index.tsx +9 -3
  225. package/src/plugins/builtin/futures/table.tsx +6 -3
  226. package/src/plugins/builtin/futures-curve/pane.tsx +2 -2
  227. package/src/plugins/builtin/gpu/board.tsx +4 -1
  228. package/src/plugins/builtin/gpu/changes.tsx +10 -10
  229. package/src/plugins/builtin/gpu/equities.tsx +4 -1
  230. package/src/plugins/builtin/gpu/history.tsx +4 -2
  231. package/src/plugins/builtin/gpu/pane.tsx +5 -3
  232. package/src/plugins/builtin/holders/client.ts +2 -1
  233. package/src/plugins/builtin/holders/headless.ts +2 -1
  234. package/src/plugins/builtin/holders/thirteenf-match.ts +3 -1
  235. package/src/plugins/builtin/insider/base-values.ts +76 -0
  236. package/src/plugins/builtin/insider/client.ts +3 -0
  237. package/src/plugins/builtin/insider/headless.ts +48 -10
  238. package/src/plugins/builtin/insider/index.tsx +17 -8
  239. package/src/plugins/builtin/insider/issuer.ts +34 -4
  240. package/src/plugins/builtin/insider/signals-client.ts +2 -2
  241. package/src/plugins/builtin/insider/table-model.ts +35 -13
  242. package/src/plugins/builtin/insider/table.tsx +5 -2
  243. package/src/plugins/builtin/iv-history/events.ts +145 -0
  244. package/src/plugins/builtin/iv-history/format.ts +3 -0
  245. package/src/plugins/builtin/iv-history/headless.ts +91 -7
  246. package/src/plugins/builtin/jobs/headless.ts +2 -2
  247. package/src/plugins/builtin/jobs/lock-note.ts +14 -3
  248. package/src/plugins/builtin/jodi/pane.tsx +4 -4
  249. package/src/plugins/builtin/market-halts/model.ts +31 -6
  250. package/src/plugins/builtin/market-halts/pane.tsx +3 -0
  251. package/src/plugins/builtin/market-movers/client.ts +4 -3
  252. package/src/plugins/builtin/market-movers/screener.ts +4 -3
  253. package/src/plugins/builtin/market-valuation/pane.tsx +2 -1
  254. package/src/plugins/builtin/members/pane.tsx +8 -3
  255. package/src/plugins/builtin/news/wire/feed-config.ts +29 -12
  256. package/src/plugins/builtin/news/wire/index.tsx +7 -2
  257. package/src/plugins/builtin/news/wire/news/preset-pane.tsx +11 -3
  258. package/src/plugins/builtin/news/wire/rss/cli.ts +3 -2
  259. package/src/plugins/builtin/news/wire/rss/source.ts +13 -4
  260. package/src/plugins/builtin/notes/cli.ts +1 -1
  261. package/src/plugins/builtin/notes/export.ts +17 -6
  262. package/src/plugins/builtin/notes/files.ts +16 -5
  263. package/src/plugins/builtin/options/enrichment-model.ts +10 -9
  264. package/src/plugins/builtin/options/footer.ts +25 -20
  265. package/src/plugins/builtin/options/live-quotes.ts +33 -18
  266. package/src/plugins/builtin/options/model-spot.ts +70 -4
  267. package/src/plugins/builtin/options/view.tsx +118 -35
  268. package/src/plugins/builtin/options-calculator/pane.tsx +7 -4
  269. package/src/plugins/builtin/options-positioning/headless.ts +11 -3
  270. package/src/plugins/builtin/options-positioning/model.ts +24 -0
  271. package/src/plugins/builtin/options-positioning/pane.tsx +10 -12
  272. package/src/plugins/builtin/options-scenario/client.ts +19 -1
  273. package/src/plugins/builtin/options-scenario/headless.ts +13 -6
  274. package/src/plugins/builtin/options-scenario/pane.tsx +54 -11
  275. package/src/plugins/builtin/plugin-marketplace/pane.tsx +5 -4
  276. package/src/plugins/builtin/portfolio-list/account-metrics.ts +3 -0
  277. package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +8 -5
  278. package/src/plugins/builtin/portfolio-list/cli/render.ts +34 -10
  279. package/src/plugins/builtin/portfolio-list/headless.ts +1 -1
  280. package/src/plugins/builtin/portfolio-list/pane/data.ts +23 -1
  281. package/src/plugins/builtin/portfolio-list/pane/index.tsx +33 -14
  282. package/src/plugins/builtin/portfolio-list/settings.ts +20 -2
  283. package/src/plugins/builtin/portfolio-list/summary/index.tsx +12 -0
  284. package/src/plugins/builtin/portfolio-list/summary/totals.ts +24 -3
  285. package/src/plugins/builtin/power/client.ts +2 -2
  286. package/src/plugins/builtin/power/pane.tsx +36 -31
  287. package/src/plugins/builtin/primary-dealers/client.ts +137 -62
  288. package/src/plugins/builtin/primary-dealers/headless.ts +30 -5
  289. package/src/plugins/builtin/primary-dealers/index.tsx +3 -2
  290. package/src/plugins/builtin/primary-dealers/model.ts +179 -129
  291. package/src/plugins/builtin/primary-dealers/pane.tsx +135 -15
  292. package/src/plugins/builtin/research/analyst-model.ts +5 -1
  293. package/src/plugins/builtin/reverse-dcf/client.ts +2 -1
  294. package/src/plugins/builtin/reverse-dcf/model.ts +4 -3
  295. package/src/plugins/builtin/risk-factors/index.tsx +12 -3
  296. package/src/plugins/builtin/scanner/feed.ts +6 -1
  297. package/src/plugins/builtin/scanner/headless.ts +1 -1
  298. package/src/plugins/builtin/scanner/lock-note.ts +3 -2
  299. package/src/plugins/builtin/seasonality/client.ts +2 -1
  300. package/src/plugins/builtin/sectors/client.ts +4 -3
  301. package/src/plugins/builtin/series-inventory.ts +1 -0
  302. package/src/plugins/builtin/shared/cloud-denied.ts +7 -6
  303. package/src/plugins/builtin/shared/cloud-resource.ts +15 -2
  304. package/src/plugins/builtin/shared/cloud-upgrade.ts +10 -11
  305. package/src/plugins/builtin/shared/free-routes.ts +24 -0
  306. package/src/plugins/builtin/shared/headless-options.ts +11 -0
  307. package/src/plugins/builtin/shared/pro-lock-note.ts +89 -7
  308. package/src/plugins/builtin/shared/research-trading.tsx +64 -0
  309. package/src/plugins/builtin/shared/series-list-detail.tsx +3 -1
  310. package/src/plugins/builtin/shared/volatility/options.ts +2 -1
  311. package/src/plugins/builtin/social-mentions/client.ts +2 -1
  312. package/src/plugins/builtin/social-mentions/headless.ts +1 -1
  313. package/src/plugins/builtin/social-mentions/lock-note.ts +1 -1
  314. package/src/plugins/builtin/themes/headless.ts +8 -2
  315. package/src/plugins/builtin/themes/model.ts +43 -10
  316. package/src/plugins/builtin/themes/pane.tsx +8 -6
  317. package/src/plugins/builtin/ticker-detail/overview/model.ts +44 -10
  318. package/src/plugins/builtin/ticker-detail/overview/option-contract.ts +64 -0
  319. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +82 -21
  320. package/src/plugins/builtin/ticker-detail/pane.tsx +21 -14
  321. package/src/plugins/builtin/ticker-detail/quote-footer.ts +5 -1
  322. package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +37 -8
  323. package/src/plugins/builtin/time-sales/pane.tsx +2 -2
  324. package/src/plugins/builtin/trace-bonds/client.ts +14 -3
  325. package/src/plugins/builtin/trace-bonds/headless.ts +1 -1
  326. package/src/plugins/builtin/vol-surface/client.ts +23 -3
  327. package/src/plugins/builtin/vol-surface/headless.ts +33 -14
  328. package/src/plugins/builtin/vol-surface/model.ts +30 -10
  329. package/src/plugins/builtin/vol-surface/pane.tsx +35 -15
  330. package/src/plugins/builtin/vol-surface/stored.ts +34 -3
  331. package/src/plugins/builtin/vol-surface/tables.ts +14 -0
  332. package/src/plugins/builtin-editorial.ts +1 -1
  333. package/src/plugins/compat.ts +3 -0
  334. package/src/plugins/external-runtime.ts +5 -0
  335. package/src/plugins/loader.ts +6 -0
  336. package/src/plugins/plugin-icon.ts +61 -0
  337. package/src/plugins/registry/host-actions.ts +2 -1
  338. package/src/plugins/registry/index.ts +9 -1
  339. package/src/plugins/runtime/context.tsx +5 -0
  340. package/src/plugins/runtime/index.tsx +3 -0
  341. package/src/portfolio/column-values.ts +13 -4
  342. package/src/portfolio/cost-fx.ts +5 -28
  343. package/src/public/__snapshots__/public-api.test.ts.snap +5 -0
  344. package/src/public/broker.ts +8 -0
  345. package/src/public/market-data.ts +5 -0
  346. package/src/renderers/dom/desktop/control-styles.ts +7 -1
  347. package/src/renderers/dom/desktop/controls.tsx +40 -8
  348. package/src/renderers/dom/desktop/icons.tsx +14 -0
  349. package/src/renderers/dom/desktop/list-view.tsx +51 -19
  350. package/src/renderers/dom/desktop/menu.tsx +10 -1
  351. package/src/renderers/dom/desktop/query-bar.tsx +2 -0
  352. package/src/renderers/dom/desktop/select-field.tsx +8 -6
  353. package/src/renderers/dom/host/style.ts +6 -1
  354. package/src/renderers/dom/host/tabs.tsx +22 -5
  355. package/src/renderers/dom/host/top-surface.ts +127 -0
  356. package/src/renderers/dom/styles.css +51 -5
  357. package/src/renderers/electrobun/bun/core-capabilities.ts +21 -1
  358. package/src/renderers/electrobun/bun/desktop/backend.ts +10 -0
  359. package/src/renderers/electrobun/bun/desktop/update.ts +210 -30
  360. package/src/renderers/electrobun/bun/external-plugins.ts +3 -0
  361. package/src/renderers/electrobun/shared/protocol.ts +2 -0
  362. package/src/renderers/electrobun/view/broker-remote-client.ts +16 -0
  363. package/src/renderers/electrobun/view/external-plugins.ts +1 -0
  364. package/src/renderers/electrobun/view/news-source.ts +4 -2
  365. package/src/renderers/electrobun/view/remote/asset-data-client.ts +8 -1
  366. package/src/renderers/opentui/start.tsx +7 -0
  367. package/src/series/validate.ts +2 -2
  368. package/src/sources/gloomberb-cloud/index.ts +57 -17
  369. package/src/sources/gloomberb-cloud/normalizers.ts +4 -0
  370. package/src/sources/history-result.ts +3 -0
  371. package/src/sources/provider-errors.ts +3 -1
  372. package/src/sources/provider-router/history.ts +49 -6
  373. package/src/sources/provider-router/index.ts +18 -1
  374. package/src/state/app/activity.ts +13 -0
  375. package/src/sync/controller.ts +4 -0
  376. package/src/sync/core-contributors.ts +62 -11
  377. package/src/sync/local-only.ts +121 -0
  378. package/src/sync/react.ts +6 -0
  379. package/src/sync/types.ts +3 -0
  380. package/src/telemetry/crash-reports.ts +4 -0
  381. package/src/telemetry/update-failures-node.ts +59 -0
  382. package/src/telemetry/update-failures.ts +307 -0
  383. package/src/tickers/company-name-listing.ts +9 -7
  384. package/src/tickers/company-name.ts +43 -0
  385. package/src/tickers/listing-fate.ts +57 -8
  386. package/src/tickers/search/index.ts +24 -3
  387. package/src/time-series/price-comparison.ts +35 -0
  388. package/src/time-series/resolve.ts +52 -11
  389. package/src/time-series/spec.ts +1 -1
  390. package/src/time-series/types.ts +5 -0
  391. package/src/types/broker.ts +56 -1
  392. package/src/types/config.ts +6 -0
  393. package/src/types/data-provider.ts +11 -1
  394. package/src/types/financials.ts +23 -0
  395. package/src/types/headless.ts +19 -2
  396. package/src/types/instrument.ts +4 -0
  397. package/src/types/issuer-listings.ts +28 -0
  398. package/src/types/plugin.ts +24 -6
  399. package/src/types/price-history.ts +6 -1
  400. package/src/types/trading.ts +67 -0
  401. package/src/ui/host.tsx +4 -0
  402. package/src/updater/failure.ts +163 -0
  403. package/src/updater/index.ts +87 -23
  404. package/src/utils/base-currency.ts +108 -0
  405. package/src/utils/currency-names.ts +128 -0
  406. package/src/utils/currency-pair.ts +28 -1
  407. package/src/utils/currency-units.ts +32 -1
  408. package/src/utils/dom-screenshot.ts +22 -9
  409. package/src/utils/exchange-notation.generated.ts +360 -0
  410. package/src/utils/exchanges.ts +15 -322
  411. package/src/utils/format.ts +31 -1
  412. package/src/utils/instrument-identity.ts +38 -1
  413. package/src/utils/options.ts +46 -1
  414. package/src/utils/price-history.ts +4 -0
  415. package/src/utils/reported-money.ts +8 -4
  416. package/src/utils/sec.ts +68 -0
@@ -6,8 +6,9 @@ import type {
6
6
  HeadlessPaneRow,
7
7
  } from "../../../types/plugin";
8
8
  import { formatCompact, formatCurrency } from "../../../utils/format";
9
- import { loadParsedInsiderFilings, type InsiderFilingsLoad } from "./client";
10
- import { insiderEmptyNotice } from "./issuer";
9
+ import { INSIDER_REPORT_FILINGS, loadParsedInsiderFilings, type InsiderFilingsLoad } from "./client";
10
+ import { insiderEmptyNotice, loadHomeInsiderReportsNote } from "./issuer";
11
+ import { FORM4_CURRENCY, withInsiderBaseValues } from "./base-values";
11
12
  import {
12
13
  buildInsiderRows,
13
14
  buildInsiderSummary,
@@ -20,7 +21,7 @@ import {
20
21
  import { isAmendedInsiderFiling, relevantInsiderAmendments } from "./amendments";
21
22
  import { formatInsiderName } from "./display";
22
23
  import { SEC_FILINGS } from "../shared/report-freshness";
23
- import { resolveHeadlessIssuerListing } from "../shared/headless-market-data";
24
+ import { resolveHeadlessInstrument, resolveHeadlessIssuerListing } from "../shared/headless-market-data";
24
25
  import { noSecReportsNotice } from "../../../utils/sec";
25
26
  import { parsePublicTickerKey } from "../../../utils/exchanges";
26
27
  import type { InsiderSignalRoleGroup, InsiderSignalSide, InsiderSignalsPayload } from "../../../api-client/insider-signals";
@@ -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(
@@ -290,6 +310,20 @@ async function noSecReportsEmpty(ctx: HeadlessPaneContext, key: string): Promise
290
310
  return noSecReportsNotice({ symbol: parsePublicTickerKey(key).symbol, exchange: listing.exchange, name: listing.name ?? null });
291
311
  }
292
312
 
313
+ /**
314
+ * What INS says for a company with no Form 4 of its own: why there is none,
315
+ * and for a company from abroad, that its home market's insider reports are
316
+ * not covered either ("Canadian insider reports (SEDI) not covered.").
317
+ */
318
+ async function insiderEmpty(ctx: HeadlessPaneContext, key: string, foreignPrivateIssuer: boolean, noFilings: boolean): Promise<string> {
319
+ const [noReports, { exchange }] = await Promise.all([
320
+ noFilings ? noSecReportsEmpty(ctx, key) : null,
321
+ resolveHeadlessInstrument(ctx, key),
322
+ ]);
323
+ const home = await loadHomeInsiderReportsNote(ctx.marketData, key, exchange, foreignPrivateIssuer);
324
+ return noReports ? [noReports, home].filter(Boolean).join(" ") : insiderEmptyNotice(foreignPrivateIssuer, home);
325
+ }
326
+
293
327
  export function createInsiderHeadless(
294
328
  dependencies: InsiderHeadlessDependencies = defaultDependencies,
295
329
  ): HeadlessPaneDefinition<"rows"> {
@@ -326,7 +360,7 @@ export function createInsiderHeadless(
326
360
  aliases: ["count", "rows"],
327
361
  description: "Maximum of the company's own Form 4 and Form 4/A filings to parse; without a ticker, companies to rank.",
328
362
  type: "integer",
329
- defaultValue: 20,
363
+ defaultValue: INSIDER_REPORT_FILINGS,
330
364
  minimum: 1,
331
365
  maximum: 100,
332
366
  },
@@ -441,9 +475,9 @@ export function createInsiderHeadless(
441
475
  const payload = await (dependencies.loadSignals ?? defaultDependencies.loadSignals!)(query, Number(args.options.limit), ctx);
442
476
  const side = payload.side ?? "buy";
443
477
  return {
444
- columns: signalColumns(side),
478
+ columns: labelDollarColumns(signalColumns(side), ["value"], ctx.config.baseCurrency),
445
479
  // 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"),
480
+ textColumns: labelDollarColumns(signalColumns(side).filter((column) => column.key !== "buyers"), ["value"], ctx.config.baseCurrency),
447
481
  rows: signalRows(payload),
448
482
  ...(payload.rows.length === 0 ? { empty: insiderSignalsEmptyTitle(payload.window, side, insiderSignalsFiltered(query)) } : {}),
449
483
  metadata: {
@@ -472,7 +506,7 @@ export function createInsiderHeadless(
472
506
  .filter((notice): notice is string => !!notice))];
473
507
  const rows = insidersRows(activity, side);
474
508
  return {
475
- columns: INSIDERS_COLUMNS,
509
+ columns: labelDollarColumns(INSIDERS_COLUMNS, ["bought", "sold"], ctx.config.baseCurrency),
476
510
  rows,
477
511
  ...(rows.length === 0 ? { empty: insiderActivityEmptyTitle(window) } : {}),
478
512
  metadata: {
@@ -490,8 +524,7 @@ export function createInsiderHeadless(
490
524
  }
491
525
  const limit = Number(args.options.limit);
492
526
  const { parsed, foreignPrivateIssuer, noFilings } = await dependencies.loadParsed(symbol, limit, args, ctx);
493
- const empty = parsed.length > 0 ? null
494
- : (noFilings ? await noSecReportsEmpty(ctx, symbol) : null) ?? insiderEmptyNotice(foreignPrivateIssuer);
527
+ const empty = parsed.length > 0 ? null : await insiderEmpty(ctx, symbol, foreignPrivateIssuer, noFilings);
495
528
  const name = String(args.options.name ?? "").trim().toLocaleLowerCase();
496
529
  const type = isInsiderTypeFilter(args.options.type) ? args.options.type : "all";
497
530
  const owned = name
@@ -503,12 +536,17 @@ export function createInsiderHeadless(
503
536
  .filter((row) => row.status !== "parsed" && row.status !== "disclosure")
504
537
  .map((row) => row.accessionNumber))];
505
538
  // 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) => {
539
+ const filed: HeadlessPaneRow[] = buildInsiderRows(filtered, parsed).map((row, index) => {
507
540
  const entry = filtered[index]!;
508
541
  const last = filtered[index + 1]?.filing.accessionNumber !== entry.filing.accessionNumber;
509
542
  return isAmendedInsiderFiling(entry) && last ? { ...row, note: amendmentNote(entry) || null } : row;
510
543
  });
544
+ // Every value says its currency; a base currency other than USD gets each value converted beside it.
545
+ const based = await withInsiderBaseValues(filed, ctx.config.baseCurrency, dependencies.fxHistory ?? ctx.apiClient);
546
+ const rows = based.rows;
511
547
  return {
548
+ ...(based.columns ? { columns: withMoneyColumns(based.columns) } : {}),
549
+ ...(based.note ? { notes: [based.note] } : {}),
512
550
  rows,
513
551
  // Nothing at all: the company has no Form 4 of its own, which is the whole answer.
514
552
  ...(empty ? { empty, unavailableSymbols: [] } : {}),
@@ -30,7 +30,7 @@ import {
30
30
  type StatItem,
31
31
  } from "../../../components";
32
32
  import { useDebouncedPluginPaneState, usePluginPaneState } from "../../runtime";
33
- import { mayBeSecEquityListing, mayBeUsEquityTicker, noSecReportsNotice, nonUsSecListingVenue } from "../../../utils/sec";
33
+ import { homeInsiderReportsNote, mayBeSecEquityListing, mayBeUsEquityTicker, noSecReportsNotice, nonUsSecListingVenue } from "../../../utils/sec";
34
34
  import { parsePublicTickerKey } from "../../../utils/exchanges";
35
35
  import { formatCompact } from "../../../utils/format";
36
36
  import { nextHeaderSort } from "../../../utils/sort-values";
@@ -56,6 +56,7 @@ import {
56
56
  } from "./model";
57
57
  import { formatInsiderName } from "./display";
58
58
  import { insiderHeadless } from "./headless";
59
+ import { FORM4_CURRENCY } from "./base-values";
59
60
  import { isInsiderForm } from "./insider-data";
60
61
  import { filesAsForeignPrivateIssuer, insiderEmptyNotice, isOtherIssuerFiling } from "./issuer";
61
62
  import { relevantInsiderAmendments } from "./amendments";
@@ -116,9 +117,10 @@ const TYPE_FILTER_LABELS: Record<InsiderTypeFilter, string> = {
116
117
  * security when there are several. A side with no trades shows 0 shares,
117
118
  * muted. Buys read in the gain colour and sales in the loss colour, as in
118
119
  * the table; when the Type filter hides open-market trades the figures dim
119
- * and say so.
120
+ * and say so. With `currency` the dollars read with its code (1.2M USD), as
121
+ * the table's CCY column names them.
120
122
  */
121
- function summaryItems(figures: InsiderSummaryFigures, typeFilter: InsiderTypeFilter): StatItem[] {
123
+ function summaryItems(figures: InsiderSummaryFigures, typeFilter: InsiderTypeFilter, currency?: string): StatItem[] {
122
124
  const securities = new Set(figures.totals.map((total) => total.security));
123
125
  const hidden = typeFilter === "other";
124
126
  const items: StatItem[] = [];
@@ -132,7 +134,8 @@ function summaryItems(figures: InsiderSummaryFigures, typeFilter: InsiderTypeFil
132
134
  label,
133
135
  value: total.unreconciled ? "Unavailable" : `${formatCompact(total.shares)} shares`,
134
136
  detail: [
135
- total.unreconciled ? "amended" : total.knownValue ? `$${formatCompact(total.value)}` : "value unavailable",
137
+ total.unreconciled ? "amended" : !total.knownValue ? "value unavailable"
138
+ : currency ? `${formatCompact(total.value)} ${currency}` : `$${formatCompact(total.value)}`,
136
139
  securities.size > 1 ? total.security : null,
137
140
  hidden ? "not in this view" : null,
138
141
  ].filter(Boolean).join(" · "),
@@ -337,7 +340,10 @@ function InsiderTradesView({ ticker, viewBar, state, width, height, focused, sta
337
340
  const order = new Map(rows.map((row, index) => [row.id, index]));
338
341
  return sortStackItems(rows, sort, compareInsiderRows, (a, b) => order.get(a.id)! - order.get(b.id)!);
339
342
  }, [rows, sort]);
340
- const columns = useMemo(() => buildInsiderColumns(width, rows), [rows, width]);
343
+ // Form 4 amounts are dollars: a reader whose base currency is another one gets them named, as PF names a holding's.
344
+ const baseCurrency = useAppSelector((state) => state.config.baseCurrency);
345
+ const amountCurrency = baseCurrency.trim().toUpperCase() === FORM4_CURRENCY ? undefined : FORM4_CURRENCY;
346
+ const columns = useMemo(() => buildInsiderColumns(width, rows, amountCurrency), [amountCurrency, rows, width]);
341
347
 
342
348
  // A narrowed list that cannot scroll cannot ask for older filings, so they
343
349
  // load here until it fills a page, back to a year at most.
@@ -358,7 +364,7 @@ function InsiderTradesView({ ticker, viewBar, state, width, height, focused, sta
358
364
  const lastSummaryRef = useRef<{ scope: string; figures: InsiderSummaryFigures } | null>(null);
359
365
  if (summary) lastSummaryRef.current = { scope: summaryScope, figures: summary };
360
366
  const shownSummary = summary ?? (lastSummaryRef.current?.scope === summaryScope ? lastSummaryRef.current.figures : null);
361
- const statItems = useMemo(() => shownSummary ? summaryItems(shownSummary, typeFilter) : [], [shownSummary, typeFilter]);
367
+ const statItems = useMemo(() => shownSummary ? summaryItems(shownSummary, typeFilter, amountCurrency) : [], [amountCurrency, shownSummary, typeFilter]);
362
368
  // The totals miss window filings only when the window passes the cap.
363
369
  const windowPartial = shownCount < windowCount;
364
370
  const insiderOptions = useMemo(() => {
@@ -483,9 +489,12 @@ function InsiderTradesView({ ticker, viewBar, state, width, height, focused, sta
483
489
  if (loading && allFilings.length === 0) return underBar(<Spinner label="Loading insider filings..." />);
484
490
  if (error && allFilings.length === 0) return underBar(<EmptyState title="Insider filings unavailable." message={error} />);
485
491
  if (noOwnFilings) {
492
+ // A company from abroad also says its home market's insider reports are not covered (RY:TSX: SEDI).
493
+ const foreignPrivateIssuer = filesAsForeignPrivateIssuer(allFilings);
494
+ const home = homeInsiderReportsNote({ exchange: abroadVenue, foreignPrivateIssuer });
486
495
  return underBar(renderFilingNotice(abroadVenue && allFilings.length === 0
487
- ? noSecReportsNotice({ symbol: parsePublicTickerKey(ticker.metadata.ticker).symbol, exchange: abroadVenue, name: ticker.metadata.name ?? null })
488
- : insiderEmptyNotice(filesAsForeignPrivateIssuer(allFilings))));
496
+ ? [noSecReportsNotice({ symbol: parsePublicTickerKey(ticker.metadata.ticker).symbol, exchange: abroadVenue, name: ticker.metadata.name ?? null }), home].filter(Boolean).join(" ")
497
+ : insiderEmptyNotice(foreignPrivateIssuer, home)));
489
498
  }
490
499
 
491
500
  // Until a filing is read there is nothing to say; after that the empty
@@ -1,4 +1,7 @@
1
- import type { SecFilingItem } from "../../../types/data-provider";
1
+ import type { DataProvider, SecFilingItem } from "../../../types/data-provider";
2
+ import { parsePublicTickerKey } from "../../../utils/exchanges";
3
+ import { logFailure } from "../../../utils/log-failure";
4
+ import { homeInsiderReportsNote, venueHomeCountry } from "../../../utils/sec";
2
5
  import { parseForm4IssuerCik } from "./insider-data";
3
6
 
4
7
  /**
@@ -40,10 +43,37 @@ export function filesAsForeignPrivateIssuer(filings: readonly SecFilingItem[]):
40
43
 
41
44
  /**
42
45
  * What the pane, `fn INS` and `insider` say when a company has no Form 4 of
43
- * its own. Each already names the company above it.
46
+ * its own, and beside it, for a company from abroad, that its insiders'
47
+ * reports at home are not covered either (`homeInsiderReportsNote`). Each
48
+ * already names the company above it.
44
49
  */
45
- export function insiderEmptyNotice(foreignPrivateIssuer: boolean): string {
46
- return foreignPrivateIssuer
50
+ export function insiderEmptyNotice(foreignPrivateIssuer: boolean, home: string | null = null): string {
51
+ const notice = foreignPrivateIssuer
47
52
  ? "No Form 4 insiders: foreign private issuers file none."
48
53
  : "No Form 4 insider filings.";
54
+ return home ? `${notice} ${home}` : notice;
55
+ }
56
+
57
+ /**
58
+ * `homeInsiderReportsNote` for a listing with no Form 4 of its own, placed by
59
+ * its company's country where the source knows the company (RY in New York
60
+ * is Royal Bank of Canada), else by its venue abroad (RY:TSX). A lookup that
61
+ * fails leaves the venue to say. A US listing of a company filing as a
62
+ * domestic one has no home market to name, and costs no lookup.
63
+ */
64
+ export async function loadHomeInsiderReportsNote(
65
+ provider: Pick<DataProvider, "getIssuerListings">,
66
+ key: string,
67
+ exchange: string | undefined,
68
+ foreignPrivateIssuer: boolean,
69
+ ): Promise<string | null> {
70
+ const parsed = parsePublicTickerKey(key);
71
+ const venue = parsed.exchange ?? exchange ?? "";
72
+ if (!foreignPrivateIssuer && !venueHomeCountry(venue)) return null;
73
+ const { getIssuerListings } = provider;
74
+ const issuer = getIssuerListings
75
+ ? await Promise.resolve().then(() => getIssuerListings.call(provider, parsed.symbol, venue))
76
+ .catch(logFailure("plugins.insider.issuer", "Reading the company's listings failed", null))
77
+ : null;
78
+ return homeInsiderReportsNote({ country: issuer?.country, exchange: venue, foreignPrivateIssuer });
49
79
  }
@@ -109,7 +109,7 @@ export async function loadInsiderSignals(
109
109
  if (isAccessDenied(error)) throw new Error(CLOUD_SESSION_REQUIRED, { cause: error });
110
110
  throw unavailableOnServer(error, INSIDER_SIGNALS_NOT_AVAILABLE);
111
111
  }),
112
- { force, validate: validateInsiderSignals },
112
+ { force, validate: validateInsiderSignals, signal },
113
113
  );
114
114
  }
115
115
 
@@ -187,7 +187,7 @@ export async function loadInsiderCompany(
187
187
  if (isAccessDenied(error)) throw new Error(CLOUD_SESSION_REQUIRED, { cause: error });
188
188
  throw unavailableOnServer(error, INSIDERS_NOT_AVAILABLE);
189
189
  }),
190
- { force, validate: companyValidator(query.ticker) },
190
+ { force, validate: companyValidator(query.ticker), signal },
191
191
  )));
192
192
  return bothSides(buy!, sell!);
193
193
  }
@@ -1,5 +1,5 @@
1
1
  import type { DataTableColumn } from "../../../components";
2
- import { displayWidth, formatCompact, formatCurrency } from "../../../utils/format";
2
+ import { displayWidth, formatCompact, formatCurrency, formatNumber } from "../../../utils/format";
3
3
  import { isAmendedInsiderFiling } from "./amendments";
4
4
  import { insiderSecurityTag, insiderTypeLabel, insiderTypeTone, isOpenMarketTrade, type InsiderTypeTone } from "./display";
5
5
  import {
@@ -10,12 +10,18 @@ import {
10
10
  type ParsedInsiderFiling,
11
11
  } from "./model";
12
12
 
13
- export type InsiderColumnId = "date" | "insider" | "role" | "type" | "security" | "shares" | "price" | "value";
13
+ export type InsiderColumnId = "date" | "insider" | "role" | "type" | "security" | "shares" | "price" | "value" | "ccy";
14
14
 
15
15
  export interface InsiderColumn extends DataTableColumn {
16
16
  id: InsiderColumnId;
17
17
  /** The type column has room for the 10b5-1 marker. */
18
18
  plan?: boolean;
19
+ /**
20
+ * The currency the CCY column names, and that Price and Value are written
21
+ * in without a sign, as PF writes LAST beside CCY. Unset when the amounts
22
+ * keep their $.
23
+ */
24
+ currency?: string;
19
25
  }
20
26
 
21
27
  /** One table row: a Form 4 line, or a filing without one (loading, unread, a disclosure). */
@@ -102,12 +108,21 @@ export function formatInsiderShares(value: number | null): string {
102
108
  return value == null ? "—" : formatCompact(value);
103
109
  }
104
110
 
105
- export function formatInsiderPrice(value: number | null): string {
106
- return value == null ? "—" : formatCurrency(value);
111
+ /** A Form 4 price: $412.50, or 412.50 when a CCY column names its currency. */
112
+ export function formatInsiderPrice(value: number | null, currency?: string): string {
113
+ if (value == null) return "—";
114
+ return currency ? formatNumber(value, 2) : formatCurrency(value);
107
115
  }
108
116
 
109
- export function formatInsiderValue(value: number | null): string {
110
- return value == null ? "—" : `$${formatCompact(value)}`;
117
+ /** A Form 4 value: $1.2M, or 1.2M when a CCY column names its currency. */
118
+ export function formatInsiderValue(value: number | null, currency?: string): string {
119
+ if (value == null) return "—";
120
+ return currency ? formatCompact(value) : `$${formatCompact(value)}`;
121
+ }
122
+
123
+ /** What a row's CCY cell says: the currency of its price and value, or a dash for a filing without either. */
124
+ export function insiderCurrencyText(row: Pick<InsiderTableRow, "price" | "value">, currency: string): string {
125
+ return row.price == null && row.value == null ? "—" : currency;
111
126
  }
112
127
 
113
128
  /** The quieter words after the type: "4/A" always, "10b5-1" when the column has room. */
@@ -136,9 +151,11 @@ function fitWidth(texts: readonly string[], label: string, max: number): number
136
151
  * When the pane runs short, the role goes first, then the price, the
137
152
  * 10b5-1 marker, the security and the share count, and only then does the name
138
153
  * column shrink below 16 cells, so a narrow pane still reads date, name,
139
- * type and value.
154
+ * type and value. With `currency` (Form 4 amounts read by someone whose base
155
+ * currency is not the dollar), CCY follows Value and is kept with it, as PF
156
+ * shows CCY once a row is in another currency than the totals.
140
157
  */
141
- export function buildInsiderColumns(width: number, rows: readonly InsiderTableRow[]): InsiderColumn[] {
158
+ export function buildInsiderColumns(width: number, rows: readonly InsiderTableRow[], currency?: string): InsiderColumn[] {
142
159
  const anyPlan = rows.some((row) => row.plan);
143
160
  const anySecurity = rows.some((row) => row.security);
144
161
  const names = rows.map((row) => row.name);
@@ -150,8 +167,9 @@ export function buildInsiderColumns(width: number, rows: readonly InsiderTableRo
150
167
  typePlan: fitWidth(rows.map((row) => insiderTypeText(row, true)), "Type", 19),
151
168
  security: fitWidth(rows.map((row) => row.security ?? ""), "Security", 12),
152
169
  shares: fitWidth(rows.map((row) => formatInsiderShares(row.shares)), "Shares", 9),
153
- price: fitWidth(rows.map((row) => formatInsiderPrice(row.price)), "Price", 10),
154
- value: fitWidth(rows.map((row) => formatInsiderValue(row.value)), "Value", 9),
170
+ price: fitWidth(rows.map((row) => formatInsiderPrice(row.price, currency)), "Price", 10),
171
+ value: fitWidth(rows.map((row) => formatInsiderValue(row.value, currency)), "Value", 9),
172
+ ccy: currency ? fitWidth(rows.map((row) => insiderCurrencyText(row, currency)), "CCY", 4) : 0,
155
173
  };
156
174
  const show = { role: true, price: true, plan: anyPlan, security: anySecurity, shares: true };
157
175
  let insider = widths.insider;
@@ -165,6 +183,7 @@ export function buildInsiderColumns(width: number, rows: readonly InsiderTableRo
165
183
  show.shares ? widths.shares : 0,
166
184
  show.price ? widths.price : 0,
167
185
  widths.value,
186
+ widths.ccy,
168
187
  ].reduce((sum, columnWidth) => sum + (columnWidth > 0 ? columnWidth + COLUMN_GAP : 0), 0);
169
188
  const shrinkInsider = (floor: number) => {
170
189
  insider = Math.max(Math.min(insider, floor), insider - Math.max(0, total() - available));
@@ -187,10 +206,11 @@ export function buildInsiderColumns(width: number, rows: readonly InsiderTableRo
187
206
  { id: "type", label: "Type", width: typeWidth, align: "left", plan: show.plan },
188
207
  ...(show.security ? [{ id: "security" as const, label: "Security", width: widths.security, align: "left" as const }] : []),
189
208
  ...(show.shares ? [{ id: "shares" as const, label: "Shares", width: widths.shares, align: "right" as const }] : []),
190
- ...(show.price ? [{ id: "price" as const, label: "Price", width: widths.price, align: "right" as const }] : []),
191
- { id: "value", label: "Value", width: widths.value, align: "right" },
209
+ ...(show.price ? [{ id: "price" as const, label: "Price", width: widths.price, align: "right" as const, currency }] : []),
210
+ { id: "value", label: "Value", width: widths.value, align: "right", currency },
211
+ ...(currency ? [{ id: "ccy" as const, label: "CCY", width: widths.ccy, align: "left" as const, currency }] : []),
192
212
  ];
193
- // Spare cells spread over the columns, so the value column ends at the
213
+ // Spare cells spread over the columns, so the last column ends at the
194
214
  // pane's edge in the terminal as it does on the desktop.
195
215
  const used = columns.reduce((sum, column) => sum + column.width + COLUMN_GAP, 0);
196
216
  const spare = Math.max(0, available - used);
@@ -222,5 +242,7 @@ export function compareInsiderRows(a: InsiderTableRow, b: InsiderTableRow, colum
222
242
  case "shares": return compareNullable(a.shares, b.shares);
223
243
  case "price": return compareNullable(a.price, b.price);
224
244
  case "value": return compareNullable(a.value, b.value);
245
+ // Every amount is in one currency: rows with one before filings without, as the CCY cells read.
246
+ case "ccy": return Number(a.price == null && a.value == null) - Number(b.price == null && b.value == null);
225
247
  }
226
248
  }
@@ -9,6 +9,7 @@ import {
9
9
  formatInsiderPrice,
10
10
  formatInsiderShares,
11
11
  formatInsiderValue,
12
+ insiderCurrencyText,
12
13
  insiderTypeMarkers,
13
14
  insiderTypeText,
14
15
  type InsiderColumn,
@@ -72,8 +73,10 @@ export function renderInsiderCell(
72
73
  case "shares":
73
74
  return { text: formatInsiderShares(row.shares), value: row.shares, color: figureColor };
74
75
  case "price":
75
- return { text: formatInsiderPrice(row.price), value: row.price, color: figureColor };
76
+ return { text: formatInsiderPrice(row.price, column.currency), value: row.price, color: figureColor };
76
77
  case "value":
77
- return { text: formatInsiderValue(row.value), value: row.value, color: figureColor };
78
+ return { text: formatInsiderValue(row.value, column.currency), value: row.value, color: figureColor };
79
+ case "ccy":
80
+ return { text: insiderCurrencyText(row, column.currency ?? ""), color: colors.textMuted };
78
81
  }
79
82
  }
@@ -0,0 +1,145 @@
1
+ import { getPublishedUsEquityCalendarDay, getPublishedUsEquitySession } from "../../../market-data/published-us-sessions";
2
+ import { zonedDateKey, zonedWallClockToUtcMs } from "../../../utils/zoned-date-time";
3
+ import type { MacroEventKind, MacroRelease } from "../macro-day/releases";
4
+ import type { DatedValue, IvHistoryModel, IvLookback } from "./model";
5
+
6
+ /** One release and the readings a set number of sessions from it. */
7
+ interface ReleaseOffsetRow {
8
+ kind: MacroEventKind;
9
+ /** The release's New York calendar day. */
10
+ releaseDate: string;
11
+ /** Sessions from the release: 0 is the first session on or after its day, -6 the sixth session before that. */
12
+ offset: number;
13
+ /** The session the readings are of. */
14
+ date: string;
15
+ iv30: number | null;
16
+ iv90: number | null;
17
+ iv365: number | null;
18
+ hv: number | null;
19
+ spread: number | null;
20
+ /** 25-delta put IV minus call IV, and the risk reversal, call minus put. */
21
+ skew25d: number | null;
22
+ rr25d: number | null;
23
+ }
24
+
25
+ export interface ReleaseOffsets {
26
+ rows: ReleaseOffsetRow[];
27
+ /** Releases whose session at the offset has not closed or has no closing reading yet, oldest first. */
28
+ pending: string[];
29
+ /** Releases inside the stored history whose session at the offset is before the lookback. */
30
+ beforeLookback: number;
31
+ /** Releases before the IV history starts. */
32
+ beforeHistory: number;
33
+ /** The first session of the IV history read, when there is one. */
34
+ historyStart: string | null;
35
+ }
36
+
37
+ const NEW_YORK = "America/New_York";
38
+ const day = (date: Date) => date.toISOString().slice(0, 10);
39
+ const byDay = (points: readonly DatedValue[]) => new Map(points.map((point) => [day(point.date), point.value]));
40
+ const nextDay = (date: string) => new Date(Date.parse(`${date}T00:00:00Z`) + 86_400_000).toISOString().slice(0, 10);
41
+
42
+ /** Whether `date`'s regular session has closed at `now`: an earlier New York day, or today from its close on. */
43
+ function sessionClosed(date: string, now: number): boolean {
44
+ const today = zonedDateKey(now, NEW_YORK);
45
+ if (date !== today) return date < today;
46
+ const session = getPublishedUsEquitySession("NYSE", date);
47
+ const [year, month, dayOfMonth] = date.split("-").map(Number) as [number, number, number];
48
+ return now >= (session?.kind === "session" ? session.close : zonedWallClockToUtcMs(NEW_YORK, year, month, dayOfMonth, 16, 0, 0));
49
+ }
50
+
51
+ /** The US equity sessions after `after` through the first on or after `until`; null where the published calendar ends. */
52
+ function calendarSessions(after: string, until: string): string[] | null {
53
+ const sessions: string[] = [];
54
+ for (let date = nextDay(after); ; date = nextDay(date)) {
55
+ const kind = getPublishedUsEquityCalendarDay("NYSE", date);
56
+ if (kind == null) return null;
57
+ if (kind !== "session") continue;
58
+ sessions.push(date);
59
+ if (date >= until) return sessions;
60
+ }
61
+ }
62
+
63
+ /**
64
+ * One row per release of `kind`, with the readings on the session `offset`
65
+ * sessions from it (the "week before FOMC" is `-5` or `-6`), oldest first.
66
+ * Offset 0 is the first session on or after the release day, so a release on
67
+ * a day with no session counts from the next one. Sessions are the history's
68
+ * own, from the first IV reading in the lookback through the last closed
69
+ * session read (`readThrough`, or the last IV reading), and past that the
70
+ * published exchange calendar: an upcoming release whose session at a
71
+ * negative offset has closed and been read gets its row. A release whose
72
+ * session at the offset has not closed or has no reading yet is pending; one
73
+ * whose session falls before the history read is counted, before the lookback
74
+ * or before the history.
75
+ */
76
+ export function releaseOffsetRows(
77
+ model: Pick<IvHistoryModel, "iv30" | "iv90" | "iv365" | "hv" | "spread" | "skew" | "since">,
78
+ releases: readonly MacroRelease[],
79
+ kind: MacroEventKind,
80
+ offset: number,
81
+ { lookback = "ALL", readThrough = null, now = Date.now() }: { lookback?: IvLookback; readThrough?: string | null; now?: number } = {},
82
+ ): ReleaseOffsets {
83
+ const result: ReleaseOffsets = { rows: [], pending: [], beforeLookback: 0, beforeHistory: 0, historyStart: null };
84
+ // Sessions from the first IV reading on: realized volatility alone, before it or without it, is no IV row.
85
+ const first = model.iv30[0] ? day(model.iv30[0].date) : null;
86
+ if (!first) return result;
87
+ const lastReading = day(model.iv30.at(-1)!.date);
88
+ const through = readThrough && readThrough > lastReading ? readThrough : lastReading;
89
+ // A session counts once it has closed and been read: today's bar before the close, or a session the IV history has not reached, is no reading.
90
+ const known = [...new Set([...model.iv30, ...model.hv].map((point) => day(point.date)))]
91
+ .filter((session) => session >= first && session <= through && sessionClosed(session, now)).sort();
92
+ if (known.length === 0) return result;
93
+ result.historyStart = known[0]!;
94
+ // The lookback cut a longer stored history: a release before it is outside the lookback, not before the history.
95
+ const trimmed = lookback !== "ALL" && model.since != null && model.since < first;
96
+ const before = (release: MacroRelease) => {
97
+ if (trimmed && release.date >= model.since!) result.beforeLookback += 1;
98
+ else result.beforeHistory += 1;
99
+ };
100
+ const iv30 = byDay(model.iv30), iv90 = byDay(model.iv90), iv365 = byDay(model.iv365);
101
+ const hv = byDay(model.hv), spread = byDay(model.spread), skew = byDay(model.skew);
102
+ for (const release of releases) {
103
+ if (release.kind !== kind) continue;
104
+ if (release.date < known[0]!) {
105
+ before(release);
106
+ continue;
107
+ }
108
+ const anchor = known.findIndex((session) => session >= release.date);
109
+ let index: number;
110
+ if (anchor >= 0) index = anchor + offset;
111
+ else {
112
+ // After the last session read: count on through the exchange calendar to the release's own session.
113
+ const ahead = calendarSessions(known.at(-1)!, release.date);
114
+ if (!ahead) {
115
+ result.pending.push(release.date);
116
+ continue;
117
+ }
118
+ index = known.length + ahead.length - 1 + offset;
119
+ }
120
+ if (index < 0) {
121
+ before(release);
122
+ continue;
123
+ }
124
+ if (index >= known.length) {
125
+ result.pending.push(release.date);
126
+ continue;
127
+ }
128
+ const date = known[index]!;
129
+ const skewValue = skew.get(date) ?? null;
130
+ result.rows.push({
131
+ kind, releaseDate: release.date, offset, date,
132
+ iv30: iv30.get(date) ?? null, iv90: iv90.get(date) ?? null, iv365: iv365.get(date) ?? null,
133
+ hv: hv.get(date) ?? null, spread: spread.get(date) ?? null,
134
+ skew25d: skewValue, rr25d: skewValue == null ? null : -skewValue,
135
+ });
136
+ }
137
+ return result;
138
+ }
139
+
140
+ /** "6 sessions before", "release day", "1 session after". */
141
+ export function offsetText(offset: number): string {
142
+ if (offset === 0) return "release day";
143
+ const sessions = `${Math.abs(offset)} session${Math.abs(offset) === 1 ? "" : "s"}`;
144
+ return offset < 0 ? `${sessions} before` : `${sessions} after`;
145
+ }
@@ -12,6 +12,9 @@ export const VCA_DEFINITIONS = [
12
12
  "VIX is Cboe's 30-day S&P 500 implied vol from the whole SPX strike strip, skew included; IV30 here is at the money, so VIX usually reads higher.",
13
13
  ] as const;
14
14
 
15
+ /** Words for implied volatility that are no function's code: `fn IVOL` names HIVG and OVDV, which list them as `suggestFor`. */
16
+ export const IMPLIED_VOL_WORDS = ["IVOL", "IV", "OVOL", "IMPVOL", "IMPLIEDVOL"];
17
+
15
18
  /**
16
19
  * A skew column's header names its sign, since the conventions differ: the
17
20
  * 25-delta skew is put minus call (HIVG, VCA), the risk reversal call minus