gloomberb 0.9.2 → 0.9.4

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 (233) hide show
  1. package/README.md +24 -2
  2. package/bin/gloomberb +1 -1
  3. package/package.json +1 -1
  4. package/src/app/runtime/desktop-menu.ts +5 -1
  5. package/src/app/runtime/ticker-refresh.ts +4 -1
  6. package/src/app.tsx +20 -8
  7. package/src/brokers/account-cache.ts +1 -1
  8. package/src/brokers/sync-broker-instance.test.ts +107 -0
  9. package/src/brokers/sync-broker-instance.ts +22 -10
  10. package/src/capabilities/registry.ts +1 -0
  11. package/src/capabilities/types.ts +23 -0
  12. package/src/cli/commands/api.ts +119 -0
  13. package/src/cli/commands/automation.ts +173 -0
  14. package/src/cli/commands/command-utils.ts +53 -0
  15. package/src/cli/commands/market.ts +527 -0
  16. package/src/cli/commands/overview.ts +222 -0
  17. package/src/cli/commands/search.ts +39 -0
  18. package/src/cli/commands/system.ts +553 -0
  19. package/src/cli/commands/ticker.ts +104 -12
  20. package/src/cli/context.ts +35 -2
  21. package/src/cli/desktop-pane-shot.ts +2 -1
  22. package/src/cli/entry.ts +51 -0
  23. package/src/cli/errors.ts +56 -5
  24. package/src/cli/index.ts +73 -11
  25. package/src/cli/options.test.ts +38 -0
  26. package/src/cli/options.ts +95 -0
  27. package/src/cli/pane-functions/index.ts +10 -1
  28. package/src/cli/pane-functions/screenshot.ts +13 -1
  29. package/src/cli/registry.test.ts +40 -0
  30. package/src/cli/registry.ts +46 -5
  31. package/src/cli/result.test.ts +82 -0
  32. package/src/cli/result.ts +149 -0
  33. package/src/cli/types.ts +11 -0
  34. package/src/cli.test.ts +11 -11
  35. package/src/components/chart/bar-chart-renderer.test.ts +108 -3
  36. package/src/components/chart/bar-chart-renderer.ts +226 -27
  37. package/src/components/chart/comparison-stock-chart/interactions.ts +7 -1
  38. package/src/components/chart/comparison-stock-chart/legend.tsx +4 -0
  39. package/src/components/chart/comparison-stock-chart/render-data.ts +37 -8
  40. package/src/components/chart/comparison-stock-chart/selection-runtime.ts +19 -2
  41. package/src/components/chart/comparison-stock-chart/sources.ts +8 -4
  42. package/src/components/chart/comparison-stock-chart/toolbar.tsx +13 -3
  43. package/src/components/chart/comparison-stock-chart/view.tsx +12 -2
  44. package/src/components/chart/core/pane-settings.test.ts +27 -0
  45. package/src/components/chart/core/pane-settings.ts +60 -1
  46. package/src/components/chart/core/pointer.ts +34 -6
  47. package/src/components/chart/price-axis-labels.tsx +3 -1
  48. package/src/components/chart/static/bar-chart-surface.test.tsx +97 -0
  49. package/src/components/chart/static/bar-chart-surface.tsx +151 -28
  50. package/src/components/chart/static/chart/axis-overlays.tsx +243 -0
  51. package/src/components/chart/static/chart/surface.test.tsx +92 -0
  52. package/src/components/chart/static/chart/surface.tsx +167 -24
  53. package/src/components/chart/stock-chart/index.test.ts +25 -0
  54. package/src/components/chart/stock-chart/resolved/runtime.ts +1 -1
  55. package/src/components/chart/stock-chart/settings.ts +7 -3
  56. package/src/components/chart/stock-chart/viewport/runtime.ts +5 -5
  57. package/src/components/command-bar/layout-items/current-layout.ts +5 -1
  58. package/src/components/command-bar/pane-templates/workflow-route.ts +10 -0
  59. package/src/components/command-bar/surface/pane.test.tsx +76 -0
  60. package/src/components/data-table/view.tsx +13 -2
  61. package/src/components/feed-data-table/stack-view.tsx +9 -1
  62. package/src/components/index.ts +1 -1
  63. package/src/components/input-search-bar.test.tsx +103 -0
  64. package/src/components/input-search-bar.tsx +16 -0
  65. package/src/components/layout/detached-pane-shell.tsx +6 -3
  66. package/src/components/layout/floating-pane.tsx +3 -2
  67. package/src/components/layout/pane/content.tsx +20 -8
  68. package/src/components/layout/pane/footer/registration.tsx +6 -2
  69. package/src/components/layout/pane/frame.tsx +4 -4
  70. package/src/components/layout/pane/header.tsx +40 -4
  71. package/src/components/layout/pane/index.tsx +3 -1
  72. package/src/components/layout/pane/sizing.test.ts +43 -0
  73. package/src/components/layout/pane/sizing.ts +13 -4
  74. package/src/components/layout/shell/active-drag.ts +5 -1
  75. package/src/components/layout/shell/cursor-occlusion.ts +196 -0
  76. package/src/components/layout/shell/drag/runtime.ts +28 -3
  77. package/src/components/layout/shell/fullscreen.ts +10 -0
  78. package/src/components/layout/shell/index.test.tsx +487 -4
  79. package/src/components/layout/shell/index.tsx +165 -4
  80. package/src/components/layout/shell/layout-state.test.ts +33 -0
  81. package/src/components/layout/shell/layout-state.ts +4 -9
  82. package/src/components/layout/shell/native/pointer-runtime.ts +10 -4
  83. package/src/components/layout/shell/pane/layers.tsx +34 -16
  84. package/src/components/layout/shell/pane/management-shortcuts.ts +19 -0
  85. package/src/components/layout/shell/shortcuts.ts +4 -0
  86. package/src/components/layout/shell/terminal-pointer-runtime.ts +29 -19
  87. package/src/components/layout/shell/visible-layout.ts +17 -0
  88. package/src/components/layout/shell/window-mode/index.ts +11 -3
  89. package/src/components/layout/shell/window-mode/overlays.tsx +8 -1
  90. package/src/components/layout/status-bar.test.tsx +111 -1
  91. package/src/components/layout/status-bar.tsx +37 -7
  92. package/src/components/layout/transient-layout.tsx +57 -0
  93. package/src/components/layout/window-edit/mode.test.ts +76 -5
  94. package/src/components/layout/window-edit/mode.ts +47 -2
  95. package/src/components/layout/window-edit/presentation.ts +2 -1
  96. package/src/components/onboarding/onboarding-steps.tsx +8 -28
  97. package/src/components/onboarding/onboarding-wizard.test.tsx +13 -12
  98. package/src/components/onboarding/onboarding-wizard.tsx +2 -4
  99. package/src/components/onboarding/portfolio-step/index.tsx +0 -12
  100. package/src/components/onboarding/portfolio-step/types.ts +1 -3
  101. package/src/components/onboarding/wizard-keyboard.ts +2 -5
  102. package/src/components/price-performance.tsx +9 -1
  103. package/src/components/table-view-shared.tsx +12 -4
  104. package/src/components/ui/data-table/opentui/index.tsx +25 -12
  105. package/src/components/ui/data-table/types.ts +4 -0
  106. package/src/components/ui/fields.tsx +10 -3
  107. package/src/components/ui/page-stack-view.tsx +6 -2
  108. package/src/components/ui/table-layout.test.ts +25 -0
  109. package/src/components/ui/table-layout.ts +22 -6
  110. package/src/components/ui/tabs.tsx +26 -10
  111. package/src/components/ui/ui.test.tsx +87 -0
  112. package/src/core/app-services.ts +7 -4
  113. package/src/core/state/app/layout-reducer.ts +2 -2
  114. package/src/data/config/store/index.ts +1 -2
  115. package/src/plugins/builtin/analytics/index.test.tsx +6 -2
  116. package/src/plugins/builtin/analytics/pane-model.ts +43 -8
  117. package/src/plugins/builtin/cloud-tweets/pane.tsx +2 -0
  118. package/src/plugins/builtin/cloud-tweets/search-bar.tsx +3 -0
  119. package/src/plugins/builtin/cloud-tweets/table.tsx +14 -2
  120. package/src/plugins/builtin/comparison-chart/index.test.tsx +130 -6
  121. package/src/plugins/builtin/kelly-sizer/constants.ts +3 -0
  122. package/src/plugins/builtin/kelly-sizer/fields.ts +214 -0
  123. package/src/plugins/builtin/kelly-sizer/index.test.tsx +197 -0
  124. package/src/plugins/builtin/kelly-sizer/index.tsx +47 -0
  125. package/src/plugins/builtin/kelly-sizer/model.test.ts +152 -0
  126. package/src/plugins/builtin/kelly-sizer/model.ts +580 -0
  127. package/src/plugins/builtin/kelly-sizer/pane.tsx +558 -0
  128. package/src/plugins/builtin/kelly-sizer/portfolio.ts +71 -0
  129. package/src/plugins/builtin/kelly-sizer/sections.tsx +142 -0
  130. package/src/plugins/builtin/kelly-sizer/state.ts +67 -0
  131. package/src/plugins/builtin/kelly-sizer/test-support.tsx +174 -0
  132. package/src/plugins/builtin/kelly-sizer/types.ts +156 -0
  133. package/src/plugins/builtin/kelly-sizer/view.test.tsx +77 -0
  134. package/src/plugins/builtin/kelly-sizer/view.tsx +375 -0
  135. package/src/plugins/builtin/layout-manager/index.tsx +5 -1
  136. package/src/plugins/builtin/news/wire/read-state.ts +9 -32
  137. package/src/plugins/builtin/options/view.test.tsx +141 -4
  138. package/src/plugins/builtin/options/view.tsx +76 -15
  139. package/src/plugins/builtin/portfolio-list/account-metrics.test.ts +26 -1
  140. package/src/plugins/builtin/portfolio-list/account-metrics.ts +14 -0
  141. package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +23 -0
  142. package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +19 -2
  143. package/src/plugins/builtin/portfolio-list/index.test.ts +23 -0
  144. package/src/plugins/builtin/portfolio-list/pane/index.tsx +3 -1
  145. package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +2 -7
  146. package/src/plugins/builtin/portfolio-list/summary/index.test.ts +59 -1
  147. package/src/plugins/builtin/portfolio-list/summary/index.tsx +36 -5
  148. package/src/plugins/builtin/shared/read-state.ts +43 -0
  149. package/src/plugins/builtin/substack/article-detail.test.tsx +10 -0
  150. package/src/plugins/builtin/substack/article-detail.tsx +39 -15
  151. package/src/plugins/builtin/substack/article-stack.tsx +6 -2
  152. package/src/plugins/builtin/substack/feed-tabs.tsx +12 -9
  153. package/src/plugins/builtin/substack/pane.tsx +8 -3
  154. package/src/plugins/builtin/substack/read-state.ts +55 -0
  155. package/src/plugins/builtin/thirteenf/pane.test.tsx +26 -0
  156. package/src/plugins/builtin/thirteenf/pane.tsx +12 -1
  157. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/content.tsx +149 -48
  158. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/model.ts +25 -2
  159. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph.tsx +5 -4
  160. package/src/plugins/builtin/ticker-detail/data-panes.test.ts +16 -0
  161. package/src/plugins/builtin/ticker-detail/index.test.tsx +143 -0
  162. package/src/plugins/catalog-ui.ts +2 -0
  163. package/src/plugins/ibkr/broker-adapter.ts +19 -0
  164. package/src/plugins/ibkr/config.test.ts +31 -2
  165. package/src/plugins/ibkr/config.ts +9 -2
  166. package/src/plugins/ibkr/flex/index.test.ts +18 -9
  167. package/src/plugins/ibkr/flex/index.ts +30 -0
  168. package/src/plugins/ibkr/flex/statement-client.ts +68 -14
  169. package/src/plugins/ibkr/gateway/account-loaders.ts +1 -1
  170. package/src/plugins/ibkr/gateway/account-summary.ts +1 -0
  171. package/src/plugins/ibkr/gateway/service/index.test.ts +8 -0
  172. package/src/plugins/pane-manager/docking.ts +9 -2
  173. package/src/plugins/pane-manager/gridlock.ts +13 -5
  174. package/src/plugins/pane-manager/layout-state.ts +25 -0
  175. package/src/plugins/pane-manager/queries.ts +8 -2
  176. package/src/plugins/pane-manager.test.ts +33 -0
  177. package/src/plugins/pane-manager.ts +6 -1
  178. package/src/plugins/prediction-markets/chart.tsx +10 -4
  179. package/src/plugins/prediction-markets/controller/index.ts +2 -0
  180. package/src/plugins/prediction-markets/controller/keyboard.ts +34 -5
  181. package/src/plugins/prediction-markets/detail/book.tsx +141 -96
  182. package/src/plugins/prediction-markets/detail/overview.tsx +3 -0
  183. package/src/plugins/prediction-markets/detail/pane.tsx +71 -64
  184. package/src/plugins/prediction-markets/detail/shared.tsx +19 -0
  185. package/src/plugins/prediction-markets/detail/trades.tsx +64 -19
  186. package/src/plugins/prediction-markets/detail.test.tsx +50 -0
  187. package/src/plugins/prediction-markets/pane.test.tsx +75 -0
  188. package/src/plugins/prediction-markets/pane.tsx +71 -23
  189. package/src/plugins/prediction-markets/test-helpers.tsx +42 -23
  190. package/src/renderers/electrobun/bun/application-menu/click.test.ts +9 -0
  191. package/src/renderers/electrobun/bun/application-menu/click.ts +1 -0
  192. package/src/renderers/electrobun/bun/application-menu/index.test.ts +9 -0
  193. package/src/renderers/electrobun/bun/application-menu/index.ts +3 -2
  194. package/src/renderers/electrobun/bun/index.ts +13 -0
  195. package/src/renderers/electrobun/bun/window/focus.test.ts +35 -0
  196. package/src/renderers/electrobun/bun/window/focus.ts +5 -0
  197. package/src/renderers/electrobun/view/app-services.ts +6 -4
  198. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +13 -2
  199. package/src/renderers/electrobun/view/data-table/index.tsx +19 -4
  200. package/src/renderers/electrobun/view/data-table/row.tsx +30 -6
  201. package/src/renderers/electrobun/view/desktop/controls.tsx +11 -5
  202. package/src/renderers/electrobun/view/host/ascii-text.ts +50 -0
  203. package/src/renderers/electrobun/view/host/input.tsx +115 -18
  204. package/src/renderers/electrobun/view/host/tabs.tsx +7 -2
  205. package/src/renderers/electrobun/view/host/text.test.ts +22 -0
  206. package/src/renderers/electrobun/view/host/text.tsx +24 -20
  207. package/src/renderers/electrobun/view/styles.css +11 -0
  208. package/src/renderers/electrobun/view/ui-host.tsx +1 -1
  209. package/src/renderers/opentui/host.tsx +17 -1
  210. package/src/renderers/opentui/start.tsx +13 -5
  211. package/src/renderers/opentui/ui-host.tsx +57 -25
  212. package/src/sources/provider-router/batches.ts +16 -2
  213. package/src/sources/provider-router/financial-routes.ts +5 -0
  214. package/src/sources/provider-router/financials.ts +11 -23
  215. package/src/sources/provider-router/index.test.ts +189 -0
  216. package/src/sources/provider-router/primary.ts +3 -2
  217. package/src/sources/sec-edgar.test.ts +83 -0
  218. package/src/sources/sec-edgar.ts +231 -0
  219. package/src/sources/yahoo-finance.test.ts +51 -0
  220. package/src/sources/yahoo-finance.ts +49 -1
  221. package/src/state/app/context/index.test.tsx +5 -8
  222. package/src/state/app/input-capture.ts +12 -0
  223. package/src/types/broker.ts +1 -0
  224. package/src/types/config.ts +10 -0
  225. package/src/types/plugin.ts +28 -0
  226. package/src/types/trading.ts +2 -0
  227. package/src/ui/host.tsx +16 -0
  228. package/src/ui/index.tsx +2 -0
  229. package/src/utils/cli-output.ts +6 -0
  230. package/src/utils/financial-statements.ts +23 -0
  231. package/src/utils/search-focus-navigation.ts +24 -0
  232. package/src/version.ts +1 -1
  233. package/src/components/onboarding/portfolio-step/manual-name-panel.tsx +0 -62
