gloomberb 0.17.3 → 0.17.5
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/package.json +2 -1
- package/patches/electrobun@1.18.1.patch +80 -0
- package/src/api-client/chat.ts +28 -0
- package/src/api-client/data.ts +4 -1
- package/src/api-client/errors.ts +11 -1
- package/src/api-client/index.ts +4 -1
- package/src/api-client/normalizers.ts +21 -0
- package/src/api-client/research-activity.ts +10 -3
- package/src/api-client/socket.ts +27 -0
- package/src/app/pane-runtime/ticker-open-runtime.ts +2 -1
- package/src/app/runtime/startup.ts +4 -0
- package/src/app/runtime/update.ts +50 -3
- package/src/app.tsx +2 -0
- package/src/ask-gloom/pane-context.ts +11 -1
- package/src/cli/commands/overview.ts +2 -2
- package/src/cli/commands/system.ts +19 -1
- package/src/cli/glossary.ts +34 -4
- package/src/cli/index.ts +6 -0
- package/src/cli/pane-functions/function-help.ts +2 -2
- package/src/cli/pane-functions/market-session.ts +18 -3
- package/src/components/chart/composite/format.ts +7 -0
- package/src/components/chart/composite/legend.tsx +8 -11
- package/src/components/command-bar/commands/direct/index.ts +47 -0
- package/src/components/command-bar/commands/registry.ts +8 -0
- package/src/components/command-bar/panel/prompt-binding.ts +8 -0
- package/src/components/command-bar/routes/root/command-items.ts +16 -1
- package/src/components/command-bar/surface/index.tsx +2 -1
- package/src/components/command-bar/workflow/ops.ts +4 -3
- package/src/components/form-modal/confirm.tsx +1 -0
- package/src/components/form-modal/request.ts +2 -0
- package/src/components/index.ts +1 -1
- package/src/components/layout/detached-pane-shell.tsx +6 -1
- package/src/components/layout/pane/sidebar.tsx +24 -11
- package/src/components/layout/shell/index.tsx +5 -1
- package/src/components/layout/shell/menu.ts +8 -0
- package/src/components/layout/shell/pane/capture.ts +29 -12
- package/src/components/metric-treemap/heat-surface.tsx +76 -14
- package/src/components/metric-treemap/heat-tiles.ts +5 -3
- package/src/components/metric-treemap/index.tsx +1 -1
- package/src/components/ui/button.tsx +24 -9
- package/src/components/ui/confirm-dialog.tsx +30 -4
- package/src/components/ui/fields.tsx +5 -0
- package/src/components/ui/index.ts +1 -1
- package/src/components/ui/status.tsx +148 -5
- package/src/data/config/store/normalize.ts +8 -0
- package/src/i18n/es.ts +2 -0
- package/src/i18n/ja.ts +2 -0
- package/src/i18n/ko.ts +2 -0
- package/src/i18n/zh-cn.ts +2 -0
- package/src/i18n/zh-tw.ts +2 -0
- package/src/layout-marketplace/gallery-desktop.tsx +2 -2
- package/src/market-data/coordinator/events.ts +5 -1
- package/src/market-data/coordinator/index.ts +4 -0
- package/src/market-data/coordinator/quotes.ts +4 -0
- package/src/market-data/futures-board.ts +44 -1
- package/src/market-data/hooks.tsx +168 -29
- package/src/plugins/builtin/account-management/calendar-tab.tsx +1 -1
- package/src/plugins/builtin/alerts/cli.ts +72 -11
- package/src/plugins/builtin/awards/headless.ts +5 -1
- package/src/plugins/builtin/broker-manager/detail.tsx +2 -2
- package/src/plugins/builtin/broker-manager/model.ts +2 -2
- package/src/plugins/builtin/catalysts/headless.ts +5 -1
- package/src/plugins/builtin/chart-composer/headless.ts +2 -1
- package/src/plugins/builtin/chart-composer/pane.tsx +20 -2
- package/src/plugins/builtin/chat/content/footer.tsx +5 -2
- package/src/plugins/builtin/chat/controller/connections.ts +18 -0
- package/src/plugins/builtin/chat/controller/fetch.ts +5 -2
- package/src/plugins/builtin/chat/controller/index.ts +69 -21
- package/src/plugins/builtin/chat/controller/messages.ts +14 -2
- package/src/plugins/builtin/chat/controller/notifications.ts +132 -72
- package/src/plugins/builtin/chat/controller/realtime.ts +37 -1
- package/src/plugins/builtin/chat/controller/state.ts +15 -0
- package/src/plugins/builtin/chat/controller/unread-people.ts +240 -0
- package/src/plugins/builtin/chat/controller/view.ts +2 -0
- package/src/plugins/builtin/chat/index.tsx +1 -1
- package/src/plugins/builtin/chat/share/flow.tsx +18 -9
- package/src/plugins/builtin/chat/unread-badge.ts +38 -7
- package/src/plugins/builtin/cloud/askg/pane-attachment.ts +2 -3
- package/src/plugins/builtin/cot/model.ts +20 -0
- package/src/plugins/builtin/custom-view/pane.tsx +2 -2
- package/src/plugins/builtin/docs/pane.tsx +1 -1
- package/src/plugins/builtin/futures/index.tsx +2 -3
- package/src/plugins/builtin/futures/model.ts +4 -2
- package/src/plugins/builtin/futures/series.ts +3 -3
- package/src/plugins/builtin/futures-curve/headless.ts +13 -6
- package/src/plugins/builtin/futures-curve/index.tsx +3 -3
- package/src/plugins/builtin/futures-curve/model.ts +64 -4
- package/src/plugins/builtin/futures-curve/pane.tsx +11 -4
- package/src/plugins/builtin/jobs/pane.tsx +12 -9
- package/src/plugins/builtin/kelly-sizer/headless.ts +1 -1
- package/src/plugins/builtin/kelly-sizer/history.ts +5 -3
- package/src/plugins/builtin/kelly-sizer/pane.tsx +1 -1
- package/src/plugins/builtin/macro-day/release-table.ts +3 -3
- package/src/plugins/builtin/market-heatmap/index.tsx +6 -15
- package/src/plugins/builtin/mna/model.ts +2 -1
- package/src/plugins/builtin/news/headless.ts +4 -2
- package/src/plugins/builtin/options-positioning/model.ts +14 -0
- package/src/plugins/builtin/options-positioning/pane.tsx +17 -10
- package/src/plugins/builtin/perps/client.ts +77 -6
- package/src/plugins/builtin/perps/headless.ts +25 -14
- package/src/plugins/builtin/perps/index.tsx +1 -1
- package/src/plugins/builtin/perps/model.ts +32 -0
- package/src/plugins/builtin/perps/pane.tsx +5 -4
- package/src/plugins/builtin/plugin-marketplace/pane.tsx +1 -1
- package/src/plugins/builtin/rate-path/model.ts +1 -1
- package/src/plugins/builtin/rate-path/pane.tsx +2 -1
- package/src/plugins/builtin/research/equity-diagnostic-pane.tsx +2 -3
- package/src/plugins/builtin/research/event-sources.ts +1 -1
- package/src/plugins/builtin/research/relative-valuation-model.ts +6 -0
- package/src/plugins/builtin/research/relative-valuation-pane.tsx +3 -3
- package/src/plugins/builtin/sectors/client.ts +46 -19
- package/src/plugins/builtin/sectors/sector-model.ts +22 -10
- package/src/plugins/builtin/thirteenf/api.ts +60 -43
- package/src/plugins/builtin/thirteenf/data.ts +76 -35
- package/src/plugins/builtin/thirteenf/headless.ts +2 -0
- package/src/plugins/builtin/thirteenf/pane.tsx +10 -2
- package/src/plugins/builtin/yield-curve/curves.ts +15 -1
- package/src/plugins/builtin/yield-curve/headless.ts +17 -7
- package/src/plugins/builtin/yield-curve/history.ts +2 -0
- package/src/plugins/builtin/yield-curve/index.tsx +23 -18
- package/src/plugins/builtin/yield-curve/series.ts +19 -9
- package/src/plugins/builtin/yield-curve/treasury-data.ts +2 -0
- package/src/plugins/builtin/yield-curve/world.ts +4 -0
- package/src/plugins/registry/host-actions.ts +3 -0
- package/src/renderers/browser/app-services.ts +7 -1
- package/src/renderers/browser/config-host.ts +55 -2
- package/src/renderers/browser/first-screen.ts +193 -0
- package/src/renderers/browser/main.tsx +59 -7
- package/src/renderers/dom/build-assets.ts +17 -4
- package/src/renderers/dom/desktop/controls.tsx +4 -0
- package/src/renderers/electrobun/bun/application-menu/index.ts +1 -0
- package/src/renderers/electrobun/bun/desktop/backend-requests.ts +3 -0
- package/src/renderers/electrobun/bun/desktop/macos-dock-badge.ts +4 -3
- package/src/renderers/electrobun/bun/desktop/macos-dock-icon.ts +2 -3
- package/src/renderers/electrobun/bun/desktop/macos-objc.ts +17 -6
- package/src/renderers/electrobun/bun/desktop/update.ts +115 -12
- package/src/renderers/electrobun/shared/protocol.ts +3 -2
- package/src/renderers/electrobun/view/update-host.ts +2 -2
- package/src/shares/bug-report-dialog.tsx +162 -0
- package/src/shares/bug-report-flow.tsx +84 -0
- package/src/shares/bug-report.ts +42 -0
- package/src/shares/capture-flow.ts +19 -0
- package/src/shares/capture-preview.tsx +129 -0
- package/src/shares/chat.ts +10 -1
- package/src/shares/dialog.tsx +16 -91
- package/src/shares/flow.tsx +12 -16
- package/src/shares/personal-pane.ts +35 -1
- package/src/sources/gloomberb-cloud/normalizers.ts +12 -4
- package/src/telemetry/usage-counts.ts +16 -0
- package/src/tickers/open-target.ts +61 -11
- package/src/time-series/resolve.ts +26 -9
- package/src/time-series/spec.ts +8 -0
- package/src/types/config.ts +14 -0
- package/src/ui/resize-settle.ts +128 -0
- package/src/updater/channel.ts +51 -0
- package/src/updater/github-releases.ts +85 -16
- package/src/updater/index.ts +125 -16
- package/src/utils/calendar-date.ts +56 -0
- package/src/utils/datetime-format.ts +7 -1
- package/src/utils/decimal-comma.ts +31 -0
- package/src/utils/futures-generic.ts +25 -4
- package/src/utils/plugin-proxy-hosts.json +5 -0
- package/src/utils/semver.ts +47 -2
- package/src/utils/utc-time.ts +14 -3
package/src/i18n/ja.ts
CHANGED
|
@@ -70,6 +70,7 @@ export const ja: Record<string, string> = {
|
|
|
70
70
|
"Could not save this value.": "この値を保存できませんでした。",
|
|
71
71
|
"Could not apply that setting.": "その設定を適用できませんでした。",
|
|
72
72
|
"Enter {action} · Esc cancel": "Enter {action} · Esc キャンセル",
|
|
73
|
+
"y {action} · Enter or Esc {cancel}": "y {action} · Enter または Esc {cancel}",
|
|
73
74
|
"Published {date}": "{date} 公開",
|
|
74
75
|
"Language": "言語",
|
|
75
76
|
"Unsupported language": "対応していない言語",
|
|
@@ -152,6 +153,7 @@ export const ja: Record<string, string> = {
|
|
|
152
153
|
"Toggle Status Bar": "ステータスバーを切り替え",
|
|
153
154
|
"Toggle Value Flashing": "値の点滅を切り替え",
|
|
154
155
|
"Check for Updates": "アップデートを確認",
|
|
156
|
+
"Release Channel": "リリースチャネル",
|
|
155
157
|
"Crash Reports": "クラッシュレポート",
|
|
156
158
|
"Usage Counts": "利用回数",
|
|
157
159
|
"Change Theme": "テーマを変更",
|
package/src/i18n/ko.ts
CHANGED
|
@@ -68,6 +68,7 @@ export const ko: Record<string, string> = {
|
|
|
68
68
|
"Could not save this value.": "이 값을 저장할 수 없습니다.",
|
|
69
69
|
"Could not apply that setting.": "해당 설정을 적용할 수 없습니다.",
|
|
70
70
|
"Enter {action} · Esc cancel": "Enter {action} · Esc 취소",
|
|
71
|
+
"y {action} · Enter or Esc {cancel}": "y {action} · Enter 또는 Esc {cancel}",
|
|
71
72
|
"Published {date}": "게시일 {date}",
|
|
72
73
|
"Language": "언어",
|
|
73
74
|
"Unsupported language": "지원하지 않는 언어",
|
|
@@ -150,6 +151,7 @@ export const ko: Record<string, string> = {
|
|
|
150
151
|
"Toggle Status Bar": "상태 표시줄 전환",
|
|
151
152
|
"Toggle Value Flashing": "값 깜박임 전환",
|
|
152
153
|
"Check for Updates": "업데이트 확인",
|
|
154
|
+
"Release Channel": "릴리스 채널",
|
|
153
155
|
"Crash Reports": "충돌 보고",
|
|
154
156
|
"Usage Counts": "사용 횟수",
|
|
155
157
|
"Change Theme": "테마 변경",
|
package/src/i18n/zh-cn.ts
CHANGED
|
@@ -70,6 +70,7 @@ export const zhCN: Record<string, string> = {
|
|
|
70
70
|
"Could not save this value.": "无法保存该值。",
|
|
71
71
|
"Could not apply that setting.": "无法应用该设置。",
|
|
72
72
|
"Enter {action} · Esc cancel": "Enter {action} · Esc 取消",
|
|
73
|
+
"y {action} · Enter or Esc {cancel}": "y {action} · Enter 或 Esc {cancel}",
|
|
73
74
|
"Published {date}": "已发布 {date}",
|
|
74
75
|
"Language": "语言",
|
|
75
76
|
"Unsupported language": "不支持的语言",
|
|
@@ -152,6 +153,7 @@ export const zhCN: Record<string, string> = {
|
|
|
152
153
|
"Toggle Status Bar": "切换状态栏",
|
|
153
154
|
"Toggle Value Flashing": "切换数值闪烁",
|
|
154
155
|
"Check for Updates": "检查更新",
|
|
156
|
+
"Release Channel": "发布通道",
|
|
155
157
|
"Crash Reports": "崩溃报告",
|
|
156
158
|
"Usage Counts": "使用统计",
|
|
157
159
|
"Change Theme": "更换主题",
|
package/src/i18n/zh-tw.ts
CHANGED
|
@@ -70,6 +70,7 @@ export const zhTW: Record<string, string> = {
|
|
|
70
70
|
"Could not save this value.": "無法儲存該值。",
|
|
71
71
|
"Could not apply that setting.": "無法套用該設定。",
|
|
72
72
|
"Enter {action} · Esc cancel": "Enter {action} · Esc 取消",
|
|
73
|
+
"y {action} · Enter or Esc {cancel}": "y {action} · Enter 或 Esc {cancel}",
|
|
73
74
|
"Published {date}": "已發布 {date}",
|
|
74
75
|
"Language": "語言",
|
|
75
76
|
"Unsupported language": "不支援的語言",
|
|
@@ -152,6 +153,7 @@ export const zhTW: Record<string, string> = {
|
|
|
152
153
|
"Toggle Status Bar": "切換狀態列",
|
|
153
154
|
"Toggle Value Flashing": "切換數值閃爍",
|
|
154
155
|
"Check for Updates": "檢查更新",
|
|
156
|
+
"Release Channel": "發布通道",
|
|
155
157
|
"Crash Reports": "當機報告",
|
|
156
158
|
"Usage Counts": "使用統計",
|
|
157
159
|
"Change Theme": "更換主題",
|
|
@@ -193,7 +193,7 @@ function DiscoverStatus({ controller }: { controller: LayoutGalleryController })
|
|
|
193
193
|
<EmptyState
|
|
194
194
|
title={discover.state.error}
|
|
195
195
|
status="error"
|
|
196
|
-
|
|
196
|
+
retry={{ onRetry: discover.refresh }}
|
|
197
197
|
/>
|
|
198
198
|
</SidebarState>
|
|
199
199
|
);
|
|
@@ -219,7 +219,7 @@ function TeamStatus({ controller }: { controller: LayoutGalleryController }) {
|
|
|
219
219
|
<EmptyState
|
|
220
220
|
title={state.error}
|
|
221
221
|
status="error"
|
|
222
|
-
|
|
222
|
+
retry={{ onRetry: refresh }}
|
|
223
223
|
/>
|
|
224
224
|
</SidebarState>
|
|
225
225
|
);
|
|
@@ -67,10 +67,14 @@ export class MarketDataCoordinatorEvents {
|
|
|
67
67
|
return (this.keyListeners.get(key)?.size ?? 0) > 0;
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
getKeyVersion(key: string): number {
|
|
71
|
+
return this.keyVersions.get(key) ?? 0;
|
|
72
|
+
}
|
|
73
|
+
|
|
70
74
|
getKeysVersion(keys: readonly string[]): number {
|
|
71
75
|
let version = 0;
|
|
72
76
|
for (const key of new Set(keys)) {
|
|
73
|
-
version += this.
|
|
77
|
+
version += this.getKeyVersion(key);
|
|
74
78
|
}
|
|
75
79
|
return version;
|
|
76
80
|
}
|
|
@@ -218,6 +218,10 @@ export class MarketDataCoordinator {
|
|
|
218
218
|
return this.events.getVersion();
|
|
219
219
|
}
|
|
220
220
|
|
|
221
|
+
getKeyVersion(key: string): number {
|
|
222
|
+
return this.events.getKeyVersion(key);
|
|
223
|
+
}
|
|
224
|
+
|
|
221
225
|
getKeysVersion(keys: readonly string[]): number {
|
|
222
226
|
return this.events.getKeysVersion(keys);
|
|
223
227
|
}
|
|
@@ -95,6 +95,10 @@ export async function loadQuoteEntry({
|
|
|
95
95
|
resolveQuote,
|
|
96
96
|
}: LoadQuoteEntryOptions): Promise<QueryEntry<Quote>> {
|
|
97
97
|
const key = buildQuoteKey(instrument);
|
|
98
|
+
if (!options.forceRefresh) {
|
|
99
|
+
const current = quoteStore.get(key);
|
|
100
|
+
if (hasFreshQuoteEntry(current, instrument, SNAPSHOT_CACHE_TTL_MS)) return current;
|
|
101
|
+
}
|
|
98
102
|
const flightKey = options.forceRefresh ? `${key}|refresh` : key;
|
|
99
103
|
return runSingleFlight(flightKey, async () => {
|
|
100
104
|
quoteStore.update(key, loadingEntry);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export type FuturesSector =
|
|
2
2
|
| "equity-index"
|
|
3
|
+
| "equity-sectors"
|
|
3
4
|
| "rates"
|
|
4
5
|
| "energy"
|
|
5
6
|
| "metals"
|
|
@@ -38,7 +39,10 @@ export interface FuturesContract {
|
|
|
38
39
|
* unset: the security keeps the code and the future is its `=F` symbol.
|
|
39
40
|
* Checked with `gloomberb quote <code>` signed out on 2026-10-11: each of
|
|
40
41
|
* these was "Not a ticker", or for ZT and ZF a quote last updated in 2023 and
|
|
41
|
-
* 2021 that search no longer lists.
|
|
42
|
+
* 2021 that search no longer lists. The sector, index and energy rows were
|
|
43
|
+
* checked the same way on 2026-10-14: XAR is an ETF, XAU Goldmoney, SXI
|
|
44
|
+
* Standex, SXT Sensient, RX BioSyent, EMD a closed-end fund, SOX the PHLX
|
|
45
|
+
* index, and RS1, MES, MYM, M2K, MCL, MNG and HH are listed abroad.
|
|
42
46
|
*/
|
|
43
47
|
bareCode?: true;
|
|
44
48
|
}
|
|
@@ -58,6 +62,37 @@ export const FUTURES_CONTRACTS: FuturesContract[] = [
|
|
|
58
62
|
{ symbol: "NQ=F", code: "NQ", name: "E-Mini Nasdaq 100", sector: "equity-index", venue: "CME", tick: 0.25, bareCode: true },
|
|
59
63
|
{ symbol: "YM=F", code: "YM", name: "E-Mini Dow", sector: "equity-index", venue: "CBT", tick: 1, bareCode: true },
|
|
60
64
|
{ symbol: "RTY=F", code: "RTY", name: "E-Mini Russell 2000", sector: "equity-index", venue: "CME", tick: 0.1, bareCode: true },
|
|
65
|
+
{ symbol: "EMD=F", code: "EMD", name: "E-Mini S&P MidCap 400", sector: "equity-index", venue: "CME", tick: 0.1 },
|
|
66
|
+
{ symbol: "RS1=F", code: "RS1", name: "E-Mini Russell 1000", sector: "equity-index", venue: "CME", tick: 0.1 },
|
|
67
|
+
{ symbol: "NKD=F", code: "NKD", name: "Nikkei 225 (USD)", sector: "equity-index", venue: "CME", tick: 5, bareCode: true },
|
|
68
|
+
// Quoted in yen: the same index points, each worth 500 yen, so the server has no dollar terms for it.
|
|
69
|
+
{ symbol: "NIY=F", code: "NIY", name: "Nikkei 225 (Yen)", sector: "equity-index", venue: "CME", tick: 5, unit: "index points", bareCode: true },
|
|
70
|
+
{ symbol: "MES=F", code: "MES", name: "Micro E-Mini S&P 500", sector: "equity-index", venue: "CME", tick: 0.25 },
|
|
71
|
+
{ symbol: "MNQ=F", code: "MNQ", name: "Micro E-Mini Nasdaq 100", sector: "equity-index", venue: "CME", tick: 0.25, bareCode: true },
|
|
72
|
+
{ symbol: "MYM=F", code: "MYM", name: "Micro E-Mini Dow", sector: "equity-index", venue: "CBT", tick: 1 },
|
|
73
|
+
{ symbol: "M2K=F", code: "M2K", name: "Micro E-Mini Russell 2000", sector: "equity-index", venue: "CME", tick: 0.1 },
|
|
74
|
+
|
|
75
|
+
// CME's E-mini S&P Select Sector futures (XA*) and Select Industry futures
|
|
76
|
+
// (SX*), named by their S&P index: each sector followed by its industries in
|
|
77
|
+
// GICS order, then the Dow Jones U.S. Real Estate future on CBOT.
|
|
78
|
+
{ symbol: "XAE=F", code: "XAE", name: "S&P Energy", sector: "equity-sectors", venue: "CME", tick: 0.1, bareCode: true },
|
|
79
|
+
{ symbol: "SXO=F", code: "SXO", name: "S&P Oil & Gas E&P", sector: "equity-sectors", venue: "CME", tick: 0.5, bareCode: true },
|
|
80
|
+
{ symbol: "XAB=F", code: "XAB", name: "S&P Materials", sector: "equity-sectors", venue: "CME", tick: 0.1, bareCode: true },
|
|
81
|
+
{ symbol: "XAI=F", code: "XAI", name: "S&P Industrial", sector: "equity-sectors", venue: "CME", tick: 0.1, bareCode: true },
|
|
82
|
+
{ symbol: "XAY=F", code: "XAY", name: "S&P Consumer Discretionary", sector: "equity-sectors", venue: "CME", tick: 0.1, bareCode: true },
|
|
83
|
+
{ symbol: "SXR=F", code: "SXR", name: "S&P Retail", sector: "equity-sectors", venue: "CME", tick: 1, bareCode: true },
|
|
84
|
+
{ symbol: "XAP=F", code: "XAP", name: "S&P Consumer Staples", sector: "equity-sectors", venue: "CME", tick: 0.1, bareCode: true },
|
|
85
|
+
{ symbol: "XAV=F", code: "XAV", name: "S&P Health Care", sector: "equity-sectors", venue: "CME", tick: 0.1, bareCode: true },
|
|
86
|
+
{ symbol: "SXT=F", code: "SXT", name: "S&P Biotechnology", sector: "equity-sectors", venue: "CME", tick: 1 },
|
|
87
|
+
{ symbol: "XAF=F", code: "XAF", name: "S&P Financial", sector: "equity-sectors", venue: "CME", tick: 0.05, bareCode: true },
|
|
88
|
+
{ symbol: "SXB=F", code: "SXB", name: "S&P Regional Banks", sector: "equity-sectors", venue: "CME", tick: 0.25, bareCode: true },
|
|
89
|
+
{ symbol: "SXI=F", code: "SXI", name: "S&P Insurance", sector: "equity-sectors", venue: "CME", tick: 0.5 },
|
|
90
|
+
{ symbol: "XAK=F", code: "XAK", name: "S&P Technology", sector: "equity-sectors", venue: "CME", tick: 0.1, bareCode: true },
|
|
91
|
+
{ symbol: "SOX=F", code: "SOX", name: "PHLX Semiconductor", sector: "equity-sectors", venue: "CME", tick: 0.5 },
|
|
92
|
+
{ symbol: "XAZ=F", code: "XAZ", name: "S&P Communication Services", sector: "equity-sectors", venue: "CME", tick: 0.05, bareCode: true },
|
|
93
|
+
{ symbol: "XAU=F", code: "XAU", name: "S&P Utilities", sector: "equity-sectors", venue: "CME", tick: 0.1 },
|
|
94
|
+
{ symbol: "XAR=F", code: "XAR", name: "S&P Real Estate", sector: "equity-sectors", venue: "CME", tick: 0.05 },
|
|
95
|
+
{ symbol: "RX=F", code: "RX", name: "Dow Jones U.S. Real Estate", sector: "equity-sectors", venue: "CBT", tick: 0.1 },
|
|
61
96
|
|
|
62
97
|
{ symbol: "ZT=F", code: "ZT", name: "2-Year T-Note", sector: "rates", venue: "CBT", bareCode: true },
|
|
63
98
|
{ symbol: "ZF=F", code: "ZF", name: "5-Year T-Note", sector: "rates", venue: "CBT", bareCode: true },
|
|
@@ -72,6 +107,12 @@ export const FUTURES_CONTRACTS: FuturesContract[] = [
|
|
|
72
107
|
{ symbol: "TTF=F", code: "TTF", name: "Dutch TTF Gas", sector: "energy", venue: "NYM", everyMonth: true, tick: 0.001, unit: "EUR / MWh", bareCode: true },
|
|
73
108
|
{ symbol: "RB=F", code: "RB", name: "RBOB Gasoline", sector: "energy", venue: "NYM", everyMonth: true, tick: 0.0001 },
|
|
74
109
|
{ symbol: "HO=F", code: "HO", name: "Heating Oil", sector: "energy", venue: "NYM", everyMonth: true, tick: 0.0001 },
|
|
110
|
+
// Cash settled on the CL and NG final settlements.
|
|
111
|
+
{ symbol: "QM=F", code: "QM", name: "E-Mini Crude Oil", sector: "energy", venue: "NYM", everyMonth: true, tick: 0.025, bareCode: true },
|
|
112
|
+
{ symbol: "MCL=F", code: "MCL", name: "Micro WTI Crude Oil", sector: "energy", venue: "NYM", everyMonth: true, tick: 0.01 },
|
|
113
|
+
{ symbol: "QG=F", code: "QG", name: "E-Mini Natural Gas", sector: "energy", venue: "NYM", everyMonth: true, tick: 0.005, bareCode: true },
|
|
114
|
+
{ symbol: "MNG=F", code: "MNG", name: "Micro Henry Hub Natural Gas", sector: "energy", venue: "NYM", everyMonth: true, tick: 0.001 },
|
|
115
|
+
{ symbol: "HH=F", code: "HH", name: "Henry Hub Last Day Financial", sector: "energy", venue: "NYM", everyMonth: true, tick: 0.001 },
|
|
75
116
|
|
|
76
117
|
{ symbol: "GC=F", code: "GC", name: "Gold", sector: "metals", venue: "CMX", tick: 0.1 },
|
|
77
118
|
{ symbol: "SI=F", code: "SI", name: "Silver", sector: "metals", venue: "CMX", tick: 0.005 },
|
|
@@ -109,6 +150,7 @@ export const FUTURES_CONTRACTS: FuturesContract[] = [
|
|
|
109
150
|
|
|
110
151
|
export const FUTURES_SECTOR_LABELS: Record<FuturesSector, string> = {
|
|
111
152
|
"equity-index": "Equity Index",
|
|
153
|
+
"equity-sectors": "Equity Sectors",
|
|
112
154
|
rates: "Rates",
|
|
113
155
|
energy: "Energy",
|
|
114
156
|
metals: "Metals",
|
|
@@ -119,6 +161,7 @@ export const FUTURES_SECTOR_LABELS: Record<FuturesSector, string> = {
|
|
|
119
161
|
|
|
120
162
|
export const FUTURES_SECTOR_ORDER: FuturesSector[] = [
|
|
121
163
|
"equity-index",
|
|
164
|
+
"equity-sectors",
|
|
122
165
|
"rates",
|
|
123
166
|
"energy",
|
|
124
167
|
"metals",
|
|
@@ -135,6 +135,125 @@ function buildTickerFinancialsKeys(tickers: TickerRecord[], options: TickerInstr
|
|
|
135
135
|
return keys;
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
+
/**
|
|
139
|
+
* A new object so memo dependencies move, over the map that already holds
|
|
140
|
+
* every unchanged entry. Copying that map would visit every instrument.
|
|
141
|
+
*/
|
|
142
|
+
class RetainedMapView<K, V> extends Map<K, V> {
|
|
143
|
+
constructor(private readonly backing: Map<K, V>) {
|
|
144
|
+
super();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
override get size(): number {
|
|
148
|
+
return this.backing.size;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
override get(key: K): V | undefined {
|
|
152
|
+
return this.backing.get(key);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
override has(key: K): boolean {
|
|
156
|
+
return this.backing.has(key);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
override forEach(callback: (value: V, key: K, map: Map<K, V>) => void, thisArg?: unknown): void {
|
|
160
|
+
this.backing.forEach((value, key) => {
|
|
161
|
+
callback.call(thisArg, value, key, this);
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
override keys(): MapIterator<K> {
|
|
166
|
+
return this.backing.keys();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
override values(): MapIterator<V> {
|
|
170
|
+
return this.backing.values();
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
override entries(): MapIterator<[K, V]> {
|
|
174
|
+
return this.backing.entries();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
override [Symbol.iterator](): MapIterator<[K, V]> {
|
|
178
|
+
return this.backing[Symbol.iterator]();
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
interface PatchedMapSlot<K, V> {
|
|
183
|
+
key: K;
|
|
184
|
+
versionKeys: readonly string[];
|
|
185
|
+
read: () => V | null | undefined;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
interface PatchedMapState<K, V> {
|
|
189
|
+
signature: string;
|
|
190
|
+
coordinator: SharedCoordinator | null;
|
|
191
|
+
slots: Array<PatchedMapSlot<K, V>>;
|
|
192
|
+
versions: number[];
|
|
193
|
+
backing: Map<K, V>;
|
|
194
|
+
view: Map<K, V>;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function slotVersion(
|
|
198
|
+
slot: PatchedMapSlot<unknown, unknown>,
|
|
199
|
+
versionOf: ((key: string) => number) | null,
|
|
200
|
+
aggregate: number,
|
|
201
|
+
): number {
|
|
202
|
+
if (!versionOf) return aggregate;
|
|
203
|
+
let version = 0;
|
|
204
|
+
for (const key of slot.versionKeys) version += versionOf(key);
|
|
205
|
+
return version;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function usePatchedMap<K, V>(
|
|
209
|
+
signature: string,
|
|
210
|
+
coordinator: SharedCoordinator | null,
|
|
211
|
+
aggregateVersion: number,
|
|
212
|
+
versionOf: ((key: string) => number) | null,
|
|
213
|
+
createSlots: () => Array<PatchedMapSlot<K, V>>,
|
|
214
|
+
): Map<K, V> {
|
|
215
|
+
const stateRef = useRef<PatchedMapState<K, V> | null>(null);
|
|
216
|
+
let state = stateRef.current;
|
|
217
|
+
if (!state || state.signature !== signature || state.coordinator !== coordinator) {
|
|
218
|
+
const slots = createSlots();
|
|
219
|
+
const backing = new Map<K, V>();
|
|
220
|
+
const versions: number[] = [];
|
|
221
|
+
for (const slot of slots) {
|
|
222
|
+
const value = slot.read();
|
|
223
|
+
if (value != null) backing.set(slot.key, value);
|
|
224
|
+
versions.push(slotVersion(slot, versionOf, aggregateVersion));
|
|
225
|
+
}
|
|
226
|
+
const view = new RetainedMapView(backing);
|
|
227
|
+
state = { signature, coordinator, slots, versions, backing, view };
|
|
228
|
+
stateRef.current = state;
|
|
229
|
+
return view;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
let changed = false;
|
|
233
|
+
for (let index = 0; index < state.slots.length; index += 1) {
|
|
234
|
+
const slot = state.slots[index]!;
|
|
235
|
+
const version = slotVersion(slot, versionOf, aggregateVersion);
|
|
236
|
+
if (version === state.versions[index]) continue;
|
|
237
|
+
state.versions[index] = version;
|
|
238
|
+
const value = slot.read();
|
|
239
|
+
if (value == null) {
|
|
240
|
+
if (state.backing.delete(slot.key)) changed = true;
|
|
241
|
+
} else if (state.backing.get(slot.key) !== value) {
|
|
242
|
+
state.backing.set(slot.key, value);
|
|
243
|
+
changed = true;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
if (!changed) return state.view;
|
|
247
|
+
const view = new RetainedMapView(state.backing);
|
|
248
|
+
state.view = view;
|
|
249
|
+
return view;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function versionOfCoordinator(coordinator: SharedCoordinator | null): ((key: string) => number) | null {
|
|
253
|
+
if (!coordinator || typeof coordinator.getKeyVersion !== "function") return null;
|
|
254
|
+
return (key) => coordinator.getKeyVersion(key);
|
|
255
|
+
}
|
|
256
|
+
|
|
138
257
|
export function useTickerFinancialsMap(
|
|
139
258
|
tickers: TickerRecord[],
|
|
140
259
|
options: TickerInstrumentOptions = {},
|
|
@@ -145,19 +264,30 @@ export function useTickerFinancialsMap(
|
|
|
145
264
|
const subscriptionKeys = useMemo(() => buildTickerFinancialsKeys(tickers, options), [optionsKey, tickerKey]);
|
|
146
265
|
const keysVersion = useCoordinatorKeysVersion(subscriptionKeys);
|
|
147
266
|
|
|
148
|
-
return
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
267
|
+
return usePatchedMap(
|
|
268
|
+
`${tickerKey}\0${optionsKey}`,
|
|
269
|
+
coordinator,
|
|
270
|
+
keysVersion,
|
|
271
|
+
versionOfCoordinator(coordinator),
|
|
272
|
+
() => {
|
|
273
|
+
if (!coordinator) return [];
|
|
274
|
+
const slots: Array<PatchedMapSlot<string, TickerFinancials>> = [];
|
|
275
|
+
for (const ticker of tickers) {
|
|
276
|
+
const instrument = instrumentFromTicker(ticker, ticker.metadata.ticker, options);
|
|
277
|
+
if (!instrument) continue;
|
|
278
|
+
slots.push({
|
|
279
|
+
key: ticker.metadata.ticker,
|
|
280
|
+
versionKeys: [
|
|
281
|
+
buildSnapshotKey(instrument),
|
|
282
|
+
buildQuoteKey(instrument),
|
|
283
|
+
buildChartKey(createBaselineChartRequest(instrument)),
|
|
284
|
+
],
|
|
285
|
+
read: () => coordinator.getTickerFinancialsSync(instrument),
|
|
286
|
+
});
|
|
157
287
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
288
|
+
return slots;
|
|
289
|
+
},
|
|
290
|
+
);
|
|
161
291
|
}
|
|
162
292
|
|
|
163
293
|
export function useQuoteEntry(symbol: string | null | undefined, ticker: TickerRecord | null | undefined): QueryEntry<Quote> | null {
|
|
@@ -197,15 +327,23 @@ export function useQuoteEntries(
|
|
|
197
327
|
const keysVersion = useCoordinatorKeysVersion(keys);
|
|
198
328
|
const coordinator = getSharedMarketDataCoordinator();
|
|
199
329
|
|
|
200
|
-
return
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
]
|
|
207
|
-
|
|
208
|
-
|
|
330
|
+
return usePatchedMap(
|
|
331
|
+
instrumentKey,
|
|
332
|
+
coordinator,
|
|
333
|
+
keysVersion,
|
|
334
|
+
versionOfCoordinator(coordinator),
|
|
335
|
+
() => {
|
|
336
|
+
if (!coordinator) return [];
|
|
337
|
+
return instruments.map((instrument) => {
|
|
338
|
+
const key = buildQuoteKey(instrument);
|
|
339
|
+
return {
|
|
340
|
+
key,
|
|
341
|
+
versionKeys: [key],
|
|
342
|
+
read: () => coordinator.getQuoteEntry(instrument),
|
|
343
|
+
};
|
|
344
|
+
});
|
|
345
|
+
},
|
|
346
|
+
);
|
|
209
347
|
}
|
|
210
348
|
|
|
211
349
|
export function useChartQueries(
|
|
@@ -219,7 +357,8 @@ export function useChartQueries(
|
|
|
219
357
|
const keysVersion = useCoordinatorKeysVersion(keys);
|
|
220
358
|
const coordinator = getSharedMarketDataCoordinator();
|
|
221
359
|
const appActive = useAppVisible();
|
|
222
|
-
const
|
|
360
|
+
const paneVisible = usePaneVisible();
|
|
361
|
+
const wasPaneVisibleRef = useRef(paneVisible);
|
|
223
362
|
const entries = useMemo(() => (
|
|
224
363
|
coordinator
|
|
225
364
|
? requests.map((request) => [buildChartKey(request), coordinator.getChartEntry(request)] as const)
|
|
@@ -229,15 +368,15 @@ export function useChartQueries(
|
|
|
229
368
|
useEffect(() => {
|
|
230
369
|
const coordinator = getSharedMarketDataCoordinator();
|
|
231
370
|
if (!coordinator) {
|
|
232
|
-
|
|
371
|
+
wasPaneVisibleRef.current = paneVisible;
|
|
233
372
|
return;
|
|
234
373
|
}
|
|
235
374
|
if (!appActive) {
|
|
236
|
-
|
|
375
|
+
wasPaneVisibleRef.current = false;
|
|
237
376
|
return;
|
|
238
377
|
}
|
|
239
|
-
const forceRefresh = refreshIntervalMs > 0 && !
|
|
240
|
-
|
|
378
|
+
const forceRefresh = refreshIntervalMs > 0 && paneVisible && !wasPaneVisibleRef.current;
|
|
379
|
+
wasPaneVisibleRef.current = paneVisible;
|
|
241
380
|
const loadRequests = () => {
|
|
242
381
|
loadChartRequests(coordinator, requests, { forceRefresh });
|
|
243
382
|
};
|
|
@@ -247,17 +386,17 @@ export function useChartQueries(
|
|
|
247
386
|
}
|
|
248
387
|
const timeout = setTimeout(loadRequests, debounceMs);
|
|
249
388
|
return () => clearTimeout(timeout);
|
|
250
|
-
}, [appActive, debounceMs, refreshIntervalMs, requestKey]);
|
|
389
|
+
}, [appActive, debounceMs, paneVisible, refreshIntervalMs, requestKey]);
|
|
251
390
|
|
|
252
391
|
useEffect(() => {
|
|
253
392
|
const coordinator = getSharedMarketDataCoordinator();
|
|
254
|
-
if (!coordinator || !
|
|
393
|
+
if (!coordinator || !paneVisible || refreshIntervalMs <= 0 || requests.length === 0) return;
|
|
255
394
|
|
|
256
395
|
const interval = setInterval(() => {
|
|
257
396
|
loadChartRequests(coordinator, requests, { forceRefresh: true });
|
|
258
397
|
}, refreshIntervalMs);
|
|
259
398
|
return () => clearInterval(interval);
|
|
260
|
-
}, [
|
|
399
|
+
}, [paneVisible, refreshIntervalMs, requestKey]);
|
|
261
400
|
|
|
262
401
|
return useMemo(() => new Map(entries), [entries]);
|
|
263
402
|
}
|
|
@@ -165,7 +165,7 @@ export function CalendarAccountTab({ width, sessionMarker }: { width: number; se
|
|
|
165
165
|
<EmptyState
|
|
166
166
|
status="error"
|
|
167
167
|
title={loadError}
|
|
168
|
-
|
|
168
|
+
retry={{ onRetry: () => load(), pending: loading }}
|
|
169
169
|
/>
|
|
170
170
|
);
|
|
171
171
|
}
|
|
@@ -2,12 +2,14 @@ import { withConfigData, withMarketData } from "../../../cli/scoped-context";
|
|
|
2
2
|
import { CLI_COMMAND_GROUPS } from "../../../cli/help";
|
|
3
3
|
import { dryRunNote } from "../../../cli/helpers";
|
|
4
4
|
import { requireArg, takeOption } from "../../../cli/commands/command-utils";
|
|
5
|
-
import { EXCHANGE_OPTION, listingReadNotes, requireListingArg } from "../../../cli/listing-arg";
|
|
5
|
+
import { EXCHANGE_OPTION, listingReadNotes, requireListingArg, type ListingArg } from "../../../cli/listing-arg";
|
|
6
6
|
import { saveConfig } from "../../../data/config/store";
|
|
7
7
|
import type { AppConfig } from "../../../types/config";
|
|
8
|
-
import type { CliCommandDef } from "../../../types/plugin";
|
|
8
|
+
import type { CliCommandContext, CliCommandDef } from "../../../types/plugin";
|
|
9
|
+
import { getActiveQuoteDisplay } from "../../../market-data/market/status";
|
|
10
|
+
import { settleWithin } from "../../../utils/async-deadline";
|
|
9
11
|
import { cliStyles } from "../../../utils/cli-output";
|
|
10
|
-
import { ambiguousNumberMessage, parsePastedNumber } from "../../../utils/decimal-comma";
|
|
12
|
+
import { ambiguousNumberMessage, meantThousandsDot, parsePastedNumber, thousandsDotReadings, type ThousandsDotReadings } from "../../../utils/decimal-comma";
|
|
11
13
|
import { newYorkToday } from "../earnings/board-model";
|
|
12
14
|
import { createAlert, deserializeAlerts, rearmAlert, serializeAlerts } from "./alert-engine";
|
|
13
15
|
import { ALERTS_KEY } from "./constants";
|
|
@@ -43,20 +45,28 @@ const DAYS_RULE = `--days takes a whole number of days from 1 to ${MAX_LEAD_DAYS
|
|
|
43
45
|
|
|
44
46
|
/** What `alerts add` was asked for, before any listing or date is looked up. */
|
|
45
47
|
export type AlertAddRequest =
|
|
46
|
-
| {
|
|
48
|
+
| {
|
|
49
|
+
kind: "price"; symbol: string; condition: AlertCondition; price: number;
|
|
50
|
+
/** A price typed as 81.000, which the last price may show was 81000 with a dot between thousands. */
|
|
51
|
+
thousandsDot?: ThousandsDotReadings & { text: string };
|
|
52
|
+
}
|
|
47
53
|
| { kind: "earnings"; symbol: string; leadDays: number }
|
|
48
54
|
| { kind: "macro"; event: MacroAlertEvent; leadDays: number };
|
|
49
55
|
|
|
50
56
|
const CONDITIONS: readonly AlertCondition[] = ["above", "below", "crosses"];
|
|
51
57
|
|
|
58
|
+
/** The first word of a price typed with a space between thousands and no quotes (`81 000`): one to three digits. */
|
|
59
|
+
const LEADING_GROUP = /^[+-]?\d{1,3}$/;
|
|
60
|
+
|
|
52
61
|
/**
|
|
53
62
|
* Reads `alerts add` arguments: `<symbol> above 250`, `<symbol> earnings`, or
|
|
54
63
|
* `CPI` / `FOMC`, with `--days` (already taken out of `args`, `null` when the
|
|
55
|
-
* flag came without a value) for the event forms only
|
|
56
|
-
* names what the command
|
|
64
|
+
* flag came without a value) for the event forms only, and `--exchange`, which
|
|
65
|
+
* a command to type again keeps. Throws a message that names what the command
|
|
66
|
+
* takes.
|
|
57
67
|
*/
|
|
58
|
-
export function parseAlertAddArgs(args: readonly string[], days: string | null | undefined): AlertAddRequest {
|
|
59
|
-
const [first, second, third] = args;
|
|
68
|
+
export function parseAlertAddArgs(args: readonly string[], days: string | null | undefined, exchange?: string | null): AlertAddRequest {
|
|
69
|
+
const [first, second, third, ...rest] = args;
|
|
60
70
|
if (!first) throw new Error(ADD_USAGE);
|
|
61
71
|
const leadDays = () => {
|
|
62
72
|
const parsed = days === null ? null : parseLeadDays(days);
|
|
@@ -83,11 +93,57 @@ export function parseAlertAddArgs(args: readonly string[], days: string | null |
|
|
|
83
93
|
}
|
|
84
94
|
if (days !== undefined) throw new Error("--days is for earnings and CPI or FOMC alerts, not price alerts.");
|
|
85
95
|
if (third === undefined) throw new Error(ADD_USAGE);
|
|
96
|
+
const retype = (price: number) => retypeCommand(first, condition, price, exchange);
|
|
97
|
+
// `81 000` without quotes arrives as 81 and a stray 000; neither reading is taken for granted.
|
|
98
|
+
const spaced = rest.length > 0 && LEADING_GROUP.test(third) ? parsePastedNumber([third, ...rest].join(" ")) : null;
|
|
99
|
+
if (spaced?.kind === "number") {
|
|
100
|
+
const typed = Number(third);
|
|
101
|
+
throw new Error(`${[third, ...rest].join(" ")} could be ${spaced.value} (thousands) or ${typed} and a stray ${rest.join(" ")}; type ${retype(spaced.value)} or ${retype(typed)}`);
|
|
102
|
+
}
|
|
86
103
|
// A price pasted from a sheet (1.350,5, 1,350.5, 1 350,5) reads as it is written; `1,350` could be either, so it asks.
|
|
87
104
|
const pasted = parsePastedNumber(third);
|
|
88
105
|
if (pasted.kind === "ambiguous") throw new Error(ambiguousNumberMessage(third, pasted));
|
|
89
106
|
if (pasted.kind === "invalid") throw new Error("Alert price must be a finite number.");
|
|
90
|
-
|
|
107
|
+
if (rest.length > 0) throw new Error(`A price alert takes one price, not "${rest.join(" ")}" after ${third}; type ${retype(pasted.value)}`);
|
|
108
|
+
const thousandsDot = thousandsDotReadings(third);
|
|
109
|
+
return { kind: "price", symbol: first, condition, price: pasted.value, ...(thousandsDot ? { thousandsDot: { ...thousandsDot, text: third.trim() } } : {}) };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** `alerts add` as typed, with another price: what an error tells someone to type. */
|
|
113
|
+
function retypeCommand(symbol: string, condition: AlertCondition, price: number, exchange: string | null | undefined): string {
|
|
114
|
+
return `gloomberb alerts add ${symbol} ${condition} ${price}${exchange ? ` --exchange ${exchange}` : ""}`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const LAST_PRICE_TIMEOUT_MS = 10_000;
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* The listing's last price, as the alert will check it, for telling 81.000
|
|
121
|
+
* typed for 81000 from 81; null when there is none, and the price is then
|
|
122
|
+
* read as written.
|
|
123
|
+
*/
|
|
124
|
+
async function lastPrice(listing: ListingArg, ctx: Pick<CliCommandContext, "initMarketData">): Promise<number | null> {
|
|
125
|
+
const quote = await settleWithin(
|
|
126
|
+
withMarketData(ctx, (market) => market.dataProvider.getQuote(listing.key.toUpperCase(), listing.exchange)),
|
|
127
|
+
LAST_PRICE_TIMEOUT_MS,
|
|
128
|
+
);
|
|
129
|
+
const price = quote ? getActiveQuoteDisplay(quote)?.price ?? quote.price : null;
|
|
130
|
+
return price != null && Number.isFinite(price) && price !== 0 ? price : null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Asks which price `81.000` meant when the last price shows it was a dot
|
|
135
|
+
* between thousands: both readings, and the command to type for each.
|
|
136
|
+
*/
|
|
137
|
+
export function thousandsDotMessage(
|
|
138
|
+
request: Extract<AlertAddRequest, { kind: "price" }>,
|
|
139
|
+
listing: string,
|
|
140
|
+
last: number | null,
|
|
141
|
+
exchange?: string | null,
|
|
142
|
+
): string | null {
|
|
143
|
+
const dot = request.thousandsDot;
|
|
144
|
+
if (!dot || !meantThousandsDot(dot, last)) return null;
|
|
145
|
+
const retype = (price: number) => retypeCommand(request.symbol, request.condition, price, exchange);
|
|
146
|
+
return `${dot.text} could be ${dot.thousands} (thousands) or ${dot.decimal} (decimal point), and ${listing.toUpperCase()} last traded at ${Number(last!.toFixed(2))}; type ${retype(dot.thousands)} or ${retype(dot.decimal)}`;
|
|
91
147
|
}
|
|
92
148
|
|
|
93
149
|
/** One row of `alerts list`: a price alert as stored, or an event alert with its next date. */
|
|
@@ -222,13 +278,17 @@ export const alertsCliCommand: CliCommandDef = {
|
|
|
222
278
|
|
|
223
279
|
const parseAdd = (): AlertAddRequest => {
|
|
224
280
|
try {
|
|
225
|
-
return parseAlertAddArgs(args.slice(1), days);
|
|
281
|
+
return parseAlertAddArgs(args.slice(1), days, exchangeOption);
|
|
226
282
|
} catch (error) {
|
|
227
283
|
return ctx.fail(error instanceof Error ? error.message : String(error));
|
|
228
284
|
}
|
|
229
285
|
};
|
|
230
286
|
const request = action === "add" ? parseAdd() : null;
|
|
231
287
|
const priceListing = request?.kind === "price" ? await requireListingArg(request.symbol, exchangeOption, ctx) : null;
|
|
288
|
+
if (request?.kind === "price" && request.thousandsDot && priceListing) {
|
|
289
|
+
const question = thousandsDotMessage(request, priceListing.key, await lastPrice(priceListing, ctx), exchangeOption);
|
|
290
|
+
if (question) ctx.fail(question);
|
|
291
|
+
}
|
|
232
292
|
// What a typed listing was read as (7203:TSE as 7203:JPX), said beside what was added.
|
|
233
293
|
let read = priceListing ? listingReadNotes(priceListing) : [];
|
|
234
294
|
let newEventRule: EventAlertRule | null = null;
|
|
@@ -309,7 +369,8 @@ export const alertsCliCommand: CliCommandDef = {
|
|
|
309
369
|
const alert = createAlert(symbol, condition, price, priceListing?.exchange);
|
|
310
370
|
await save({ alerts: [...alerts, alert] });
|
|
311
371
|
ctx.printResult({ data: { changed: !ctx.cliOptions.dryRun, dryRun: ctx.cliOptions.dryRun, alert }, ...(read.length > 0 ? { warnings: read } : {}) }, {
|
|
312
|
-
|
|
372
|
+
// No full stop after the price, which would read as part of it (below 81.).
|
|
373
|
+
text: (data) => `Added alert ${data.alert.id}: ${symbol} ${condition} ${price}${dryRunNote(data.dryRun)}`,
|
|
313
374
|
});
|
|
314
375
|
return;
|
|
315
376
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { AwardFilter, AwardRow, AwardType } from "../../../api-client/awards";
|
|
2
2
|
import { notATickerSymbol } from "../../../sources/provider-errors";
|
|
3
3
|
import type { HeadlessBundleSection, HeadlessPaneColumn, HeadlessPaneContext, HeadlessPaneDefinition, HeadlessPaneRow } from "../../../types/headless";
|
|
4
|
+
import { isoDateOption } from "../../../utils/calendar-date";
|
|
4
5
|
import { resolveHeadlessInstrument } from "../shared/headless-market-data";
|
|
5
6
|
import { fetchAward, fetchAwards } from "./client";
|
|
6
7
|
import { AWARD_TABS, awardScope, awardsPreviewNote, awardTypeLabel, futureAwardDatesNote, money } from "./model";
|
|
@@ -85,7 +86,10 @@ export const awardsHeadless: HeadlessPaneDefinition<"bundle"> = {
|
|
|
85
86
|
options: [
|
|
86
87
|
{ key: "tab", type: "enum", settingKey: "tab", defaultValue: "feed", values: [...AWARD_TABS.map(({ value }) => ({ value })), { value: "all" }], description: "Feed, company history, agencies, sectors, events, or all sections." },
|
|
87
88
|
{ key: "view", type: "enum", settingKey: "leaderView", defaultValue: "companies", values: [{ value: "companies" }, { value: "sectors" }], description: "Sectors tab: company leaders or sector totals." },
|
|
88
|
-
...
|
|
89
|
+
...FILTER_KEYS.map((key) => key === "from" || key === "to"
|
|
90
|
+
? { key, type: "string" as const, settingKey: key, placeholder: "YYYY-MM-DD", description: `${key === "from" ? "Start" : "End"} of the date range. Dates use YYYY-MM-DD.`,
|
|
91
|
+
normalize: (value: string) => isoDateOption(value, key) }
|
|
92
|
+
: { key, type: "string" as const, settingKey: key, description: `Filter by ${key}.` }),
|
|
89
93
|
{ key: "type", type: "enum", settingKey: "awardType", defaultValue: "prime", values: [{ value: "prime" }, { value: "subaward" }, { value: "notice" }, { value: "modification" }], description: "Keep record types separate." },
|
|
90
94
|
{ key: "cursor", type: "string", description: "Continue after the previous response's nextCursor." },
|
|
91
95
|
{ key: "limit", type: "integer", minimum: 1, maximum: 200, defaultValue: 100, description: "Maximum feed rows in this response." },
|
|
@@ -91,8 +91,8 @@ function BrokerConfigFieldEditor({
|
|
|
91
91
|
function accountDetail(account: BrokerAccount): string {
|
|
92
92
|
const parts = [
|
|
93
93
|
account.accountId,
|
|
94
|
-
account.netLiquidation != null ? tf("{value} net liq", { value: formatCurrency(account.netLiquidation, account.currency || "
|
|
95
|
-
account.buyingPower != null ? tf("{value} buying power", { value: formatCurrency(account.buyingPower, account.currency || "
|
|
94
|
+
account.netLiquidation != null ? tf("{value} net liq", { value: formatCurrency(account.netLiquidation, account.currency || "") }) : null,
|
|
95
|
+
account.buyingPower != null ? tf("{value} buying power", { value: formatCurrency(account.buyingPower, account.currency || "") }) : null,
|
|
96
96
|
];
|
|
97
97
|
return parts.filter(Boolean).join(" · ");
|
|
98
98
|
}
|