gloomberb 0.7.2 → 0.7.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.
- package/README.md +79 -132
- package/package.json +1 -1
- package/src/app.tsx +73 -67
- package/src/components/chart/chart-data.ts +7 -2
- package/src/components/chart/chart-pane-settings.ts +7 -4
- package/src/components/chart/chart-renderer.ts +49 -15
- package/src/components/chart/chart-types.ts +2 -2
- package/src/components/chart/chart.test.ts +27 -8
- package/src/components/chart/comparison-stock-chart.tsx +84 -121
- package/src/components/chart/native/chart-rasterizer.ts +43 -17
- package/src/components/chart/price-axis-labels.tsx +25 -9
- package/src/components/chart/stock-chart-renderer-switch.test.tsx +1 -1
- package/src/components/chart/stock-chart.tsx +59 -41
- package/src/components/command-bar/command-bar.test.tsx +446 -120
- package/src/components/command-bar/command-bar.tsx +528 -383
- package/src/components/command-bar/command-registry.ts +16 -6
- package/src/components/command-bar/root-shortcuts.ts +4 -5
- package/src/components/command-bar/view-model.test.ts +1 -0
- package/src/components/command-bar/view-model.ts +2 -1
- package/src/components/command-bar/workflow-ops.ts +11 -32
- package/src/components/data-table-view.test.tsx +24 -1
- package/src/components/data-table-view.tsx +3 -2
- package/src/components/feed-data-table-stack-view.tsx +3 -2
- package/src/components/layout/detached-pane-shell.tsx +2 -2
- package/src/components/layout/floating-pane.tsx +9 -2
- package/src/components/layout/pane-footer.tsx +1 -1
- package/src/components/layout/pane-header.tsx +4 -2
- package/src/components/layout/pane-sizing.ts +12 -0
- package/src/components/layout/pane.tsx +24 -2
- package/src/components/layout/shell.tsx +24 -10
- package/src/components/layout/status-bar.test.tsx +4 -1
- package/src/components/layout/status-bar.tsx +4 -24
- package/src/components/markdown-text.tsx +53 -8
- package/src/components/onboarding/onboarding-wizard.tsx +7 -6
- package/src/components/pane-settings-dialog.tsx +9 -6
- package/src/components/pane-template-wizard.tsx +3 -2
- package/src/components/price-selector-dialog.tsx +3 -2
- package/src/components/price-sparkline-view.tsx +463 -0
- package/src/components/price-sparkline.ts +46 -0
- package/src/components/table-view-shared.tsx +21 -3
- package/src/components/ticker-list-table-view.tsx +3 -2
- package/src/components/ticker-list-table.tsx +11 -51
- package/src/components/toggle-list.tsx +3 -3
- package/src/components/ui/choice-dialog.tsx +3 -2
- package/src/components/ui/multi-select-dialog.tsx +3 -2
- package/src/components/ui/native-select.tsx +3 -3
- package/src/core/state/app-state.ts +210 -80
- package/src/data/config-store-node.ts +113 -25
- package/src/data/config-store.test.ts +65 -3
- package/src/data/plugin-state-store.ts +39 -13
- package/src/data/sqlite-cache.test.ts +13 -0
- package/src/market-data/coordinator.ts +8 -1
- package/src/news/aggregator.test.ts +41 -0
- package/src/news/aggregator.ts +61 -9
- package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +25 -6
- package/src/plugins/builtin/ai/settings.ts +3 -0
- package/src/plugins/builtin/broker-manager/index.tsx +4 -3
- package/src/plugins/builtin/chat-controller.test.ts +398 -17
- package/src/plugins/builtin/chat-controller.ts +258 -44
- package/src/plugins/builtin/chat.test.tsx +257 -6
- package/src/plugins/builtin/chat.tsx +225 -60
- package/src/plugins/builtin/comparison-chart.test.tsx +117 -3
- package/src/plugins/builtin/correlation/colors.test.ts +39 -0
- package/src/plugins/builtin/correlation/colors.ts +87 -0
- package/src/plugins/builtin/correlation/index.test.tsx +151 -0
- package/src/plugins/builtin/correlation/index.tsx +92 -21
- package/src/plugins/builtin/debug.tsx +4 -3
- package/src/plugins/builtin/econ/index.tsx +3 -2
- package/src/plugins/builtin/help.test.tsx +2 -2
- package/src/plugins/builtin/holders/index.tsx +25 -13
- package/src/plugins/builtin/layout-manager.tsx +1 -7
- package/src/plugins/builtin/news-wire/index.tsx +1 -1
- package/src/plugins/builtin/news-wire/news-detail-view.tsx +3 -2
- package/src/plugins/builtin/news-wire/news-preset-pane.tsx +4 -1
- package/src/plugins/builtin/news-wire/persisted-articles.ts +7 -1
- package/src/plugins/builtin/options.tsx +9 -17
- package/src/plugins/builtin/portfolio-list/cli/commands.ts +6 -10
- package/src/plugins/builtin/portfolio-list/metrics.ts +11 -0
- package/src/plugins/builtin/portfolio-list/mutations.ts +17 -0
- package/src/plugins/builtin/portfolio-list/pane.tsx +117 -18
- package/src/plugins/builtin/portfolio-list/quick-add.tsx +422 -0
- package/src/plugins/builtin/portfolio-list/settings.ts +20 -12
- package/src/plugins/builtin/portfolio-list/table.tsx +3 -3
- package/src/plugins/builtin/portfolio-list.test.tsx +282 -2
- package/src/plugins/builtin/quote-monitor.test.tsx +158 -25
- package/src/plugins/builtin/sectors/index.tsx +252 -41
- package/src/plugins/builtin/sectors/sector-data.ts +41 -1
- package/src/plugins/builtin/ticker-detail/chart-tab.tsx +3 -2
- package/src/plugins/builtin/ticker-detail/index.tsx +30 -9
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +25 -15
- package/src/plugins/builtin/ticker-detail/pane.tsx +0 -6
- package/src/plugins/builtin/ticker-detail/quote-monitor.tsx +442 -63
- package/src/plugins/builtin/ticker-detail/settings.ts +84 -3
- package/src/plugins/builtin/ticker-detail.test.tsx +2 -2
- package/src/plugins/ibkr/trading-pane.tsx +3 -2
- package/src/plugins/plugin-runtime.tsx +2 -2
- package/src/plugins/prediction-markets/settings.ts +4 -0
- package/src/plugins/registry.test.ts +55 -0
- package/src/plugins/registry.ts +11 -4
- package/src/plugins/ticker-navigation.test.ts +35 -5
- package/src/plugins/ticker-navigation.ts +16 -22
- package/src/renderers/electrobun/bun/application-menu-click.test.ts +9 -0
- package/src/renderers/electrobun/bun/application-menu-click.ts +7 -4
- package/src/renderers/electrobun/bun/application-menu.ts +6 -1
- package/src/renderers/electrobun/bun/desktop-workspace.ts +47 -29
- package/src/renderers/electrobun/bun/index.ts +52 -13
- package/src/renderers/electrobun/view/app-services.ts +51 -9
- package/src/renderers/electrobun/view/backend-rpc.ts +5 -8
- package/src/renderers/electrobun/view/desktop-controls.tsx +4 -4
- package/src/renderers/electrobun/view/main.tsx +0 -16
- package/src/renderers/electrobun/view/styles.css +23 -21
- package/src/renderers/electrobun/view/ui-host.tsx +6 -4
- package/src/renderers/opentui/host.tsx +2 -1
- package/src/renderers/opentui/start.tsx +1 -1
- package/src/renderers/opentui/test-utils.tsx +6 -5
- package/src/state/app-bootstrap.ts +3 -3
- package/src/state/app-context.test.ts +113 -0
- package/src/state/app-context.tsx +8 -4
- package/src/theme/colors.ts +13 -14
- package/src/types/config.ts +9 -4
- package/src/types/plugin.ts +8 -1
- package/src/ui/context-menu.tsx +4 -4
- package/src/utils/api-client.test.ts +128 -0
- package/src/utils/api-client.ts +112 -7
- package/src/utils/fuzzy-search.ts +12 -1
- package/src/utils/keyboard.ts +31 -0
- package/src/utils/ticker-search.test.ts +2 -2
- package/src/utils/ticker-search.ts +48 -1
- package/src/version.ts +1 -1
package/README.md
CHANGED
|
@@ -1,30 +1,70 @@
|
|
|
1
1
|
<div align="center">
|
|
2
2
|
|
|
3
|
-
<img src="
|
|
3
|
+
<img src="https://gloomberb.com/gloomberb-logo-grayscale.svg" alt="Gloomberb logo" width="76" />
|
|
4
4
|
|
|
5
5
|
# Gloomberb
|
|
6
6
|
|
|
7
|
-
**
|
|
7
|
+
**Open-source finance terminal. Fast, keyboard-driven and extensible.**
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Available as a desktop app or TUI.
|
|
10
10
|
|
|
11
|
-
<
|
|
11
|
+
<a href="https://gloomberb.com/download/desktop"><strong>Download desktop for Mac</strong></a>
|
|
12
|
+
|
|
13
|
+
<br />
|
|
14
|
+
<br />
|
|
15
|
+
|
|
16
|
+
<img src="https://gloomberb.com/landing-terminal.png" alt="Gloomberb terminal showing portfolio, watchlists, market data, and chart panels." width="720" />
|
|
12
17
|
|
|
13
18
|
</div>
|
|
14
19
|
|
|
15
|
-
##
|
|
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
|
+
| `QQ <tickers>` | Ticker quote monitor |
|
|
28
|
+
| `PM <query>` | Polymarket and Kalshi prediction data |
|
|
29
|
+
| `TOP` | Ranked market stories |
|
|
30
|
+
| `MOST` | Top gainers, losers, most active, and trending tickers |
|
|
31
|
+
| `WEI` | Global equity indices |
|
|
32
|
+
| `ECON` | Economic events and releases |
|
|
33
|
+
| `CMP <tickers>` | Ticker charts |
|
|
34
|
+
| `CORR <tickers>` | Ticker return correlations |
|
|
35
|
+
| `ANR <ticker>` | Analyst targets and ratings |
|
|
36
|
+
| `SEC <ticker>` | SEC filings and company disclosures |
|
|
37
|
+
| `TWIT <query>` | Ticker-related market posts |
|
|
38
|
+
| `OMON <ticker>` | Options monitor |
|
|
39
|
+
| `PORT` | Portfolio risk and sector exposure |
|
|
40
|
+
| `BI` / `SP` | S&P 500 sector performance |
|
|
41
|
+
| `FXC` | Major FX cross rates |
|
|
42
|
+
| `FNG` | Fear and greed market gauge |
|
|
43
|
+
| `ALRT` | Price alerts |
|
|
44
|
+
| `CHAT` | Gloomberb Cloud chat |
|
|
45
|
+
| `PF` | Portfolio and watchlist workspace |
|
|
46
|
+
| `N` | News feed |
|
|
47
|
+
| `CN <ticker>` | Ticker news |
|
|
48
|
+
| `NI` | Sector news |
|
|
49
|
+
| `FIRST` | Breaking news |
|
|
50
|
+
| `NOTE` | Notes |
|
|
51
|
+
| `AI <prompt>` | AI screener |
|
|
52
|
+
| `GC` | Yield curve |
|
|
53
|
+
| `ERN` | Earnings calendar |
|
|
54
|
+
| `HDS <ticker>` | Institutional holders |
|
|
55
|
+
| `INS <ticker>` | Insider activity |
|
|
56
|
+
| `EVT <ticker>` | Corporate actions |
|
|
57
|
+
| `RV <tickers>` | Relative valuation |
|
|
58
|
+
| `IBKR` | IBKR trading pane |
|
|
59
|
+
| `BR` | Broker connections |
|
|
60
|
+
|
|
61
|
+
## Install
|
|
16
62
|
|
|
17
|
-
|
|
18
|
-
- **Real-time quotes & fundamentals**
|
|
19
|
-
- **Beautiful high-rez charts**
|
|
20
|
-
- **Fast & keyboard-driven**
|
|
21
|
-
- **Powerful layouts**
|
|
22
|
-
- **Extensible**
|
|
23
|
-
- **100% local**
|
|
63
|
+
Desktop:
|
|
24
64
|
|
|
25
|
-
|
|
65
|
+
- [Download Gloomberb for Mac](https://gloomberb.com/download/desktop)
|
|
26
66
|
|
|
27
|
-
|
|
67
|
+
Terminal UI:
|
|
28
68
|
|
|
29
69
|
```bash
|
|
30
70
|
curl -fsSL gloomberb.com/install | bash
|
|
@@ -32,139 +72,46 @@ curl -fsSL gloomberb.com/install | bash
|
|
|
32
72
|
bun install -g gloomberb
|
|
33
73
|
```
|
|
34
74
|
|
|
35
|
-
Then run
|
|
36
|
-
|
|
37
|
-
## 🧩 Plugins
|
|
38
|
-
|
|
39
|
-
Everything, from the portfolio list to broker integrations, is a plugin.
|
|
40
|
-
|
|
41
|
-
Plugins can add tabs, columns, command bar commands, CLI commands, status bar widgets, and more.
|
|
42
|
-
|
|
43
|
-
See **[PLUGINS.md](PLUGINS.md)** for the plugin API and the shared UI surface available through `gloomberb/components`.
|
|
44
|
-
|
|
45
|
-
### Core plugins
|
|
46
|
-
|
|
47
|
-
| Plugin | Description |
|
|
48
|
-
|--------|-------------|
|
|
49
|
-
| **Portfolio List** | Main ticker list with portfolios & watchlists |
|
|
50
|
-
| **Ticker Detail** | Overview, financials, and chart tabs |
|
|
51
|
-
| **Gloomberb Cloud** | Near-real-time market data, macro calendar, yield curve, and chat |
|
|
52
|
-
| **Manual Entry** | Manually add positions, saved locally |
|
|
53
|
-
| **IBKR** | Import positions from Flex Query or trade with Gateway API |
|
|
54
|
-
| **News** | Ticker and market-wide news from Cloud, Yahoo fallback, and RSS feeds |
|
|
55
|
-
| **SEC** | View recent SEC filings for supported US equities |
|
|
56
|
-
| **Options** | View US equity options chains |
|
|
57
|
-
| **Notes** | Write and save markdown notes, stored locally |
|
|
58
|
-
| **AI** | Keep an Ask AI detail tab for tickers and create prompt-driven AI screener panes |
|
|
59
|
-
| **Alerts** | Create price trigger alerts with desktop notifications |
|
|
60
|
-
| **Compare Charts** | Compare multiple ticker charts overlaid on one shared chart |
|
|
61
|
-
| **Prediction Markets** | Browse Polymarket and Kalshi markets |
|
|
62
|
-
| **Correlation** | Compare ticker return correlations |
|
|
63
|
-
| **Portfolio Analytics** | Sharpe ratio, beta, and sector allocation |
|
|
64
|
-
| **Insider** | View recent insider transactions |
|
|
65
|
-
| **World Equity Indices** | Global equity index monitor grouped by region |
|
|
66
|
-
| **Market Movers** | Gainers, losers, most active, and trending tickers |
|
|
67
|
-
| **FX Cross Rates** | Currency cross-rate matrix for major FX pairs |
|
|
68
|
-
| **Sector Performance** | S&P 500 sector performance via ETF proxies |
|
|
69
|
-
| **Earnings Calendar** | Upcoming earnings dates and estimates |
|
|
70
|
-
| **Research** | Analyst ratings, corporate actions, and relative valuation |
|
|
75
|
+
Then run:
|
|
71
76
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|----------|-------------|
|
|
76
|
-
| **Gloomberb Cloud** | Real-time data (recommended, free) |
|
|
77
|
-
| **Yahoo Finance** | Delayed data, rate-limiting |
|
|
78
|
-
|
|
79
|
-
### Broker connectors
|
|
80
|
-
|
|
81
|
-
| Plugin | Description |
|
|
82
|
-
|--------|-------------|
|
|
83
|
-
| **IBKR** | Import positions from Flex Query or trade with Gateway API |
|
|
84
|
-
| **Manual Entry** | Manually add positions, saved locally |
|
|
77
|
+
```bash
|
|
78
|
+
gloomberb
|
|
79
|
+
```
|
|
85
80
|
|
|
86
|
-
|
|
81
|
+
For the best terminal experience, use a [Kitty](https://sw.kovidgoyal.net/kitty/)-compatible terminal such as Ghostty, Kitty, or WezTerm.
|
|
87
82
|
|
|
88
|
-
##
|
|
83
|
+
## CLI
|
|
89
84
|
|
|
90
85
|
Running `gloomberb` with no arguments launches the terminal UI. Use `gloomberb help` to see the full command list.
|
|
91
|
-
Feature-owned root commands such as `portfolio`, `watchlist`, and `predictions` are registered by their plugins rather than hardcoded in the main CLI switch.
|
|
92
86
|
|
|
93
87
|
| Command | Use |
|
|
94
88
|
|---------|-----|
|
|
95
89
|
| `gloomberb` | Launch the terminal UI |
|
|
96
|
-
| `gloomberb help` | Show all CLI commands
|
|
90
|
+
| `gloomberb help` | Show all CLI commands |
|
|
97
91
|
| `gloomberb search <query>` | Search tickers and company names |
|
|
98
92
|
| `gloomberb ticker <symbol>` | Show quote, ownership, and financials |
|
|
99
|
-
| `gloomberb portfolio [action]` |
|
|
100
|
-
| `gloomberb watchlist [action]` |
|
|
101
|
-
| `gloomberb predictions [...]` | Launch
|
|
93
|
+
| `gloomberb portfolio [action]` | Manage manual portfolios |
|
|
94
|
+
| `gloomberb watchlist [action]` | Manage watchlists |
|
|
95
|
+
| `gloomberb predictions [...]` | Launch Prediction Markets |
|
|
102
96
|
| `gloomberb plugins` | List installed plugins |
|
|
103
97
|
| `gloomberb install <user/repo>` | Install a plugin from GitHub |
|
|
104
98
|
| `gloomberb remove <name>` | Remove an installed plugin |
|
|
105
99
|
| `gloomberb update [name]` | Update plugins |
|
|
106
100
|
|
|
107
|
-
|
|
108
|
-
gloomberb
|
|
109
|
-
gloomberb help
|
|
110
|
-
gloomberb search NVDA
|
|
111
|
-
gloomberb ticker AAPL
|
|
112
|
-
gloomberb portfolio list
|
|
113
|
-
gloomberb portfolio show Research
|
|
114
|
-
gloomberb portfolio create Research
|
|
115
|
-
gloomberb portfolio add Research ASML
|
|
116
|
-
gloomberb portfolio position set Research NVDA 10 400
|
|
117
|
-
gloomberb watchlist list
|
|
118
|
-
gloomberb watchlist add Growth NVDA
|
|
119
|
-
gloomberb predictions kalshi macro top fed
|
|
120
|
-
gloomberb plugins
|
|
121
|
-
gloomberb update
|
|
122
|
-
```
|
|
101
|
+
## Plugins
|
|
123
102
|
|
|
124
|
-
|
|
103
|
+
Everything from the portfolio list to broker integrations is a plugin. Plugins can add panes, tabs, columns, command bar commands, CLI commands, status bar widgets, and data providers.
|
|
125
104
|
|
|
126
|
-
|
|
105
|
+
Core plugin areas include:
|
|
127
106
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
| `AI <prompt>` | AI Screener |
|
|
137
|
-
| `CMP <tickers>` | Comparison Chart |
|
|
138
|
-
| `PM <query>` | Prediction Markets |
|
|
139
|
-
| `CORR <tickers>` | Correlation Matrix |
|
|
140
|
-
| `PORT` | Portfolio Analytics |
|
|
141
|
-
| `ECON` | Economic Calendar |
|
|
142
|
-
| `WEI` | World Equity Indices |
|
|
143
|
-
| `MOST` | Market Movers |
|
|
144
|
-
| `TOP` | Top News |
|
|
145
|
-
| `N` | News Feed |
|
|
146
|
-
| `CN <ticker>` | Ticker News |
|
|
147
|
-
| `NI` | Industry News |
|
|
148
|
-
| `FIRST` | Breaking News |
|
|
149
|
-
| `ALRT` | Alerts |
|
|
150
|
-
| `FXC` | FX Cross Rates |
|
|
151
|
-
| `GC` | Yield Curve |
|
|
152
|
-
| `BI` | Sector Performance |
|
|
153
|
-
| `ERN` | Earnings Calendar |
|
|
154
|
-
| `OMON <ticker>` | Options |
|
|
155
|
-
| `HDS <ticker>` | Holders |
|
|
156
|
-
| `ANR <ticker>` | Analyst Research |
|
|
157
|
-
| `EVT <ticker>` | Corporate Actions |
|
|
158
|
-
| `SEC <ticker>` | SEC Filings |
|
|
159
|
-
| `INS <ticker>` | Insider Activity |
|
|
160
|
-
| `RV <tickers>` | Relative Valuation |
|
|
161
|
-
|
|
162
|
-
| Shortcut | Command |
|
|
163
|
-
|----------|---------|
|
|
164
|
-
| `GL` | Gridlock all windows |
|
|
165
|
-
| `SA <symbol condition price>` | Add Alert, e.g. `SA AMD above 200` |
|
|
166
|
-
|
|
167
|
-
## ⌨️ Keyboard Shortcuts
|
|
107
|
+
- Portfolios, watchlists, manual entry, and broker connections
|
|
108
|
+
- Ticker details, quotes, charts, options, filings, holders, insiders, and research
|
|
109
|
+
- News, market movers, global indices, sectors, FX, earnings, macro data, and yield curves
|
|
110
|
+
- Prediction markets, alerts, notes, chat, AI screeners, and external plugins
|
|
111
|
+
|
|
112
|
+
See [PLUGINS.md](PLUGINS.md) for the plugin API and the shared UI surface available through `gloomberb/components`.
|
|
113
|
+
|
|
114
|
+
## Keyboard
|
|
168
115
|
|
|
169
116
|
| Key | Action |
|
|
170
117
|
|-----|--------|
|
|
@@ -174,10 +121,10 @@ Open the command bar with `Ctrl+P` or `` ` ``, then type a shortcut or command n
|
|
|
174
121
|
| `Ctrl+Shift+D` | Dock or float focused pane |
|
|
175
122
|
| `Ctrl+Shift+L` | Layout actions |
|
|
176
123
|
| `Ctrl+Shift+G` | Gridlock all windows |
|
|
177
|
-
| `Tab` | Switch
|
|
178
|
-
| `j` / `k` | Navigate
|
|
124
|
+
| `Tab` | Switch panes |
|
|
125
|
+
| `j` / `k` | Navigate lists |
|
|
179
126
|
| `h` / `l` | Switch tabs |
|
|
180
|
-
| `m` | Cycle chart mode
|
|
127
|
+
| `m` | Cycle chart mode |
|
|
181
128
|
| `q` | Quit |
|
|
182
129
|
|
|
183
130
|
Desktop builds also accept the matching `Cmd` shortcuts on macOS, plus `Cmd/Ctrl+Shift+O` to pop out a pane.
|
|
@@ -188,4 +135,4 @@ MIT
|
|
|
188
135
|
|
|
189
136
|
## Credits
|
|
190
137
|
|
|
191
|
-
- [OpenTUI](https://opentui.com/) for the layout
|
|
138
|
+
- [OpenTUI](https://opentui.com/) for the layout engine
|
package/package.json
CHANGED
package/src/app.tsx
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
AppProvider,
|
|
7
7
|
getFocusedCollectionId,
|
|
8
8
|
getFocusedTickerSymbol,
|
|
9
|
+
syncConfigActiveLayoutState,
|
|
9
10
|
useAppDispatch,
|
|
10
11
|
useAppSelector,
|
|
11
12
|
useAppStateRef,
|
|
@@ -96,7 +97,8 @@ import {
|
|
|
96
97
|
} from "./components/command-bar/workflow-ops";
|
|
97
98
|
import { getPaneTemplateDisplayLabel } from "./components/command-bar/pane-template-display";
|
|
98
99
|
import {
|
|
99
|
-
|
|
100
|
+
findFixedTickerPaneForSymbol,
|
|
101
|
+
resolveTickerNavigationReplacementPane,
|
|
100
102
|
shouldFocusTickerNavigationTarget,
|
|
101
103
|
} from "./plugins/ticker-navigation";
|
|
102
104
|
import { debugLog } from "./utils/debug-log";
|
|
@@ -348,11 +350,13 @@ function AppInner({
|
|
|
348
350
|
if (!ensured) return null;
|
|
349
351
|
if (ensured.layout !== state.config.layout) {
|
|
350
352
|
dispatch({ type: "PUSH_LAYOUT_HISTORY" });
|
|
351
|
-
const layouts = state.config.layouts.map((savedLayout, index) => (
|
|
352
|
-
index === state.config.activeLayoutIndex ? { ...savedLayout, layout: ensured.layout } : savedLayout
|
|
353
|
-
));
|
|
354
353
|
dispatch({ type: "UPDATE_LAYOUT", layout: ensured.layout });
|
|
355
|
-
scheduleConfigSave(
|
|
354
|
+
scheduleConfigSave(syncConfigActiveLayoutState(
|
|
355
|
+
{ ...state.config, layout: ensured.layout },
|
|
356
|
+
state.paneState,
|
|
357
|
+
state.focusedPaneId,
|
|
358
|
+
state.activePanel,
|
|
359
|
+
));
|
|
356
360
|
}
|
|
357
361
|
return ensured.instance.instanceId;
|
|
358
362
|
})();
|
|
@@ -1078,14 +1082,16 @@ function AppInner({
|
|
|
1078
1082
|
const persistLayout = (layout: LayoutConfig, options?: { pushHistory?: boolean }) => {
|
|
1079
1083
|
const currentState = stateRef.current;
|
|
1080
1084
|
const normalizedLayout = normalizePaneLayout(layout);
|
|
1081
|
-
const layouts = currentState.config.layouts.map((savedLayout, index) => (
|
|
1082
|
-
index === currentState.config.activeLayoutIndex ? { ...savedLayout, layout: normalizedLayout } : savedLayout
|
|
1083
|
-
));
|
|
1084
1085
|
if (options?.pushHistory !== false) {
|
|
1085
1086
|
dispatch({ type: "PUSH_LAYOUT_HISTORY" });
|
|
1086
1087
|
}
|
|
1087
1088
|
dispatch({ type: "UPDATE_LAYOUT", layout: normalizedLayout });
|
|
1088
|
-
scheduleConfigSave(
|
|
1089
|
+
scheduleConfigSave(syncConfigActiveLayoutState(
|
|
1090
|
+
{ ...currentState.config, layout: normalizedLayout },
|
|
1091
|
+
currentState.paneState,
|
|
1092
|
+
currentState.focusedPaneId,
|
|
1093
|
+
currentState.activePanel,
|
|
1094
|
+
));
|
|
1089
1095
|
};
|
|
1090
1096
|
const resolvePanelForPane = useCallback((paneId: string, layout: LayoutConfig = state.config.layout): "left" | "right" => {
|
|
1091
1097
|
const instanceId = resolvePaneTarget(paneId, layout);
|
|
@@ -1109,6 +1115,16 @@ function AppInner({
|
|
|
1109
1115
|
dispatch({ type: "SET_ACTIVE_PANEL", panel: resolvePanelForPane(paneId, layout) });
|
|
1110
1116
|
dispatch({ type: "FOCUS_PANE", paneId });
|
|
1111
1117
|
}, [dispatch, resolvePanelForPane, state.config.layout]);
|
|
1118
|
+
const focusVisiblePane = (paneId: string, layout: LayoutConfig = state.config.layout) => {
|
|
1119
|
+
const nextLayout = layout.floating.some((entry) => entry.instanceId === paneId)
|
|
1120
|
+
? bringToFront(layout, paneId)
|
|
1121
|
+
: layout;
|
|
1122
|
+
|
|
1123
|
+
if (nextLayout !== state.config.layout) {
|
|
1124
|
+
persistLayout(nextLayout, { pushHistory: false });
|
|
1125
|
+
}
|
|
1126
|
+
activatePane(paneId, nextLayout);
|
|
1127
|
+
};
|
|
1112
1128
|
const placePaneInstance = useCallback((
|
|
1113
1129
|
instance: PaneInstanceConfig,
|
|
1114
1130
|
paneDef: NonNullable<ReturnType<typeof pluginRegistry.panes.get>>,
|
|
@@ -1341,20 +1357,20 @@ function AppInner({
|
|
|
1341
1357
|
return;
|
|
1342
1358
|
}
|
|
1343
1359
|
|
|
1344
|
-
|
|
1345
|
-
? bringToFront(state.config.layout, instanceId)
|
|
1346
|
-
: state.config.layout;
|
|
1347
|
-
|
|
1348
|
-
if (layout !== state.config.layout) {
|
|
1349
|
-
persistLayout(layout, { pushHistory: false });
|
|
1350
|
-
}
|
|
1351
|
-
activatePane(instanceId, layout);
|
|
1360
|
+
focusVisiblePane(instanceId);
|
|
1352
1361
|
};
|
|
1353
1362
|
pluginRegistry.pinTickerFn = (symbol, options) => {
|
|
1354
1363
|
if (isDetachedWindow) return;
|
|
1355
1364
|
const paneType = options?.paneType ?? "ticker-detail";
|
|
1356
1365
|
const paneDef = pluginRegistry.panes.get(paneType);
|
|
1357
1366
|
if (!paneDef) return;
|
|
1367
|
+
const existing = options?.forceNewPane
|
|
1368
|
+
? null
|
|
1369
|
+
: findFixedTickerPaneForSymbol(state.config.layout, paneType, symbol);
|
|
1370
|
+
if (existing) {
|
|
1371
|
+
focusVisiblePane(existing.instanceId);
|
|
1372
|
+
return;
|
|
1373
|
+
}
|
|
1358
1374
|
const instance = buildPaneInstance(paneType, {
|
|
1359
1375
|
title: symbol,
|
|
1360
1376
|
binding: { kind: "fixed", symbol },
|
|
@@ -1383,52 +1399,43 @@ function AppInner({
|
|
|
1383
1399
|
const sourcePaneId = options?.sourcePaneId ?? stateRef.current.focusedPaneId;
|
|
1384
1400
|
(async () => {
|
|
1385
1401
|
try {
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1402
|
+
// Resolve or create the ticker in the local database
|
|
1403
|
+
const resolved = await resolveTickerSearch({
|
|
1404
|
+
query: rawSymbol,
|
|
1405
|
+
activeTicker: null,
|
|
1406
|
+
tickers: stateRef.current.tickers,
|
|
1407
|
+
dataProvider,
|
|
1408
|
+
});
|
|
1393
1409
|
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1410
|
+
let symbol = rawSymbol;
|
|
1411
|
+
if (resolved?.kind === "local") {
|
|
1412
|
+
symbol = resolved.symbol;
|
|
1413
|
+
} else if (resolved?.kind === "provider" && resolved.result) {
|
|
1414
|
+
const { ticker, created } = await upsertTickerFromSearchResult(tickerRepository, resolved.result);
|
|
1415
|
+
symbol = ticker.metadata.ticker;
|
|
1416
|
+
dispatch({ type: "UPDATE_TICKER", ticker });
|
|
1417
|
+
if (created) {
|
|
1418
|
+
pluginRegistry.events.emit("ticker:added", { symbol, ticker });
|
|
1419
|
+
}
|
|
1403
1420
|
}
|
|
1404
|
-
}
|
|
1405
1421
|
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
}
|
|
1421
|
-
return;
|
|
1422
|
-
}
|
|
1423
|
-
activatePane(paneId, layout);
|
|
1424
|
-
};
|
|
1422
|
+
// Only actions that originate from a ticker detail replace that exact pane.
|
|
1423
|
+
// Everything else opens or focuses a fixed ticker detail for the symbol.
|
|
1424
|
+
const currentState = stateRef.current;
|
|
1425
|
+
const currentLayout = currentState.config.layout;
|
|
1426
|
+
const detailPane = resolveTickerNavigationReplacementPane(currentLayout, sourcePaneId);
|
|
1427
|
+
const focusIfStillOwned = (paneId: string, layout: LayoutConfig) => {
|
|
1428
|
+
if (!shouldFocusTickerNavigationTarget({
|
|
1429
|
+
sourcePaneId,
|
|
1430
|
+
currentFocusedPaneId: stateRef.current.focusedPaneId,
|
|
1431
|
+
targetPaneId: paneId,
|
|
1432
|
+
})) {
|
|
1433
|
+
return;
|
|
1434
|
+
}
|
|
1435
|
+
activatePane(paneId, layout);
|
|
1436
|
+
};
|
|
1425
1437
|
|
|
1426
|
-
|
|
1427
|
-
if (detailPane.binding?.kind === "follow") {
|
|
1428
|
-
const sourceId = detailPane.binding.sourceInstanceId;
|
|
1429
|
-
dispatch({ type: "UPDATE_PANE_STATE", paneId: sourceId, patch: { cursorSymbol: symbol } });
|
|
1430
|
-
focusIfStillOwned(detailPane.instanceId, currentLayout);
|
|
1431
|
-
} else {
|
|
1438
|
+
if (detailPane) {
|
|
1432
1439
|
const nextLayout = {
|
|
1433
1440
|
...currentLayout,
|
|
1434
1441
|
instances: currentLayout.instances.map((instance) => (
|
|
@@ -1439,14 +1446,13 @@ function AppInner({
|
|
|
1439
1446
|
};
|
|
1440
1447
|
persistLayout(nextLayout);
|
|
1441
1448
|
focusIfStillOwned(detailPane.instanceId, nextLayout);
|
|
1449
|
+
} else if (shouldFocusTickerNavigationTarget({
|
|
1450
|
+
sourcePaneId,
|
|
1451
|
+
currentFocusedPaneId: stateRef.current.focusedPaneId,
|
|
1452
|
+
targetPaneId: null,
|
|
1453
|
+
})) {
|
|
1454
|
+
pluginRegistry.pinTicker(symbol, { floating: false });
|
|
1442
1455
|
}
|
|
1443
|
-
} else if (shouldFocusTickerNavigationTarget({
|
|
1444
|
-
sourcePaneId,
|
|
1445
|
-
currentFocusedPaneId: stateRef.current.focusedPaneId,
|
|
1446
|
-
targetPaneId: null,
|
|
1447
|
-
})) {
|
|
1448
|
-
pluginRegistry.pinTicker(symbol, { floating: false });
|
|
1449
|
-
}
|
|
1450
1456
|
} catch (err) {
|
|
1451
1457
|
const message = err instanceof Error ? err.message : String(err);
|
|
1452
1458
|
pluginRegistry.notify({ body: `Failed to navigate to ${rawSymbol}: ${message}`, type: "error" });
|
|
@@ -1588,7 +1594,7 @@ function AppInner({
|
|
|
1588
1594
|
}
|
|
1589
1595
|
}
|
|
1590
1596
|
}
|
|
1591
|
-
});
|
|
1597
|
+
}, { phase: "before" });
|
|
1592
1598
|
|
|
1593
1599
|
if (desktopWindowBridge?.kind === "detached" && desktopWindowBridge.paneId) {
|
|
1594
1600
|
return (
|
|
@@ -43,6 +43,10 @@ function getRequestedRenderMode(mode?: ChartRenderMode, compact = false): ChartR
|
|
|
43
43
|
return mode ?? "area";
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
function isOhlcProjectionMode(mode: ChartRenderMode): boolean {
|
|
47
|
+
return mode === "candles" || mode === "ohlc" || mode === "hlc";
|
|
48
|
+
}
|
|
49
|
+
|
|
46
50
|
export function resolveRenderMode(
|
|
47
51
|
mode: ChartRenderMode | undefined,
|
|
48
52
|
chartWidth: number,
|
|
@@ -64,6 +68,7 @@ export function resolveRenderMode(
|
|
|
64
68
|
else if (chartWidth < 40) effectiveMode = "ohlc";
|
|
65
69
|
break;
|
|
66
70
|
case "ohlc":
|
|
71
|
+
case "hlc":
|
|
67
72
|
if (chartWidth < 28) effectiveMode = "line";
|
|
68
73
|
break;
|
|
69
74
|
}
|
|
@@ -328,11 +333,11 @@ export function projectChartData(
|
|
|
328
333
|
): ChartProjection {
|
|
329
334
|
const { requestedMode, effectiveMode, fallbackMode } = resolveRenderMode(mode, targetWidth, compact);
|
|
330
335
|
const ohlcBucketWidth = options.ohlcBucketWidth ?? targetWidth;
|
|
331
|
-
const projectionWidth = effectiveMode
|
|
336
|
+
const projectionWidth = isOhlcProjectionMode(effectiveMode)
|
|
332
337
|
? Math.max(Math.floor(ohlcBucketWidth / 2), 1)
|
|
333
338
|
: targetWidth;
|
|
334
339
|
|
|
335
|
-
const projectedPoints = effectiveMode
|
|
340
|
+
const projectedPoints = isOhlcProjectionMode(effectiveMode)
|
|
336
341
|
? bucketOhlcSeries(points, projectionWidth, options)
|
|
337
342
|
: projectCloseSeries(points, projectionWidth);
|
|
338
343
|
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
useAppStateRef,
|
|
5
5
|
usePaneInstance,
|
|
6
6
|
usePaneInstanceId,
|
|
7
|
+
syncConfigActiveLayoutState,
|
|
7
8
|
} from "../../state/app-context";
|
|
8
9
|
import { scheduleConfigSave } from "../../state/config-save-scheduler";
|
|
9
10
|
import type { ChartResolution, TimeRange } from "./chart-types";
|
|
@@ -24,10 +25,12 @@ export function usePersistChartControlSelection(rangePresetKey: string): (
|
|
|
24
25
|
[rangePresetKey]: range,
|
|
25
26
|
chartResolution: resolution,
|
|
26
27
|
});
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
const nextConfig = syncConfigActiveLayoutState(
|
|
29
|
+
{ ...currentState.config, layout },
|
|
30
|
+
currentState.paneState,
|
|
31
|
+
currentState.focusedPaneId,
|
|
32
|
+
currentState.activePanel,
|
|
33
|
+
);
|
|
31
34
|
dispatch({ type: "SET_CONFIG", config: nextConfig });
|
|
32
35
|
scheduleConfigSave(nextConfig);
|
|
33
36
|
};
|
|
@@ -278,15 +278,41 @@ function getBarHalfWidth(pointCount: number, bufWidth: number): number {
|
|
|
278
278
|
return Math.floor(barW / 2);
|
|
279
279
|
}
|
|
280
280
|
|
|
281
|
+
function isOhlcLikeMode(mode: ChartRenderMode): boolean {
|
|
282
|
+
return mode === "ohlc" || mode === "hlc";
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function isHighLowMode(mode: ChartRenderMode): boolean {
|
|
286
|
+
return mode === "candles" || isOhlcLikeMode(mode);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function getOhlcTickLength(pointCount: number, bufWidth: number): number {
|
|
290
|
+
if (pointCount <= 1) return 4;
|
|
291
|
+
const spacing = bufWidth / pointCount;
|
|
292
|
+
return Math.min(Math.max(Math.round(spacing * 0.45), 3), 5);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function getOhlcStemWidth(pointCount: number, bufWidth: number): number {
|
|
296
|
+
if (pointCount <= 1) return 3;
|
|
297
|
+
const spacing = bufWidth / pointCount;
|
|
298
|
+
return Math.min(Math.max(Math.round(spacing * 0.2), 2), 3);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function getOhlcHorizontalPad(pointCount: number, bufWidth: number): number {
|
|
302
|
+
const tickLen = getOhlcTickLength(pointCount, bufWidth);
|
|
303
|
+
const stemWidth = getOhlcStemWidth(pointCount, bufWidth);
|
|
304
|
+
return Math.max(tickLen - 1, Math.ceil((stemWidth - 1) / 2), 0);
|
|
305
|
+
}
|
|
306
|
+
|
|
281
307
|
function getDotX(index: number, pointCount: number, width: number, mode: ChartRenderMode): number {
|
|
282
308
|
switch (mode) {
|
|
283
309
|
case "candles": {
|
|
284
310
|
const pad = getCandleHalfWidth(pointCount, width);
|
|
285
311
|
return getSeriesPosition(index, pointCount, width, pad, pad);
|
|
286
312
|
}
|
|
287
|
-
case "ohlc":
|
|
288
|
-
|
|
289
|
-
const pad =
|
|
313
|
+
case "ohlc":
|
|
314
|
+
case "hlc": {
|
|
315
|
+
const pad = getOhlcHorizontalPad(pointCount, width);
|
|
290
316
|
return getSeriesPosition(index, pointCount, width, pad, pad);
|
|
291
317
|
}
|
|
292
318
|
default:
|
|
@@ -415,12 +441,16 @@ function drawOhlcChart(
|
|
|
415
441
|
palette: ResolvedChartPalette,
|
|
416
442
|
min: number,
|
|
417
443
|
max: number,
|
|
444
|
+
mode: Extract<ChartRenderMode, "ohlc" | "hlc">,
|
|
418
445
|
) {
|
|
419
|
-
const tickLen =
|
|
446
|
+
const tickLen = getOhlcTickLength(points.length, buf.width);
|
|
447
|
+
const stemWidth = getOhlcStemWidth(points.length, buf.width);
|
|
448
|
+
const stemLeft = Math.floor((stemWidth - 1) / 2);
|
|
449
|
+
const stemRight = Math.ceil((stemWidth - 1) / 2);
|
|
420
450
|
|
|
421
451
|
for (let i = 0; i < points.length; i++) {
|
|
422
452
|
const point = points[i]!;
|
|
423
|
-
const x = getDotX(i, points.length, buf.width,
|
|
453
|
+
const x = getDotX(i, points.length, buf.width, mode);
|
|
424
454
|
const highY = getScaledY(point.high, min, max, chartTop, chartBottom);
|
|
425
455
|
const lowY = getScaledY(point.low, min, max, chartTop, chartBottom);
|
|
426
456
|
const openY = getScaledY(point.open, min, max, chartTop, chartBottom);
|
|
@@ -428,14 +458,15 @@ function drawOhlcChart(
|
|
|
428
458
|
const isUp = point.close >= point.open;
|
|
429
459
|
const color = isUp ? palette.candleUp : palette.candleDown;
|
|
430
460
|
|
|
431
|
-
|
|
432
|
-
|
|
461
|
+
for (let dx = -stemLeft; dx <= stemRight; dx++) {
|
|
462
|
+
drawLine(buf, x + dx, highY, x + dx, lowY, color, LAYER_DATA);
|
|
463
|
+
}
|
|
433
464
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
465
|
+
if (mode === "ohlc") {
|
|
466
|
+
for (let dx = 0; dx < tickLen; dx++) {
|
|
467
|
+
setPixel(buf, x - dx, openY, color, LAYER_DATA);
|
|
468
|
+
}
|
|
437
469
|
}
|
|
438
|
-
// Close tick extends right
|
|
439
470
|
for (let dx = 0; dx < tickLen; dx++) {
|
|
440
471
|
setPixel(buf, x + dx, closeY, color, LAYER_DATA);
|
|
441
472
|
}
|
|
@@ -1222,10 +1253,10 @@ export function buildChartScene(
|
|
|
1222
1253
|
if (points.length === 0) return null;
|
|
1223
1254
|
|
|
1224
1255
|
const dimensions = normalizeRenderDimensions(opts);
|
|
1225
|
-
const dataMin = opts.mode
|
|
1256
|
+
const dataMin = isHighLowMode(opts.mode)
|
|
1226
1257
|
? Math.min(...points.map((point) => point.low))
|
|
1227
1258
|
: Math.min(...points.map((point) => point.close));
|
|
1228
|
-
const dataMax = opts.mode
|
|
1259
|
+
const dataMax = isHighLowMode(opts.mode)
|
|
1229
1260
|
? Math.max(...points.map((point) => point.high))
|
|
1230
1261
|
: Math.max(...points.map((point) => point.close));
|
|
1231
1262
|
const indicatorValues = getIndicatorOverlayValues(opts.indicators);
|
|
@@ -1394,7 +1425,10 @@ export function renderChart(
|
|
|
1394
1425
|
drawCandlestickChart(buf, points, 0, chartDotBottom, palette, min, max);
|
|
1395
1426
|
break;
|
|
1396
1427
|
case "ohlc":
|
|
1397
|
-
drawOhlcChart(buf, points, 0, chartDotBottom, palette, min, max);
|
|
1428
|
+
drawOhlcChart(buf, points, 0, chartDotBottom, palette, min, max, "ohlc");
|
|
1429
|
+
break;
|
|
1430
|
+
case "hlc":
|
|
1431
|
+
drawOhlcChart(buf, points, 0, chartDotBottom, palette, min, max, "hlc");
|
|
1398
1432
|
break;
|
|
1399
1433
|
}
|
|
1400
1434
|
|
|
@@ -1406,7 +1440,7 @@ export function renderChart(
|
|
|
1406
1440
|
volDotBottom,
|
|
1407
1441
|
palette.volumeUp,
|
|
1408
1442
|
palette.volumeDown,
|
|
1409
|
-
mode
|
|
1443
|
+
isHighLowMode(mode) ? "openClose" : "previousClose",
|
|
1410
1444
|
mode,
|
|
1411
1445
|
);
|
|
1412
1446
|
}
|