gloomberb 0.8.0 → 0.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +102 -52
- package/package.json +1 -1
- package/src/api-client/chat.ts +8 -0
- package/src/api-client/index.test.ts +30 -0
- package/src/api-client/index.ts +4 -0
- package/src/api-client/normalizers.ts +1 -0
- package/src/api-client/types.ts +1 -0
- package/src/capabilities/factories.ts +4 -0
- package/src/components/chart/comparison/data.test.ts +54 -0
- package/src/components/chart/comparison/data.ts +47 -24
- package/src/components/chart/core/projection.ts +51 -27
- package/src/components/data-table/stack-view.test.tsx +14 -11
- package/src/components/data-table/stack-view.tsx +1 -2
- package/src/components/data-table/view.test.tsx +24 -18
- package/src/components/data-table/view.tsx +313 -83
- package/src/components/feed-data-table/stack-view.tsx +6 -8
- package/src/components/layout/pane/footer/index.test.tsx +6 -0
- package/src/components/layout/pane/footer/index.tsx +14 -2
- package/src/components/layout/pane/title.ts +5 -1
- package/src/components/pane-settings-dialog/field-dialogs.tsx +0 -2
- package/src/components/pane-settings-dialog/tui.tsx +1 -1
- package/src/components/ticker/list-table-view.tsx +11 -25
- package/src/components/ui/choice-dialog.tsx +12 -2
- package/src/components/ui/multi-select/dialog.tsx +17 -5
- package/src/components/ui/shortcut-hint.tsx +14 -2
- package/src/components/ui/table-layout.test.ts +2 -2
- package/src/components/ui/table-layout.ts +8 -2
- package/src/components/ui/tabs.tsx +20 -6
- package/src/components/ui/ui.test.tsx +30 -5
- package/src/market-data/coordinator/auxiliary.ts +29 -1
- package/src/market-data/coordinator/index.ts +17 -1
- package/src/market-data/hooks.tsx +17 -1
- package/src/market-data/selectors.ts +4 -0
- package/src/plugins/builtin/account-management/form-components.tsx +0 -4
- package/src/plugins/builtin/account-management/model.test.ts +54 -0
- package/src/plugins/builtin/account-management/model.ts +13 -3
- package/src/plugins/builtin/account-management/pane.tsx +5 -11
- package/src/plugins/builtin/account-management/password-dialog.tsx +1 -1
- package/src/plugins/builtin/alerts/pane.tsx +5 -3
- package/src/plugins/builtin/analytics/index.tsx +0 -3
- package/src/plugins/builtin/analytics/view.tsx +6 -5
- package/src/plugins/builtin/broker-manager/index.tsx +6 -6
- package/src/plugins/builtin/buildout/pane/index.tsx +6 -6
- package/src/plugins/builtin/changelog/index.tsx +40 -27
- package/src/plugins/builtin/changelog/model.test.ts +77 -0
- package/src/plugins/builtin/changelog/model.ts +98 -0
- package/src/plugins/builtin/chat/channel-labels.ts +53 -0
- package/src/plugins/builtin/chat/channels.test.ts +28 -0
- package/src/plugins/builtin/chat/channels.ts +36 -19
- package/src/plugins/builtin/chat/chat.test.tsx +264 -5
- package/src/plugins/builtin/chat/content/channel-navigation.ts +6 -0
- package/src/plugins/builtin/chat/content/composer-runtime.test.tsx +144 -0
- package/src/plugins/builtin/chat/content/composer-runtime.ts +151 -20
- package/src/plugins/builtin/chat/content/composer.tsx +23 -2
- package/src/plugins/builtin/chat/content/index.tsx +61 -0
- package/src/plugins/builtin/chat/content/layout-metrics.ts +3 -1
- package/src/plugins/builtin/chat/content/shortcuts.ts +20 -1
- package/src/plugins/builtin/chat/content/transcript.tsx +8 -0
- package/src/plugins/builtin/chat/content/types.ts +1 -0
- package/src/plugins/builtin/chat/controller/index.ts +41 -0
- package/src/plugins/builtin/chat/controller/messages.ts +18 -0
- package/src/plugins/builtin/chat/controller/notifications.ts +10 -4
- package/src/plugins/builtin/chat/controller/utils.ts +6 -3
- package/src/plugins/builtin/chat/controller/view.ts +1 -1
- package/src/plugins/builtin/chat/controller.test.ts +189 -0
- package/src/plugins/builtin/chat/edit-window.ts +24 -0
- package/src/plugins/builtin/chat/layout.ts +94 -31
- package/src/plugins/builtin/chat/message/action-chip.tsx +1 -0
- package/src/plugins/builtin/chat/message/desktop.tsx +53 -20
- package/src/plugins/builtin/chat/message/inline-tokens.tsx +16 -10
- package/src/plugins/builtin/chat/message/render-state.ts +5 -1
- package/src/plugins/builtin/chat/message/terminal.tsx +49 -20
- package/src/plugins/builtin/chat/message/types.ts +2 -0
- package/src/plugins/builtin/chat/pane.tsx +35 -7
- package/src/plugins/builtin/chat/sidebar.test.tsx +1 -0
- package/src/plugins/builtin/chat/status-widget.tsx +29 -11
- package/src/plugins/builtin/chat/test-harness.tsx +2 -0
- package/src/plugins/builtin/cloud/plugin.tsx +3 -0
- package/src/plugins/builtin/cloud-tweets/table.tsx +7 -11
- package/src/plugins/builtin/congress-trades/pane.tsx +14 -22
- package/src/plugins/builtin/earnings/index.tsx +8 -7
- package/src/plugins/builtin/econ/index.tsx +6 -8
- package/src/plugins/builtin/help/index.tsx +6 -0
- package/src/plugins/builtin/holders/pane.tsx +7 -6
- package/src/plugins/builtin/market-movers/index.tsx +6 -3
- package/src/plugins/builtin/news/wire/news/table.tsx +7 -6
- package/src/plugins/builtin/options/view.tsx +16 -8
- package/src/plugins/builtin/research/analyst-pane.tsx +1 -2
- package/src/plugins/builtin/research/corporate-actions-pane.tsx +358 -7
- package/src/plugins/builtin/research/index.test.ts +30 -1
- package/src/plugins/builtin/research/relative-valuation-pane.tsx +6 -8
- package/src/plugins/builtin/sec/filing-documents.ts +66 -0
- package/src/plugins/builtin/sec/index.tsx +132 -20
- package/src/plugins/builtin/sectors/index.tsx +13 -23
- package/src/plugins/builtin/shared/ticker-request.ts +1 -0
- package/src/plugins/builtin/thirteenf/pane.tsx +26 -25
- package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/content.tsx +5 -4
- package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +5 -4
- package/src/plugins/builtin/ticker-detail/data-panes/provider-search.tsx +6 -5
- package/src/plugins/builtin/ticker-detail/financials/tab.tsx +10 -8
- package/src/plugins/builtin/world-indices/index.tsx +9 -6
- package/src/plugins/prediction-markets/pane.tsx +10 -9
- package/src/plugins/registry/index.ts +3 -0
- package/src/plugins/runtime/context.tsx +1 -0
- package/src/plugins/runtime/index.tsx +4 -0
- package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +1 -0
- package/src/renderers/electrobun/view/data-table/index.tsx +3 -2
- package/src/renderers/electrobun/view/data-table/row.tsx +2 -2
- package/src/renderers/electrobun/view/host/input.tsx +23 -2
- package/src/renderers/electrobun/view/remote/asset-data-client.ts +1 -0
- package/src/sources/gloomberb-cloud/index.ts +4 -0
- package/src/sources/provider-router/cache.ts +1 -0
- package/src/sources/provider-router/document-routes.ts +45 -0
- package/src/sources/provider-router/index.ts +4 -0
- package/src/sources/sec-edgar/content.ts +9 -3
- package/src/sources/sec-edgar.test.ts +108 -1
- package/src/sources/sec-edgar.ts +105 -1
- package/src/sources/yahoo-finance.ts +4 -0
- package/src/test-support/plugin-runtime.ts +1 -0
- package/src/types/data-provider.ts +11 -0
- package/src/ui/host.tsx +1 -0
- package/src/version.ts +1 -1
package/README.md
CHANGED
|
@@ -17,57 +17,6 @@ Available as a desktop app or TUI.
|
|
|
17
17
|
|
|
18
18
|
</div>
|
|
19
19
|
|
|
20
|
-
## Functions
|
|
21
|
-
|
|
22
|
-
Open the command bar with `Ctrl+P` or `` ` ``, then type a shortcut or command name.
|
|
23
|
-
|
|
24
|
-
| Shortcut | Function |
|
|
25
|
-
|----------|----------|
|
|
26
|
-
| `DES <ticker>` | Security details for a ticker |
|
|
27
|
-
| `FA <ticker>` | Financial statement view |
|
|
28
|
-
| `GP <ticker>` | Price chart |
|
|
29
|
-
| `GIP <ticker>` | Intraday price chart |
|
|
30
|
-
| `HP <ticker>` | Historical OHLCV prices |
|
|
31
|
-
| `GF <tickers>` | Fundamental statement graph |
|
|
32
|
-
| `GE <tickers>` | Valuation multiple graph |
|
|
33
|
-
| `GR <tickers>` | Security relationship graph |
|
|
34
|
-
| `EE <ticker>` | Events view with earnings and revenue estimates |
|
|
35
|
-
| `EM [tickers]` | Earnings monitor |
|
|
36
|
-
| `SRCH <query>` | Provider symbol search |
|
|
37
|
-
| `QQ <tickers>` | Ticker quote monitor |
|
|
38
|
-
| `PM <query>` | Polymarket and Kalshi prediction data |
|
|
39
|
-
| `TOP` | Ranked market stories |
|
|
40
|
-
| `MOST` | Top gainers, losers, most active, and trending tickers |
|
|
41
|
-
| `WEI` | Global equity indices |
|
|
42
|
-
| `ECON` | Economic events and releases |
|
|
43
|
-
| `CMP <tickers>` | Ticker charts |
|
|
44
|
-
| `CORR <tickers>` | Ticker return correlations |
|
|
45
|
-
| `ANR <ticker>` | Analyst targets and ratings |
|
|
46
|
-
| `SEC <ticker>` | SEC filings and company disclosures |
|
|
47
|
-
| `TWIT <query>` | Ticker-related market posts |
|
|
48
|
-
| `OMON <ticker>` | Options monitor |
|
|
49
|
-
| `PORT` | Portfolio risk and sector exposure |
|
|
50
|
-
| `BI` / `SP` | S&P 500 sector performance |
|
|
51
|
-
| `FXC` | Major FX cross rates |
|
|
52
|
-
| `FNG` | Fear and greed market gauge |
|
|
53
|
-
| `ALRT` | Price alerts |
|
|
54
|
-
| `CHAT` | Gloom Cloud chat |
|
|
55
|
-
| `PF` | Portfolio and watchlist workspace |
|
|
56
|
-
| `N` | News feed |
|
|
57
|
-
| `CN <ticker>` | Ticker news |
|
|
58
|
-
| `NI` | Sector news |
|
|
59
|
-
| `FIRST` | Breaking news |
|
|
60
|
-
| `NOTE` | Notes |
|
|
61
|
-
| `AI <prompt>` | AI screener |
|
|
62
|
-
| `GC` | Yield curve |
|
|
63
|
-
| `ERN` | Earnings calendar |
|
|
64
|
-
| `HDS <ticker>` | Institutional holders |
|
|
65
|
-
| `INS <ticker>` | Insider activity |
|
|
66
|
-
| `EVT <ticker>` | Corporate actions, earnings, and estimates |
|
|
67
|
-
| `RV <tickers>` | Relative valuation |
|
|
68
|
-
| `IBKR` | IBKR trading pane |
|
|
69
|
-
| `BR` | Broker connections |
|
|
70
|
-
|
|
71
20
|
## Install
|
|
72
21
|
|
|
73
22
|
macOS desktop app + terminal command:
|
|
@@ -100,6 +49,25 @@ On macOS, app updates replace the app bundle in place and keep the terminal comm
|
|
|
100
49
|
|
|
101
50
|
For the best terminal experience, use a [Kitty](https://sw.kovidgoyal.net/kitty/)-compatible terminal such as Ghostty, Kitty, or WezTerm.
|
|
102
51
|
|
|
52
|
+
## Start
|
|
53
|
+
|
|
54
|
+
Open the command bar with `Ctrl+P` or `` ` ``, then type a command.
|
|
55
|
+
|
|
56
|
+
| Try | Opens |
|
|
57
|
+
|-----|-------|
|
|
58
|
+
| `DES AAPL` | Security details |
|
|
59
|
+
| `GP NVDA` | Price chart |
|
|
60
|
+
| `TOP` | Ranked market stories |
|
|
61
|
+
| `MOST` | Market movers |
|
|
62
|
+
| `PF` | Portfolios and watchlists |
|
|
63
|
+
| `HELP` | Full in-app shortcut list |
|
|
64
|
+
|
|
65
|
+
## What It Does
|
|
66
|
+
|
|
67
|
+
- Research companies with quotes, charts, financials, filings, holders, insiders, options, analyst ratings, events, and relative valuation.
|
|
68
|
+
- Follow markets with top stories, breaking news, sector feeds, global indices, FX, macro events, yield curves, market movers, and fear/greed.
|
|
69
|
+
- Track portfolios and watchlists, connect brokers, set alerts, keep notes, run AI screens, browse prediction markets, and use Gloom Cloud chat.
|
|
70
|
+
|
|
103
71
|
## CLI
|
|
104
72
|
|
|
105
73
|
Running `gloomberb` with no arguments launches the terminal UI. Use `gloomberb help` to see the full command list.
|
|
@@ -148,7 +116,89 @@ See [PLUGINS.md](PLUGINS.md) for the plugin API and the shared UI surface availa
|
|
|
148
116
|
| `m` | Cycle chart mode |
|
|
149
117
|
| `q` | Quit |
|
|
150
118
|
|
|
151
|
-
Desktop builds also accept the matching `Cmd` shortcuts on macOS,
|
|
119
|
+
Desktop builds also accept `Cmd/Ctrl+K` for the command bar, the matching `Cmd` shortcuts on macOS, `Cmd/Ctrl+Shift+O` to pop out a pane, and `Cmd/Ctrl+Shift+C` to copy a focused pane screenshot.
|
|
120
|
+
|
|
121
|
+
## Command Reference
|
|
122
|
+
|
|
123
|
+
Use `HELP` inside Gloomberb for the live shortcut list. The common command-bar prefixes are listed here for quick scanning.
|
|
124
|
+
|
|
125
|
+
### Company Research
|
|
126
|
+
|
|
127
|
+
| Shortcut | Function |
|
|
128
|
+
|----------|----------|
|
|
129
|
+
| `DES <ticker>` / `T <ticker>` | Security details for a ticker |
|
|
130
|
+
| `FA <ticker>` | Financial statement view |
|
|
131
|
+
| `GP <ticker>` | Price chart |
|
|
132
|
+
| `GIP <ticker>` | Intraday price chart |
|
|
133
|
+
| `HP <ticker>` | Historical OHLCV prices |
|
|
134
|
+
| `GF <tickers>` | Fundamental statement graph |
|
|
135
|
+
| `GE <tickers>` | Valuation multiple graph |
|
|
136
|
+
| `GR <tickers>` | Security relationship graph |
|
|
137
|
+
| `EE <ticker>` | Events view with earnings and revenue estimates |
|
|
138
|
+
| `EM [tickers]` | Earnings monitor |
|
|
139
|
+
| `SRCH <query>` | Provider symbol search |
|
|
140
|
+
| `QQ <tickers>` | Ticker quote monitor |
|
|
141
|
+
| `CMP <tickers>` | Ticker charts |
|
|
142
|
+
| `CORR <tickers>` | Ticker return correlations |
|
|
143
|
+
| `ANR <ticker>` | Analyst targets and ratings |
|
|
144
|
+
| `SEC <ticker>` | SEC filings and company disclosures |
|
|
145
|
+
| `OMON <ticker>` | Options monitor |
|
|
146
|
+
| `HDS <ticker>` | Institutional holders |
|
|
147
|
+
| `13F [fund/ticker/CIK]` | 13F fund filings and holdings |
|
|
148
|
+
| `INS <ticker>` | Insider activity |
|
|
149
|
+
| `EVT <ticker>` | Corporate actions, earnings, and estimates |
|
|
150
|
+
| `RV <tickers>` | Relative valuation |
|
|
151
|
+
|
|
152
|
+
### Markets, News, and Macro
|
|
153
|
+
|
|
154
|
+
| Shortcut | Function |
|
|
155
|
+
|----------|----------|
|
|
156
|
+
| `TOP` | Ranked market stories |
|
|
157
|
+
| `MOST` | Top gainers, losers, most active, and trending tickers |
|
|
158
|
+
| `N` | News feed |
|
|
159
|
+
| `CN <ticker>` | Ticker news |
|
|
160
|
+
| `NI` | Sector news |
|
|
161
|
+
| `FIRST` | Breaking news |
|
|
162
|
+
| `TWIT <query>` | Ticker-related market posts |
|
|
163
|
+
| `WEI` | Global equity indices |
|
|
164
|
+
| `ECON` | Economic events and releases |
|
|
165
|
+
| `GC` | Yield curve |
|
|
166
|
+
| `ERN` | Earnings calendar |
|
|
167
|
+
| `BI` / `SP` | S&P 500 sector performance |
|
|
168
|
+
| `FXC` | Major FX cross rates |
|
|
169
|
+
| `FNG` | Fear and greed market gauge |
|
|
170
|
+
|
|
171
|
+
### Workspace and App Controls
|
|
172
|
+
|
|
173
|
+
| Shortcut | Function |
|
|
174
|
+
|----------|----------|
|
|
175
|
+
| `PF` | Portfolio and watchlist workspace |
|
|
176
|
+
| `PORT` | Portfolio risk and sector exposure |
|
|
177
|
+
| `ALRT` | Price alerts |
|
|
178
|
+
| `SA <symbol condition price>` | Create a price alert |
|
|
179
|
+
| `PM <query>` | Polymarket and Kalshi prediction data |
|
|
180
|
+
| `AI <prompt>` | AI screener |
|
|
181
|
+
| `CHAT [channel]` | Gloom Cloud chat |
|
|
182
|
+
| `DM @user [@user...]` | Open or start a direct or group chat |
|
|
183
|
+
| `ACM` | Gloom Cloud account settings |
|
|
184
|
+
| `TBO` | TheBuildout infrastructure intelligence |
|
|
185
|
+
| `CG` | Congress trading disclosures |
|
|
186
|
+
| `NOTE` | Notes |
|
|
187
|
+
| `IBKR` | IBKR trading pane |
|
|
188
|
+
| `BR` | Broker connections |
|
|
189
|
+
| `CHG` | Changelog |
|
|
190
|
+
| `HELP` | Open shortcut and layout help |
|
|
191
|
+
| `AW` / `AP <ticker>` | Add a ticker to the active watchlist or portfolio |
|
|
192
|
+
| `RW` / `RP <ticker>` | Remove a ticker from the active watchlist or portfolio |
|
|
193
|
+
| `PS` | Open focused pane settings |
|
|
194
|
+
| `LAY <action>` | Open layout actions |
|
|
195
|
+
| `WIN move\|resize` | Move or resize the focused window |
|
|
196
|
+
| `GL` | Gridlock all visible panes |
|
|
197
|
+
| `SB` | Toggle the status bar |
|
|
198
|
+
| `VF` | Toggle quote value flashing |
|
|
199
|
+
| `TH <theme>` | Change color theme |
|
|
200
|
+
| `CR` | Cycle chart renderer |
|
|
201
|
+
| `PL <plugin>` | Manage plugins |
|
|
152
202
|
|
|
153
203
|
## License
|
|
154
204
|
|
package/package.json
CHANGED
package/src/api-client/chat.ts
CHANGED
|
@@ -93,6 +93,14 @@ export class CloudChatApi {
|
|
|
93
93
|
return normalizeChatMessage(message);
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
async editMessage(channelId: string, messageId: string, content: string): Promise<ChatMessage> {
|
|
97
|
+
const message = await this.options.request<ChatMessage>(`/chat/channels/${channelId}/messages/${messageId}`, {
|
|
98
|
+
method: "PATCH",
|
|
99
|
+
body: JSON.stringify({ content }),
|
|
100
|
+
});
|
|
101
|
+
return normalizeChatMessage(message);
|
|
102
|
+
}
|
|
103
|
+
|
|
96
104
|
connectChannel(
|
|
97
105
|
channelId: string,
|
|
98
106
|
onMessage: (msg: ChatMessage) => void,
|
|
@@ -379,6 +379,36 @@ describe("apiClient chat timestamps", () => {
|
|
|
379
379
|
expect(sentMessage.createdAt).toBe("2026-04-08T07:29:27.625Z");
|
|
380
380
|
});
|
|
381
381
|
|
|
382
|
+
test("edits a chat message and normalizes the edit timestamp", async () => {
|
|
383
|
+
const requests: Array<{ path: string; method: string; body: unknown }> = [];
|
|
384
|
+
globalThis.fetch = mockFetch(async (input: Request | string | URL, init?: RequestInit) => {
|
|
385
|
+
requests.push({
|
|
386
|
+
path: new URL(String(input)).pathname,
|
|
387
|
+
method: init?.method ?? "GET",
|
|
388
|
+
body: init?.body ? JSON.parse(String(init.body)) : null,
|
|
389
|
+
});
|
|
390
|
+
return createResponse({
|
|
391
|
+
id: "m2",
|
|
392
|
+
channelId: "everyone",
|
|
393
|
+
content: "hello edited",
|
|
394
|
+
replyToId: null,
|
|
395
|
+
createdAt: "2026-04-08 07:29:27.625",
|
|
396
|
+
editedAt: "2026-04-08 07:30:27.625",
|
|
397
|
+
user: { id: "u1", username: "alice", displayName: "Alice" },
|
|
398
|
+
replyTo: null,
|
|
399
|
+
});
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
const editedMessage = await apiClient.editMessage("everyone", "m2", "hello edited");
|
|
403
|
+
|
|
404
|
+
expect(requests).toEqual([{
|
|
405
|
+
path: "/chat/channels/everyone/messages/m2",
|
|
406
|
+
method: "PATCH",
|
|
407
|
+
body: { content: "hello edited" },
|
|
408
|
+
}]);
|
|
409
|
+
expect(editedMessage.editedAt).toBe("2026-04-08T07:30:27.625Z");
|
|
410
|
+
});
|
|
411
|
+
|
|
382
412
|
test("normalizes websocket chat timestamps before notifying listeners", async () => {
|
|
383
413
|
const seenCreatedAts: string[] = [];
|
|
384
414
|
const channel = apiClient.connectChannel("everyone", (message) => {
|
package/src/api-client/index.ts
CHANGED
|
@@ -244,6 +244,10 @@ class GloomApiClient {
|
|
|
244
244
|
return this.chat.sendMessage(channelId, content, replyToId, clientMessageId);
|
|
245
245
|
}
|
|
246
246
|
|
|
247
|
+
async editMessage(channelId: string, messageId: string, content: string): Promise<ChatMessage> {
|
|
248
|
+
return this.chat.editMessage(channelId, messageId, content);
|
|
249
|
+
}
|
|
250
|
+
|
|
247
251
|
connectChannel(
|
|
248
252
|
channelId: string,
|
|
249
253
|
onMessage: (msg: ChatMessage) => void,
|
package/src/api-client/types.ts
CHANGED
|
@@ -28,6 +28,7 @@ export interface ChatMessage {
|
|
|
28
28
|
content: string;
|
|
29
29
|
replyToId: string | null;
|
|
30
30
|
createdAt: string;
|
|
31
|
+
editedAt?: string | null;
|
|
31
32
|
user: ChatUserSummary;
|
|
32
33
|
replyTo?: { content: string; user: { id?: string; username: string } } | null;
|
|
33
34
|
clientStatus?: "sending" | "failed";
|
|
@@ -52,6 +52,10 @@ export function assetDataProvider(provider: AssetDataProvider): AssetDataCapabil
|
|
|
52
52
|
if (!provider.getEarningsCalendar) throw new Error(`${provider.name} does not provide earnings calendar.`);
|
|
53
53
|
return provider.getEarningsCalendar(input.symbols ?? [], input.context);
|
|
54
54
|
}, "query"),
|
|
55
|
+
getSecFilingDocuments: op((input: any) => {
|
|
56
|
+
if (!provider.getSecFilingDocuments) throw new Error(`${provider.name} does not provide SEC filing documents.`);
|
|
57
|
+
return provider.getSecFilingDocuments(input.filing);
|
|
58
|
+
}, "query"),
|
|
55
59
|
getSecFilingContent: op((input: any) => {
|
|
56
60
|
if (!provider.getSecFilingContent) throw new Error(`${provider.name} does not provide SEC filing content.`);
|
|
57
61
|
return provider.getSecFilingContent(input.filing);
|
|
@@ -57,6 +57,60 @@ describe("projectComparisonChartData", () => {
|
|
|
57
57
|
expect(projection.series[1]?.points.map((point) => point.rawValue)).toEqual([null, null, 20, 22]);
|
|
58
58
|
});
|
|
59
59
|
|
|
60
|
+
test("carries the latest series value across shared timestamps from other markets", () => {
|
|
61
|
+
const projection = projectComparisonChartData([
|
|
62
|
+
makeSeries("DAX", "EUR", [
|
|
63
|
+
["2024-01-02T08:00:00.000Z", 100],
|
|
64
|
+
["2024-01-02T08:15:00.000Z", 101],
|
|
65
|
+
["2024-01-03T08:00:00.000Z", 102],
|
|
66
|
+
]),
|
|
67
|
+
makeSeries("AAPL", "USD", [
|
|
68
|
+
["2024-01-02T14:30:00.000Z", 200],
|
|
69
|
+
["2024-01-02T14:45:00.000Z", 201],
|
|
70
|
+
]),
|
|
71
|
+
], 8, {
|
|
72
|
+
panOffset: 0,
|
|
73
|
+
zoomLevel: 1,
|
|
74
|
+
renderMode: "line",
|
|
75
|
+
}, "price");
|
|
76
|
+
|
|
77
|
+
expect(projection.dates.map((date) => date.toISOString())).toEqual([
|
|
78
|
+
"2024-01-02T08:00:00.000Z",
|
|
79
|
+
"2024-01-02T08:15:00.000Z",
|
|
80
|
+
"2024-01-02T14:30:00.000Z",
|
|
81
|
+
"2024-01-02T14:45:00.000Z",
|
|
82
|
+
"2024-01-03T08:00:00.000Z",
|
|
83
|
+
]);
|
|
84
|
+
expect(projection.series[0]?.points.map((point) => point.rawValue)).toEqual([100, 101, 101, 101, 102]);
|
|
85
|
+
expect(projection.series[1]?.points.map((point) => point.rawValue)).toEqual([null, null, 200, 201, 201]);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("uses the same alternating close bucket selection as stock chart projection", () => {
|
|
89
|
+
const projection = projectComparisonChartData([
|
|
90
|
+
makeSeries("DAX", "EUR", [
|
|
91
|
+
["2024-01-02T08:00:00.000Z", 100],
|
|
92
|
+
["2024-01-02T08:15:00.000Z", 105],
|
|
93
|
+
["2024-01-02T14:30:00.000Z", 101],
|
|
94
|
+
["2024-01-02T14:45:00.000Z", 99],
|
|
95
|
+
]),
|
|
96
|
+
makeSeries("AAPL", "USD", [
|
|
97
|
+
["2024-01-02T14:30:00.000Z", 200],
|
|
98
|
+
["2024-01-02T14:45:00.000Z", 201],
|
|
99
|
+
]),
|
|
100
|
+
], 2, {
|
|
101
|
+
panOffset: 0,
|
|
102
|
+
zoomLevel: 1,
|
|
103
|
+
renderMode: "line",
|
|
104
|
+
}, "price");
|
|
105
|
+
|
|
106
|
+
expect(projection.dates.map((date) => date.toISOString())).toEqual([
|
|
107
|
+
"2024-01-02T08:15:00.000Z",
|
|
108
|
+
"2024-01-02T14:45:00.000Z",
|
|
109
|
+
]);
|
|
110
|
+
expect(projection.series[0]?.points.map((point) => point.rawValue)).toEqual([105, 99]);
|
|
111
|
+
expect(projection.series[1]?.points.map((point) => point.rawValue)).toEqual([null, 200]);
|
|
112
|
+
});
|
|
113
|
+
|
|
60
114
|
test("normalizes percent mode from each series first visible point", () => {
|
|
61
115
|
const projection = projectComparisonChartData([
|
|
62
116
|
makeSeries("AAPL", "USD", [
|
|
@@ -7,6 +7,11 @@ import type {
|
|
|
7
7
|
} from "../core/types";
|
|
8
8
|
import { buildVisibleDateWindowFromRange } from "../core/date-window";
|
|
9
9
|
import { getVisiblePointCount } from "../core/viewport";
|
|
10
|
+
import {
|
|
11
|
+
buildIndexProjectionBuckets,
|
|
12
|
+
selectRepresentativeCloseValue,
|
|
13
|
+
type IndexProjectionBucket,
|
|
14
|
+
} from "../core/projection";
|
|
10
15
|
|
|
11
16
|
export interface ComparisonProjectedPoint {
|
|
12
17
|
date: Date;
|
|
@@ -38,6 +43,14 @@ interface ComparisonAxisModeResolution {
|
|
|
38
43
|
warning: string | null;
|
|
39
44
|
}
|
|
40
45
|
|
|
46
|
+
type ComparisonWindowViewState =
|
|
47
|
+
Pick<ComparisonChartViewState, "panOffset" | "zoomLevel">
|
|
48
|
+
& Partial<Pick<ComparisonChartViewState, "dateWindow">>;
|
|
49
|
+
|
|
50
|
+
type ComparisonProjectionViewState =
|
|
51
|
+
ComparisonWindowViewState
|
|
52
|
+
& Pick<ComparisonChartViewState, "renderMode">;
|
|
53
|
+
|
|
41
54
|
export interface ComparisonChartProjection {
|
|
42
55
|
dates: Date[];
|
|
43
56
|
series: ComparisonProjectedSeries[];
|
|
@@ -92,7 +105,7 @@ export function getMaxComparisonPanOffset(
|
|
|
92
105
|
|
|
93
106
|
export function getVisibleComparisonWindow(
|
|
94
107
|
series: ComparisonChartSeries[],
|
|
95
|
-
viewState:
|
|
108
|
+
viewState: ComparisonWindowViewState,
|
|
96
109
|
): ComparisonVisibleWindow {
|
|
97
110
|
const dates = getUniqueSortedDates(series);
|
|
98
111
|
|
|
@@ -138,10 +151,7 @@ function transformRawValue(rawValue: number | null, axisMode: ChartAxisMode, bas
|
|
|
138
151
|
}
|
|
139
152
|
|
|
140
153
|
function resolveRepresentativeValue(values: number[], index: number): number {
|
|
141
|
-
|
|
142
|
-
return index % 2 === 0
|
|
143
|
-
? Math.max(...values)
|
|
144
|
-
: Math.min(...values);
|
|
154
|
+
return selectRepresentativeCloseValue(values, index, (value) => value);
|
|
145
155
|
}
|
|
146
156
|
|
|
147
157
|
function buildSeriesMap(points: PricePoint[]): Map<number, number> {
|
|
@@ -155,6 +165,28 @@ function buildSeriesMap(points: PricePoint[]): Map<number, number> {
|
|
|
155
165
|
return map;
|
|
156
166
|
}
|
|
157
167
|
|
|
168
|
+
function buildFilledRawValuesByDate(
|
|
169
|
+
rawByDate: ReadonlyMap<number, number>,
|
|
170
|
+
dates: readonly Date[],
|
|
171
|
+
): Array<number | null> {
|
|
172
|
+
let latestRawValue: number | null = null;
|
|
173
|
+
return dates.map((date) => {
|
|
174
|
+
const timestamp = date.getTime();
|
|
175
|
+
if (rawByDate.has(timestamp)) {
|
|
176
|
+
latestRawValue = rawByDate.get(timestamp)!;
|
|
177
|
+
return latestRawValue;
|
|
178
|
+
}
|
|
179
|
+
return latestRawValue;
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function getBucketDate(
|
|
184
|
+
dates: readonly Date[],
|
|
185
|
+
bucket: IndexProjectionBucket,
|
|
186
|
+
): Date {
|
|
187
|
+
return dates[Math.max(bucket.end - 1, 0)] ?? dates[dates.length - 1] ?? new Date(0);
|
|
188
|
+
}
|
|
189
|
+
|
|
158
190
|
function resolveComparisonAxisMode(
|
|
159
191
|
requestedAxisMode: ChartAxisMode,
|
|
160
192
|
series: ComparisonChartSeries[],
|
|
@@ -194,7 +226,7 @@ function resolveComparisonAxisMode(
|
|
|
194
226
|
export function projectComparisonChartData(
|
|
195
227
|
series: ComparisonChartSeries[],
|
|
196
228
|
chartWidth: number,
|
|
197
|
-
viewState:
|
|
229
|
+
viewState: ComparisonProjectionViewState,
|
|
198
230
|
requestedAxisMode: ChartAxisMode,
|
|
199
231
|
): ComparisonChartProjection {
|
|
200
232
|
const normalizedSeries = series.map((entry) => ({ ...entry, points: normalizeSeriesPoints(entry.points) }));
|
|
@@ -221,31 +253,26 @@ export function projectComparisonChartData(
|
|
|
221
253
|
warning: axisResolution.warning,
|
|
222
254
|
};
|
|
223
255
|
}
|
|
224
|
-
const
|
|
225
|
-
const bucketSize = window.dates.length > 0 ? window.dates.length / bucketCount : 1;
|
|
256
|
+
const buckets = buildIndexProjectionBuckets(window.dates.length, chartWidth);
|
|
226
257
|
|
|
227
258
|
const projectedSeries = normalizedSeries.map((entry) => {
|
|
228
259
|
const rawByDate = buildSeriesMap(entry.points);
|
|
229
|
-
const
|
|
230
|
-
|
|
260
|
+
const filledRawValues = buildFilledRawValuesByDate(rawByDate, window.dates);
|
|
261
|
+
const visibleRawValues = filledRawValues
|
|
231
262
|
.filter((value): value is number => value !== null);
|
|
232
263
|
const baseValue = visibleRawValues[0] ?? null;
|
|
233
264
|
const latestRawValue = visibleRawValues[visibleRawValues.length - 1] ?? null;
|
|
234
265
|
|
|
235
|
-
const points =
|
|
236
|
-
const
|
|
237
|
-
|
|
238
|
-
const bucketDates = window.dates.slice(start, end);
|
|
239
|
-
const date = bucketDates[bucketDates.length - 1] ?? window.dates[window.dates.length - 1] ?? new Date(0);
|
|
240
|
-
const bucketRawValues = bucketDates
|
|
241
|
-
.map((bucketDate) => rawByDate.get(bucketDate.getTime()) ?? null)
|
|
266
|
+
const points = buckets.map((bucket) => {
|
|
267
|
+
const bucketRawValues = filledRawValues
|
|
268
|
+
.slice(bucket.start, bucket.end)
|
|
242
269
|
.filter((value): value is number => value !== null);
|
|
243
270
|
const rawValue = bucketRawValues.length > 0
|
|
244
|
-
? resolveRepresentativeValue(bucketRawValues, index)
|
|
271
|
+
? resolveRepresentativeValue(bucketRawValues, bucket.index)
|
|
245
272
|
: null;
|
|
246
273
|
|
|
247
274
|
return {
|
|
248
|
-
date,
|
|
275
|
+
date: getBucketDate(window.dates, bucket),
|
|
249
276
|
rawValue,
|
|
250
277
|
value: transformRawValue(rawValue, axisResolution.effectiveAxisMode, baseValue),
|
|
251
278
|
};
|
|
@@ -264,11 +291,7 @@ export function projectComparisonChartData(
|
|
|
264
291
|
});
|
|
265
292
|
|
|
266
293
|
return {
|
|
267
|
-
dates:
|
|
268
|
-
const start = Math.floor(index * bucketSize);
|
|
269
|
-
const end = Math.min(window.dates.length, Math.max(Math.floor((index + 1) * bucketSize), start + 1));
|
|
270
|
-
return window.dates[Math.max(end - 1, 0)] ?? new Date(0);
|
|
271
|
-
}),
|
|
294
|
+
dates: buckets.map((bucket) => getBucketDate(window.dates, bucket)),
|
|
272
295
|
series: projectedSeries,
|
|
273
296
|
requestedMode,
|
|
274
297
|
effectiveMode: requestedMode,
|
|
@@ -24,6 +24,12 @@ export interface ProjectChartDataOptions {
|
|
|
24
24
|
sourceIndexOffset?: number;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
export interface IndexProjectionBucket {
|
|
28
|
+
index: number;
|
|
29
|
+
start: number;
|
|
30
|
+
end: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
27
33
|
interface StableOhlcProjectionOptionsInput {
|
|
28
34
|
pointCount: number;
|
|
29
35
|
sourceIndexOffset: number;
|
|
@@ -131,6 +137,44 @@ function aggregateOhlcBucket(bucket: readonly ProjectedChartPoint[]): ProjectedC
|
|
|
131
137
|
};
|
|
132
138
|
}
|
|
133
139
|
|
|
140
|
+
export function buildIndexProjectionBuckets(
|
|
141
|
+
itemCount: number,
|
|
142
|
+
targetWidth: number,
|
|
143
|
+
): IndexProjectionBucket[] {
|
|
144
|
+
const normalizedItemCount = Math.max(Math.floor(itemCount), 0);
|
|
145
|
+
if (normalizedItemCount === 0) return [];
|
|
146
|
+
|
|
147
|
+
const bucketCount = Math.min(normalizedItemCount, Math.max(Math.floor(targetWidth), 1));
|
|
148
|
+
const bucketSize = normalizedItemCount / bucketCount;
|
|
149
|
+
|
|
150
|
+
return Array.from({ length: bucketCount }, (_, index) => {
|
|
151
|
+
const start = Math.floor(index * bucketSize);
|
|
152
|
+
const end = Math.min(
|
|
153
|
+
normalizedItemCount,
|
|
154
|
+
Math.max(Math.floor((index + 1) * bucketSize), start + 1),
|
|
155
|
+
);
|
|
156
|
+
return { index, start, end };
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function selectRepresentativeCloseValue<T>(
|
|
161
|
+
bucket: readonly T[],
|
|
162
|
+
index: number,
|
|
163
|
+
getClose: (item: T) => number,
|
|
164
|
+
): T {
|
|
165
|
+
let best = bucket[0] as T;
|
|
166
|
+
if (index % 2 === 0) {
|
|
167
|
+
for (const item of bucket) {
|
|
168
|
+
if (getClose(item) > getClose(best)) best = item;
|
|
169
|
+
}
|
|
170
|
+
} else {
|
|
171
|
+
for (const item of bucket) {
|
|
172
|
+
if (getClose(item) < getClose(best)) best = item;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return best;
|
|
176
|
+
}
|
|
177
|
+
|
|
134
178
|
function bucketOhlcBySourceIndex(
|
|
135
179
|
normalizedPoints: readonly ProjectedChartPoint[],
|
|
136
180
|
bucketSize: number,
|
|
@@ -202,33 +246,13 @@ function projectCloseSeries(
|
|
|
202
246
|
targetWidth: number,
|
|
203
247
|
): ProjectedChartPoint[] {
|
|
204
248
|
if (points.length === 0) return [];
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
const end = Math.min(points.length, Math.max(Math.floor((i + 1) * bucketSize), start + 1));
|
|
213
|
-
const bucket = points.slice(start, end);
|
|
214
|
-
|
|
215
|
-
if (bucket.length === 0) continue;
|
|
216
|
-
|
|
217
|
-
// Alternate bucket extremes to preserve line/area shape after downsampling.
|
|
218
|
-
let best = bucket[0]!;
|
|
219
|
-
if (i % 2 === 0) {
|
|
220
|
-
for (const point of bucket) {
|
|
221
|
-
if (point.close > best.close) best = point;
|
|
222
|
-
}
|
|
223
|
-
} else {
|
|
224
|
-
for (const point of bucket) {
|
|
225
|
-
if (point.close < best.close) best = point;
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
result.push(normalizePoint(best));
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
return result;
|
|
249
|
+
return buildIndexProjectionBuckets(points.length, targetWidth).map((bucket) => (
|
|
250
|
+
normalizePoint(selectRepresentativeCloseValue(
|
|
251
|
+
points.slice(bucket.start, bucket.end),
|
|
252
|
+
bucket.index,
|
|
253
|
+
(point) => point.close,
|
|
254
|
+
))
|
|
255
|
+
));
|
|
232
256
|
}
|
|
233
257
|
|
|
234
258
|
/**
|
|
@@ -67,18 +67,18 @@ function Harness() {
|
|
|
67
67
|
)
|
|
68
68
|
}
|
|
69
69
|
detailTitle={openRow?.title}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
70
|
+
selection={{
|
|
71
|
+
kind: "index",
|
|
72
|
+
selectedIndex,
|
|
73
|
+
onChange: (index) => setSelectedIndex(index),
|
|
74
|
+
}}
|
|
75
|
+
onActivate={(row) => setOpenRow(row)}
|
|
73
76
|
columns={columns}
|
|
74
77
|
items={rows}
|
|
75
78
|
sortColumnId={null}
|
|
76
79
|
sortDirection="asc"
|
|
77
80
|
onHeaderClick={() => {}}
|
|
78
81
|
getItemKey={(row) => row.id}
|
|
79
|
-
isSelected={(_row, index) => index === selectedIndex}
|
|
80
|
-
onSelect={(_row, index) => setSelectedIndex(index)}
|
|
81
|
-
onActivate={(row) => setOpenRow(row)}
|
|
82
82
|
renderCell={(row): DataTableCell => ({ text: row.title })}
|
|
83
83
|
emptyStateTitle="No rows"
|
|
84
84
|
showHorizontalScrollbar={false}
|
|
@@ -90,13 +90,14 @@ function Harness() {
|
|
|
90
90
|
|
|
91
91
|
function GroupedHarness() {
|
|
92
92
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
93
|
+
const [cursorIndex, setCursorIndex] = useState(0);
|
|
93
94
|
const state = createInitialState(
|
|
94
95
|
createDefaultConfig("/tmp/gloomberb-data-table-stack-view-grouped-test"),
|
|
95
96
|
);
|
|
96
97
|
const columns: Column[] = [
|
|
97
98
|
{ id: "title", label: "Title", width: 20, align: "left" },
|
|
98
99
|
];
|
|
99
|
-
const selectedTitle = groupedRows[
|
|
100
|
+
const selectedTitle = groupedRows[cursorIndex]?.title ?? "none";
|
|
100
101
|
|
|
101
102
|
return (
|
|
102
103
|
<AppContext value={{ state, dispatch: () => {} }}>
|
|
@@ -106,8 +107,12 @@ function GroupedHarness() {
|
|
|
106
107
|
detailOpen={false}
|
|
107
108
|
onBack={() => {}}
|
|
108
109
|
detailContent={<Box flexGrow={1} />}
|
|
109
|
-
|
|
110
|
-
|
|
110
|
+
selection={{
|
|
111
|
+
kind: "index",
|
|
112
|
+
selectedIndex,
|
|
113
|
+
onChange: (index) => setSelectedIndex(index),
|
|
114
|
+
}}
|
|
115
|
+
onCursorChange={(_row, index) => setCursorIndex(index)}
|
|
111
116
|
isNavigable={(row) => row.type !== "section"}
|
|
112
117
|
rootAfter={
|
|
113
118
|
<Box height={1}>
|
|
@@ -120,8 +125,6 @@ function GroupedHarness() {
|
|
|
120
125
|
sortDirection="asc"
|
|
121
126
|
onHeaderClick={() => {}}
|
|
122
127
|
getItemKey={(row) => row.id}
|
|
123
|
-
isSelected={(_row, index) => index === selectedIndex}
|
|
124
|
-
onSelect={(_row, index) => setSelectedIndex(index)}
|
|
125
128
|
renderSectionHeader={(row) => row.type === "section"
|
|
126
129
|
? { text: row.title }
|
|
127
130
|
: null}
|
|
@@ -10,13 +10,12 @@ import { PageStackView, type DataTableColumn } from "../ui";
|
|
|
10
10
|
export interface DataTableStackViewProps<
|
|
11
11
|
T,
|
|
12
12
|
C extends DataTableColumn = DataTableColumn,
|
|
13
|
-
> extends Omit<DataTableViewProps<T, C>, "focused"
|
|
13
|
+
> extends Omit<DataTableViewProps<T, C>, "focused"> {
|
|
14
14
|
focused: boolean;
|
|
15
15
|
detailOpen: boolean;
|
|
16
16
|
onBack: () => void;
|
|
17
17
|
detailContent: ReactNode;
|
|
18
18
|
detailTitle?: string;
|
|
19
|
-
selectedIndex: number;
|
|
20
19
|
onDetailKeyDown?: (event: DataTableKeyEvent) => boolean | void;
|
|
21
20
|
}
|
|
22
21
|
|