@@ -71,9 +71,19 @@ export function buildFundamentalGraphRows(
71
71
  symbol = "",
72
72
  period: FundamentalPeriod = "annual",
73
73
  ): FundamentalGraphRow[] {
74
- const sorted = [...statements]
74
+ const sortedStatements = [...statements]
75
75
  .filter((statement) => typeof statement[metric] === "number")
76
76
  .sort((left, right) => left.date.localeCompare(right.date));
77
+ const byCategory = new Map<string, FinancialStatement>();
78
+ for (const statement of sortedStatements) {
79
+ const category = periodCategory(statement.date, period);
80
+ const key = `${symbol}\u0000${category}`;
81
+ const previous = byCategory.get(key);
82
+ if (!previous || statement.date.localeCompare(previous.date) >= 0) {
83
+ byCategory.set(key, statement);
84
+ }
85
+ }
86
+ const sorted = [...byCategory.values()].sort((left, right) => left.date.localeCompare(right.date));
77
87
  const maxAbs = Math.max(1, ...sorted.map((statement) => Math.abs(statement[metric] as number)));
78
88
  return sorted.map((statement, index) => {
79
89
  const value = statement[metric] as number;
@@ -159,10 +169,23 @@ export function buildValuationGraphRows(
159
169
  });
160
170
  }
161
171
 
162
- function metricDefs(kind: GraphKind): ReadonlyArray<MetricDefinition> {
172
+ export function metricDefs(kind: GraphKind): ReadonlyArray<MetricDefinition> {
163
173
  return kind === "valuation" ? VALUATION_METRICS : FUNDAMENTAL_METRICS;
164
174
  }
165
175
 
176
+ export function allMetricDefs(): ReadonlyArray<{ kind: GraphKind; definition: MetricDefinition }> {
177
+ return [
178
+ ...FUNDAMENTAL_METRICS.map((definition) => ({ kind: "fundamental" as const, definition })),
179
+ ...VALUATION_METRICS.map((definition) => ({ kind: "valuation" as const, definition })),
180
+ ];
181
+ }
182
+
183
+ export function metricKind(metric: GraphMetricKey): GraphKind | null {
184
+ if (FUNDAMENTAL_METRICS.some((definition) => definition.key === metric)) return "fundamental";
185
+ if (VALUATION_METRICS.some((definition) => definition.key === metric)) return "valuation";
186
+ return null;
187
+ }
188
+
166
189
  export function defaultMetric(kind: GraphKind): GraphMetricKey {
167
190
  return kind === "valuation" ? "priceSales" : "totalRevenue";
168
191
  }
@@ -1,5 +1,5 @@
1
1
  import { useMemo } from "react";
2
- import type { PaneProps, PaneTemplateDef } from "../../../../types/plugin";
2
+ import type { PaneProps, PaneTemplateDef, TickerResearchTabProps } from "../../../../types/plugin";
3
3
  import { formatTickerListInput } from "../../../../tickers/list";
4
4
  import { usePaneInstance, usePaneTicker } from "../../../../state/app/context";
5
5
  import { usePluginPaneState } from "../../../runtime";
@@ -26,7 +26,7 @@ export function FundamentalGraphPane({ focused, width, height }: PaneProps) {
26
26
  const { symbol, exchange } = useBoundTicker();
27
27
  const symbols = useMemo(() => symbolsFromPaneSettings(pane?.settings, symbol), [pane?.settings, symbol]);
28
28
  const configuredKind = graphKindFromSettings(pane?.settings, "fundamental");
29
- const [period, setPeriod] = usePluginPaneState<FundamentalPeriod>("period", "annual");
29
+ const [period, setPeriod] = usePluginPaneState<FundamentalPeriod>("period", "quarterly");
30
30
  const [chartKind, setChartKind] = usePluginPaneState<GraphKind>("chartKind", configuredKind);
31
31
  const [metric, setMetric] = usePluginPaneState<GraphMetricKey>("metric", defaultMetric(configuredKind));
32
32
  const resolvedMetric = isMetricForKind(chartKind, metric) ? metric : defaultMetric(chartKind);
@@ -54,9 +54,9 @@ export function FundamentalGraphPane({ focused, width, height }: PaneProps) {
54
54
  );
55
55
  }
56
56
 
57
- export function FundamentalGraphsResearchTab({ focused, width, height }: { focused: boolean; width: number; height: number }) {
57
+ export function FundamentalGraphsResearchTab({ focused, width, height, onCapture }: TickerResearchTabProps) {
58
58
  const { symbol, financials } = usePaneTicker();
59
- const [period, setPeriod] = usePluginPaneState<FundamentalPeriod>("detailPeriod", "annual");
59
+ const [period, setPeriod] = usePluginPaneState<FundamentalPeriod>("detailPeriod", "quarterly");
60
60
  const [chartKind, setChartKind] = usePluginPaneState<GraphKind>("detailChartKind", "fundamental");
61
61
  const [metric, setMetric] = usePluginPaneState<GraphMetricKey>("detailMetric", "totalRevenue");
62
62
  const resolvedMetric = isMetricForKind(chartKind, metric) ? metric : defaultMetric(chartKind);
@@ -79,6 +79,7 @@ export function FundamentalGraphsResearchTab({ focused, width, height }: { focus
79
79
  setMetric={setMetric}
80
80
  chartKind={chartKind}
81
81
  setChartKind={setChartKind}
82
+ onCapture={onCapture}
82
83
  />
83
84
  );
84
85
  }
@@ -49,6 +49,22 @@ describe("ticker data panes", () => {
49
49
  ]);
50
50
  });
51
51
 
52
+ test("collapses duplicate quarterly statement dates into one displayed period", () => {
53
+ const rows = buildFundamentalGraphRows([
54
+ { date: "2025-03-29", eps: 0.44 },
55
+ { date: "2025-03-31", eps: 0.44 },
56
+ { date: "2025-06-28", eps: 0.54 },
57
+ { date: "2025-06-30", eps: 0.54 },
58
+ { date: "2025-09-27", eps: 0.75 },
59
+ ] satisfies FinancialStatement[], "eps", "AMD", "quarterly");
60
+
61
+ expect(rows.map((row) => [row.date, row.category, row.value])).toEqual([
62
+ ["2025-03-31", "2025 Q1", 0.44],
63
+ ["2025-06-30", "2025 Q2", 0.54],
64
+ ["2025-09-27", "2025 Q3", 0.75],
65
+ ]);
66
+ });
67
+
52
68
  test("builds valuation graph rows from available multiples", () => {
53
69
  const rows = buildValuationGraphRows({
54
70
  quote: { symbol: "AMD", price: 100, currency: "USD", change: 0, changePercent: 0, lastUpdated: 1, marketCap: 1_000 },
@@ -305,6 +305,25 @@ async function clickFrameText(text: string) {
305
305
  await flushFrame();
306
306
  }
307
307
 
308
+ async function emitKeypress(event: { name?: string; sequence?: string }) {
309
+ await act(async () => {
310
+ (testSetup!.renderer as any).keyInput.emit("keypress", {
311
+ ctrl: false,
312
+ meta: false,
313
+ option: false,
314
+ shift: false,
315
+ eventType: "press",
316
+ repeated: false,
317
+ preventDefault: () => {},
318
+ stopPropagation: () => {},
319
+ ...event,
320
+ });
321
+ await Promise.resolve();
322
+ await testSetup!.renderOnce();
323
+ });
324
+ await flushFrame();
325
+ }
326
+
308
327
  function spanLineText(line: { spans: Array<{ text: string }> }): string {
309
328
  return line.spans.map((span) => span.text).join("");
310
329
  }
@@ -531,6 +550,130 @@ describe("TickerResearchPane", () => {
531
550
  expect(detailHarnessState?.paneState[TEST_PANE_ID]?.activeTabId).toBe("financials");
532
551
  });
533
552
 
553
+ test("shows metric tabs for fundamental graph charts and defaults them to quarterly data", async () => {
554
+ setSharedRegistryForTests(makeRegistry());
555
+ setOptionsProvider(createProvider(false));
556
+
557
+ testSetup = await testRender(
558
+ <DetailHarness
559
+ config={createDetailConfig("AAPL")}
560
+ ticker={makeTicker("AAPL")}
561
+ financials={makeFinancials({
562
+ annualStatements: [{ date: "2024-12-31", totalRevenue: 1_000 }],
563
+ quarterlyStatements: [
564
+ { date: "2025-03-31", totalRevenue: 200 },
565
+ { date: "2025-06-30", totalRevenue: 250 },
566
+ ],
567
+ })}
568
+ activeTabId="fundamental-graphs"
569
+ height={28}
570
+ />,
571
+ { width: 90, height: 28 },
572
+ );
573
+
574
+ await flushFrame();
575
+ const frame = testSetup.captureCharFrame();
576
+ expect(frame).toContain("Revenue");
577
+ expect(frame).toContain("Gross Profit");
578
+ expect(frame).not.toContain("Revenue (quarterly)");
579
+ expect(frame).not.toContain("[g]roup");
580
+ expect(frame).toContain("2025-03-31");
581
+ expect(frame).toContain("2025-06-30");
582
+ expect(frame.indexOf("2025-06-30")).toBeLessThan(frame.indexOf("2025-03-31"));
583
+ });
584
+
585
+ test("captures arrows for graph metric tabs between enter and escape", async () => {
586
+ setSharedRegistryForTests(makeRegistry());
587
+ setOptionsProvider(createProvider(false));
588
+
589
+ testSetup = await testRender(
590
+ <DetailHarness
591
+ config={createDetailConfig("AAPL")}
592
+ ticker={makeTicker("AAPL")}
593
+ financials={makeFinancials({
594
+ annualStatements: [{ date: "2024-12-31", totalRevenue: 1_000, grossProfit: 400 }],
595
+ quarterlyStatements: [
596
+ { date: "2025-03-31", totalRevenue: 200, grossProfit: 80 },
597
+ { date: "2025-06-30", totalRevenue: 250, grossProfit: 120 },
598
+ ],
599
+ })}
600
+ activeTabId="fundamental-graphs"
601
+ height={28}
602
+ />,
603
+ { width: 90, height: 28 },
604
+ );
605
+
606
+ await flushFrame();
607
+ expect(detailHarnessState?.paneState[TEST_PANE_ID]?.activeTabId).toBe("fundamental-graphs");
608
+
609
+ await act(async () => {
610
+ testSetup!.mockInput.pressEnter();
611
+ await testSetup!.renderOnce();
612
+ });
613
+ await flushFrame();
614
+
615
+ await act(async () => {
616
+ testSetup!.mockInput.pressArrow("right");
617
+ await testSetup!.renderOnce();
618
+ });
619
+ await flushFrame();
620
+
621
+ expect(detailHarnessState?.paneState[TEST_PANE_ID]?.pluginState?.["ticker-detail"]?.detailMetric).toBe("grossProfit");
622
+ expect(detailHarnessState?.paneState[TEST_PANE_ID]?.activeTabId).toBe("fundamental-graphs");
623
+
624
+ await emitKeypress({ name: "escape", sequence: "\u001b" });
625
+ expect(detailHarnessState?.inputCaptured).toBe(false);
626
+
627
+ await act(async () => {
628
+ testSetup!.mockInput.pressArrow("right");
629
+ await testSetup!.renderOnce();
630
+ });
631
+ await flushFrame();
632
+
633
+ await settleTickerTabCommit();
634
+ expect(detailHarnessState?.paneState[TEST_PANE_ID]?.activeTabId).toBe("chart");
635
+ });
636
+
637
+ test("clicking graph content focuses metric tabs for arrow navigation", async () => {
638
+ setSharedRegistryForTests(makeRegistry());
639
+ setOptionsProvider(createProvider(false));
640
+
641
+ testSetup = await testRender(
642
+ <DetailHarness
643
+ config={createDetailConfig("AAPL")}
644
+ ticker={makeTicker("AAPL")}
645
+ financials={makeFinancials({
646
+ annualStatements: [{ date: "2024-12-31", totalRevenue: 1_000, grossProfit: 400 }],
647
+ quarterlyStatements: [
648
+ { date: "2025-03-31", totalRevenue: 200, grossProfit: 80 },
649
+ { date: "2025-06-30", totalRevenue: 250, grossProfit: 120 },
650
+ ],
651
+ })}
652
+ activeTabId="fundamental-graphs"
653
+ height={28}
654
+ />,
655
+ { width: 90, height: 28 },
656
+ );
657
+
658
+ await flushFrame();
659
+
660
+ await act(async () => {
661
+ await testSetup!.mockMouse.click(24, 6);
662
+ await testSetup!.renderOnce();
663
+ });
664
+ await flushFrame();
665
+ expect(detailHarnessState?.inputCaptured).toBe(true);
666
+
667
+ await act(async () => {
668
+ testSetup!.mockInput.pressArrow("right");
669
+ await testSetup!.renderOnce();
670
+ });
671
+ await flushFrame();
672
+
673
+ expect(detailHarnessState?.paneState[TEST_PANE_ID]?.pluginState?.["ticker-detail"]?.detailMetric).toBe("grossProfit");
674
+ expect(detailHarnessState?.paneState[TEST_PANE_ID]?.activeTabId).toBe("fundamental-graphs");
675
+ });
676
+
534
677
  test("shows Trade when an IBKR gateway profile exists", async () => {
535
678
  setSharedRegistryForTests(makeRegistry());
536
679
  setOptionsProvider(createProvider(false));
@@ -12,6 +12,7 @@ import { layoutManagerPlugin } from "./builtin/layout-manager";
12
12
  import { predictionMarketsPlugin } from "./prediction-markets";
13
13
  import { analyticsPlugin } from "./builtin/analytics";
14
14
  import { alertsPlugin } from "./builtin/alerts";
15
+ import { kellySizerPlugin } from "./builtin/kelly-sizer";
15
16
  import {
16
17
  brokerPlugin,
17
18
  macroPlugin,
@@ -36,6 +37,7 @@ export const uiBuiltinPlugins: GloomPlugin[] = [
36
37
  marketOverviewPlugin,
37
38
  macroPlugin,
38
39
  analyticsPlugin,
40
+ kellySizerPlugin,
39
41
  alertsPlugin,
40
42
  ];
41
43
 
@@ -40,6 +40,25 @@ export const ibkrBroker: BrokerAdapter = {
40
40
  return importFlexPositions(normalized.flex);
41
41
  },
42
42
 
43
+ async importPortfolioSnapshot(instance) {
44
+ const normalized = normalizeIbkrConfig(instance.config);
45
+ if (normalized.connectionMode === "gateway") {
46
+ const service = ibkrGatewayManager.getService(instance.id);
47
+ await refreshGatewayData(instance);
48
+ const [accounts, positions] = await Promise.all([
49
+ service.getAccounts(normalized.gateway),
50
+ service.getPositions(normalized.gateway),
51
+ ]);
52
+ return { accounts, positions };
53
+ }
54
+
55
+ const xml = await loadFlexStatement(normalized.flex);
56
+ return {
57
+ accounts: parseFlexAccounts(xml),
58
+ positions: parseFlexPositions(xml),
59
+ };
60
+ },
61
+
43
62
  async connect(instance) {
44
63
  const normalized = normalizeIbkrConfig(instance.config);
45
64
  if (normalized.connectionMode !== "gateway") return;
@@ -1,5 +1,10 @@
1
1
  import { describe, expect, test } from "bun:test";
2
- import { buildIbkrConfigFromValues, normalizeIbkrConfig } from "./config";
2
+ import {
3
+ buildIbkrConfigFromValues,
4
+ IBKR_STATEMENT_URL,
5
+ LEGACY_IBKR_STATEMENT_URL,
6
+ normalizeIbkrConfig,
7
+ } from "./config";
3
8
 
4
9
  describe("ibkr config helpers", () => {
5
10
  test("builds nested manual gateway config from flat wizard values", () => {
@@ -45,6 +50,30 @@ describe("ibkr config helpers", () => {
45
50
  expect(config.gatewaySetupMode).toBe("auto");
46
51
  expect(config.flex.token).toBe("abc");
47
52
  expect(config.flex.queryId).toBe("123");
48
- expect(config.flex.endpoint).toContain("FlexStatementService.SendRequest");
53
+ expect(config.flex.endpoint).toBe(IBKR_STATEMENT_URL);
54
+ });
55
+
56
+ test("migrates the legacy default Flex endpoint to the current IBKR endpoint", () => {
57
+ const config = normalizeIbkrConfig({
58
+ flex: {
59
+ token: "abc",
60
+ queryId: "123",
61
+ endpoint: LEGACY_IBKR_STATEMENT_URL,
62
+ },
63
+ });
64
+
65
+ expect(config.flex.endpoint).toBe(IBKR_STATEMENT_URL);
66
+ });
67
+
68
+ test("preserves custom non-default Flex endpoints", () => {
69
+ const config = normalizeIbkrConfig({
70
+ flex: {
71
+ token: "abc",
72
+ queryId: "123",
73
+ endpoint: "https://example.com/custom/SendRequest",
74
+ },
75
+ });
76
+
77
+ expect(config.flex.endpoint).toBe("https://example.com/custom/SendRequest");
49
78
  });
50
79
  });
@@ -1,7 +1,8 @@
1
1
  import type { BrokerConfigField } from "../../types/broker";
2
2
  import type { IbkrGatewayConfig, ResolvedIbkrGatewayConnection } from "./gateway/service";
3
3
 
4
- export const IBKR_STATEMENT_URL = "https://gdcdyn.interactivebrokers.com/Universal/servlet/FlexStatementService.SendRequest";
4
+ export const LEGACY_IBKR_STATEMENT_URL = "https://gdcdyn.interactivebrokers.com/Universal/servlet/FlexStatementService.SendRequest";
5
+ export const IBKR_STATEMENT_URL = "https://ndcdyn.interactivebrokers.com/AccountManagement/FlexWebService/SendRequest";
5
6
 
6
7
  type IbkrConnectionMode = "flex" | "gateway";
7
8
  type IbkrGatewaySetupMode = "auto" | "manual";
@@ -119,6 +120,12 @@ function coerceString(value: unknown, fallback = ""): string {
119
120
  return typeof value === "string" ? value : fallback;
120
121
  }
121
122
 
123
+ function normalizeFlexEndpoint(value: unknown): string {
124
+ const endpoint = coerceString(value, DEFAULT_FLEX_CONFIG.endpoint).trim();
125
+ if (!endpoint || endpoint === LEGACY_IBKR_STATEMENT_URL) return IBKR_STATEMENT_URL;
126
+ return endpoint;
127
+ }
128
+
122
129
  function getMode(value: unknown): IbkrConnectionMode {
123
130
  return value === "gateway" ? "gateway" : "flex";
124
131
  }
@@ -166,7 +173,7 @@ export function normalizeIbkrConfig(raw?: Record<string, unknown>): IbkrConfig {
166
173
  flex: {
167
174
  token: coerceString(nestedFlex.token ?? input.token),
168
175
  queryId: coerceString(nestedFlex.queryId ?? input.queryId),
169
- endpoint: coerceString(nestedFlex.endpoint ?? input.endpoint, DEFAULT_FLEX_CONFIG.endpoint),
176
+ endpoint: normalizeFlexEndpoint(nestedFlex.endpoint ?? input.endpoint),
170
177
  },
171
178
  gateway: {
172
179
  host,
@@ -59,6 +59,10 @@ describe("parseFlexAccounts", () => {
59
59
  <FxPosition accountId="DU12345" assetCategory="CASH" functionalCurrency="USD" fxCurrency="USD" quantity="-303029.144938754" value="-303029.144938754" />
60
60
  <FxPosition accountId="DU12345" assetCategory="CASH" functionalCurrency="USD" fxCurrency="EUR" quantity="-351957.025" value="-405102.535775" />
61
61
  </FxPositions>
62
+ <OpenPositions>
63
+ <OpenPosition accountId="DU12345" symbol="AAPL" assetCategory="STK" position="100" currency="USD" positionValue="100000" fxRateToBase="1" />
64
+ <OpenPosition accountId="DU12345" symbol="ASML" assetCategory="STK" position="10" currency="EUR" positionValue="50000" fxRateToBase="1.2" />
65
+ </OpenPositions>
62
66
  </FlexStatement>
63
67
  </FlexStatements>
64
68
  </FlexQueryResponse>
@@ -71,7 +75,9 @@ describe("parseFlexAccounts", () => {
71
75
  name: "Main",
72
76
  currency: "USD",
73
77
  source: "flex",
78
+ asOfDate: "2026-03-27",
74
79
  netLiquidation: 764713.626876249,
80
+ grossPositionValue: 160000,
75
81
  totalCashValue: -1050953.720462251,
76
82
  settledCash: -917604.448220862,
77
83
  cashBalances: [
@@ -110,7 +116,9 @@ describe("parseFlexAccounts", () => {
110
116
  currency: "USD",
111
117
  source: "flex",
112
118
  updatedAt: new Date(2026, 2, 27).getTime(),
119
+ asOfDate: "2026-03-27",
113
120
  netLiquidation: 12345.67,
121
+ grossPositionValue: undefined,
114
122
  totalCashValue: undefined,
115
123
  settledCash: undefined,
116
124
  cashBalances: undefined,
@@ -190,9 +198,9 @@ describe("parseFlexPortfolioPerformance", () => {
190
198
 
191
199
  describe("requestFlexStatement", () => {
192
200
  test("uses the configured HTTP transport for statement requests", async () => {
193
- const requests: string[] = [];
194
- setHttpFetchTransport(async (url) => {
195
- requests.push(url);
201
+ const requests: Array<{ url: string; init: RequestInit | undefined }> = [];
202
+ setHttpFetchTransport(async (url, init) => {
203
+ requests.push({ url, init });
196
204
  return new Response(
197
205
  "<FlexStatementResponse><ReferenceCode>987654</ReferenceCode></FlexStatementResponse>",
198
206
  { status: 200 },
@@ -202,16 +210,17 @@ describe("requestFlexStatement", () => {
202
210
  await expect(requestFlexStatement({
203
211
  token: "secret-flex-token",
204
212
  queryId: "12345",
205
- endpoint: "https://gdcdyn.interactivebrokers.com/Universal/servlet/FlexStatementService.SendRequest",
213
+ endpoint: "https://ndcdyn.interactivebrokers.com/AccountManagement/FlexWebService/SendRequest",
206
214
  })).resolves.toBe("987654");
207
215
 
208
216
  expect(requests).toHaveLength(1);
209
- expect(requests[0]).toContain("q=12345");
217
+ expect(requests[0]?.url).toContain("q=12345");
218
+ expect((requests[0]?.init?.headers as Record<string, string> | undefined)?.["User-Agent"]).toBeTruthy();
210
219
  });
211
220
 
212
221
  test("adds request context to vague IBKR Flex errors without exposing the token", async () => {
213
222
  globalThis.fetch = (async () => new Response(
214
- "<FlexStatementResponse><ErrorMessage>Load failed</ErrorMessage></FlexStatementResponse>",
223
+ "<FlexStatementResponse><ErrorCode>1001</ErrorCode><ErrorMessage>Load failed</ErrorMessage></FlexStatementResponse>",
215
224
  { status: 200 },
216
225
  )) as unknown as typeof fetch;
217
226
 
@@ -220,14 +229,14 @@ describe("requestFlexStatement", () => {
220
229
  await requestFlexStatement({
221
230
  token: "secret-flex-token",
222
231
  queryId: "12345",
223
- endpoint: "https://gdcdyn.interactivebrokers.com/Universal/servlet/FlexStatementService.SendRequest",
232
+ endpoint: "https://ndcdyn.interactivebrokers.com/AccountManagement/FlexWebService/SendRequest",
224
233
  });
225
234
  } catch (error) {
226
235
  message = error instanceof Error ? error.message : String(error);
227
236
  }
228
237
 
229
- expect(message).toContain("IBKR Flex request failed while requesting the statement: Load failed.");
230
- expect(message).toContain("Endpoint FlexStatementService.SendRequest");
238
+ expect(message).toContain("IBKR Flex request failed while requesting the statement: IBKR error 1001: Load failed.");
239
+ expect(message).toContain("Endpoint SendRequest");
231
240
  expect(message).toContain("query ID 12345");
232
241
  expect(message).toContain("token configured");
233
242
  expect(message).toContain("Flex Web Service is enabled");
@@ -63,6 +63,34 @@ function parseFlexDate(raw: string | undefined): string | null {
63
63
  return new Date(parsed).toISOString().slice(0, 10);
64
64
  }
65
65
 
66
+ function parseFlexGrossPositionValue(
67
+ body: string,
68
+ statementAccountId: string,
69
+ baseCurrency: string | undefined,
70
+ ): number | undefined {
71
+ let total = 0;
72
+ let hasValue = false;
73
+
74
+ for (const entry of body.matchAll(/<OpenPosition\b([^>]*)\/?>/g)) {
75
+ const attributes = parseAttributes(entry[1] ?? "");
76
+ const accountId = attributes.accountId || statementAccountId;
77
+ if (!accountId || (statementAccountId && accountId !== statementAccountId)) continue;
78
+
79
+ const marketValue = parseNumber(attributes.positionValue);
80
+ if (marketValue == null) continue;
81
+
82
+ const positionCurrency = attributes.currency || baseCurrency;
83
+ const fxRateToBase = parseNumber(attributes.fxRateToBase)
84
+ ?? (baseCurrency && positionCurrency === baseCurrency ? 1 : undefined);
85
+ if (fxRateToBase == null) continue;
86
+
87
+ total += Math.abs(marketValue * fxRateToBase);
88
+ hasValue = true;
89
+ }
90
+
91
+ return hasValue ? total : undefined;
92
+ }
93
+
66
94
  function buildContractRef(attributes: Record<string, string>, symbol: string, assetCategory: string): BrokerContractRef | undefined {
67
95
  const conIdRaw = attributes.conid || attributes.conId;
68
96
  const strikeRaw = attributes.strike;
@@ -211,7 +239,9 @@ export function parseFlexAccounts(xml: string): BrokerAccount[] {
211
239
  currency: accountCurrency,
212
240
  source: "flex",
213
241
  updatedAt: parseFlexTimestamp(statementAttrs.whenGenerated, statementAttrs.toDate || statementAttrs.fromDate),
242
+ asOfDate: parseFlexDate(statementAttrs.toDate || statementAttrs.fromDate) ?? undefined,
214
243
  netLiquidation: parseNumber(changeInNavAttrs?.endingValue),
244
+ grossPositionValue: parseFlexGrossPositionValue(body, accountId, accountCurrency),
215
245
  totalCashValue: parseNumber(baseCashAttrs?.endingCash),
216
246
  settledCash: parseNumber(baseCashAttrs?.endingSettledCash),
217
247
  cashBalances: cashBalances.length > 0 ? cashBalances : undefined,
@@ -2,7 +2,8 @@ import { httpFetch } from "../../../utils/http-transport";
2
2
  import type { FlexQueryConfig } from "../config";
3
3
  import { IBKR_STATEMENT_URL } from "../config";
4
4
 
5
- const IBKR_STATEMENT_GET_URL = "https://gdcdyn.interactivebrokers.com/Universal/servlet/FlexStatementService.GetStatement";
5
+ const IBKR_STATEMENT_GET_URL = "https://ndcdyn.interactivebrokers.com/AccountManagement/FlexWebService/GetStatement";
6
+ const FLEX_USER_AGENT = "Gloomberb/1.0 Flex";
6
7
  const FLEX_STATEMENT_CACHE_MS = 15 * 60_000;
7
8
  const FLEX_STATEMENT_INITIAL_WAIT_MS = 3_000;
8
9
  const FLEX_STATEMENT_POLL_DELAY_MS = 5_000;
@@ -29,10 +30,54 @@ function decodeXmlText(value: string): string {
29
30
  .replace(/&apos;/g, "'");
30
31
  }
31
32
 
33
+ function extractXmlTag(text: string, tagName: string): string | null {
34
+ const match = text.match(new RegExp(`<${tagName}>\\s*([^<]*?)\\s*<\\/${tagName}>`, "i"));
35
+ return match?.[1] ? decodeXmlText(match[1].trim()) : null;
36
+ }
37
+
32
38
  function extractFlexErrorMessage(text: string): string | null {
33
- const errorMatch = text.match(/<ErrorMessage>([^<]+)<\/ErrorMessage>/);
34
- if (errorMatch?.[1]) return decodeXmlText(errorMatch[1].trim());
35
- return null;
39
+ const message = extractXmlTag(text, "ErrorMessage");
40
+ const code = extractXmlTag(text, "ErrorCode");
41
+ if (message && code) return `IBKR error ${code}: ${message}`;
42
+ return message;
43
+ }
44
+
45
+ function buildFlexUrl(endpoint: string, params: Record<string, string>): string {
46
+ const url = new URL(endpoint);
47
+ for (const [key, value] of Object.entries(params)) {
48
+ url.searchParams.set(key, value);
49
+ }
50
+ return url.toString();
51
+ }
52
+
53
+ function resolveFlexGetEndpoint(sendEndpoint: string | undefined): string {
54
+ if (!sendEndpoint || sendEndpoint === IBKR_STATEMENT_URL) return IBKR_STATEMENT_GET_URL;
55
+
56
+ try {
57
+ const url = new URL(sendEndpoint);
58
+ url.search = "";
59
+ if (url.pathname.endsWith("/SendRequest")) {
60
+ url.pathname = url.pathname.replace(/\/SendRequest$/, "/GetStatement");
61
+ return url.toString();
62
+ }
63
+ if (url.pathname.endsWith("FlexStatementService.SendRequest")) {
64
+ url.pathname = url.pathname.replace(/FlexStatementService\.SendRequest$/, "FlexStatementService.GetStatement");
65
+ return url.toString();
66
+ }
67
+ } catch {
68
+ return IBKR_STATEMENT_GET_URL;
69
+ }
70
+
71
+ return IBKR_STATEMENT_GET_URL;
72
+ }
73
+
74
+ function flexRequestInit(): RequestInit {
75
+ return {
76
+ headers: {
77
+ "User-Agent": FLEX_USER_AGENT,
78
+ },
79
+ signal: AbortSignal.timeout(30_000),
80
+ };
36
81
  }
37
82
 
38
83
  function endpointName(endpoint: string | undefined): string {
@@ -69,11 +114,15 @@ function flexError(providerMessage: string, context: FlexErrorContext): Error {
69
114
 
70
115
  export async function requestFlexStatement(config: FlexQueryConfig): Promise<string> {
71
116
  const endpoint = config.endpoint || IBKR_STATEMENT_URL;
72
- const url = `${endpoint}?t=${config.token}&q=${config.queryId}&v=3`;
117
+ const url = buildFlexUrl(endpoint, {
118
+ t: config.token,
119
+ q: config.queryId,
120
+ v: "3",
121
+ });
73
122
  let resp: Response;
74
123
  let text: string;
75
124
  try {
76
- resp = await httpFetch(url, { signal: AbortSignal.timeout(30_000) });
125
+ resp = await httpFetch(url, flexRequestInit());
77
126
  text = await resp.text();
78
127
  } catch (error) {
79
128
  throw flexError(error instanceof Error ? error.message : "Network request failed", {
@@ -84,8 +133,8 @@ export async function requestFlexStatement(config: FlexQueryConfig): Promise<str
84
133
  });
85
134
  }
86
135
 
87
- const codeMatch = text.match(/<ReferenceCode>(\d+)<\/ReferenceCode>/);
88
- if (!codeMatch) {
136
+ const referenceCode = extractXmlTag(text, "ReferenceCode");
137
+ if (!referenceCode) {
89
138
  throw flexError(extractFlexErrorMessage(text) || "No reference code returned", {
90
139
  phase: "request",
91
140
  endpoint,
@@ -95,7 +144,7 @@ export async function requestFlexStatement(config: FlexQueryConfig): Promise<str
95
144
  });
96
145
  }
97
146
 
98
- return codeMatch[1]!;
147
+ return referenceCode;
99
148
  }
100
149
 
101
150
  async function getFlexStatement(
@@ -105,17 +154,22 @@ async function getFlexStatement(
105
154
  ): Promise<string> {
106
155
  await new Promise((resolve) => setTimeout(resolve, FLEX_STATEMENT_INITIAL_WAIT_MS));
107
156
 
108
- const url = `${IBKR_STATEMENT_GET_URL}?t=${token}&q=${referenceCode}&v=3`;
157
+ const endpoint = resolveFlexGetEndpoint(context.endpoint);
158
+ const url = buildFlexUrl(endpoint, {
159
+ t: token,
160
+ q: referenceCode,
161
+ v: "3",
162
+ });
109
163
  for (let attempt = 0; attempt < FLEX_STATEMENT_MAX_DOWNLOAD_ATTEMPTS; attempt += 1) {
110
164
  let resp: Response;
111
165
  let text: string;
112
166
  try {
113
- resp = await httpFetch(url, { signal: AbortSignal.timeout(30_000) });
167
+ resp = await httpFetch(url, flexRequestInit());
114
168
  text = await resp.text();
115
169
  } catch (error) {
116
170
  throw flexError(error instanceof Error ? error.message : "Network request failed", {
117
171
  phase: "download",
118
- endpoint: context.endpoint,
172
+ endpoint,
119
173
  queryId: context.queryId,
120
174
  token,
121
175
  referenceCode,
@@ -135,7 +189,7 @@ async function getFlexStatement(
135
189
  if (providerMessage) {
136
190
  throw flexError(providerMessage, {
137
191
  phase: "download",
138
- endpoint: context.endpoint,
192
+ endpoint,
139
193
  queryId: context.queryId,
140
194
  token,
141
195
  referenceCode,
@@ -146,7 +200,7 @@ async function getFlexStatement(
146
200
 
147
201
  throw flexError("statement generation timed out", {
148
202
  phase: "download",
149
- endpoint: context.endpoint,
203
+ endpoint,
150
204
  queryId: context.queryId,
151
205
  token,
152
206
  referenceCode,
@@ -96,7 +96,7 @@ export async function loadIbkrAccounts({
96
96
  const result = await firstValueFrom(
97
97
  api.getAccountSummary(
98
98
  "All",
99
- "NetLiquidation,TotalCashValue,SettledCash,AvailableFunds,BuyingPower,ExcessLiquidity,InitMarginReq,MaintMarginReq,$LEDGER:ALL",
99
+ "NetLiquidation,GrossPositionValue,TotalCashValue,SettledCash,AvailableFunds,BuyingPower,ExcessLiquidity,InitMarginReq,MaintMarginReq,$LEDGER:ALL",
100
100
  )
101
101
  .pipe(take(1), timeout(10_000)),
102
102
  );