gloomberb 0.4.0 → 0.5.0
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 +2 -2
- package/package.json +1 -1
- package/src/app.tsx +184 -69
- package/src/cli/commands/ticker.ts +45 -15
- package/src/cli/helpers.ts +4 -2
- package/src/cli/registry.test.ts +42 -35
- package/src/cli.test.ts +4 -2
- package/src/components/chart/chart-control-hint.tsx +39 -0
- package/src/components/chart/chart-controller.test.ts +110 -0
- package/src/components/chart/chart-controller.ts +364 -2
- package/src/components/chart/chart-data.ts +177 -29
- package/src/components/chart/chart-indicator-selector.tsx +53 -0
- package/src/components/chart/chart-pane-settings.ts +20 -0
- package/src/components/chart/chart-renderer.ts +220 -30
- package/src/components/chart/chart-resolution.test.ts +67 -0
- package/src/components/chart/chart-resolution.ts +74 -4
- package/src/components/chart/chart-scroll.test.ts +69 -0
- package/src/components/chart/chart-scroll.ts +104 -0
- package/src/components/chart/chart-types.ts +12 -0
- package/src/components/chart/chart-viewport.test.ts +5 -1
- package/src/components/chart/chart-viewport.ts +1 -1
- package/src/components/chart/chart.test.ts +249 -10
- package/src/components/chart/comparison-chart-renderer.test.ts +71 -1
- package/src/components/chart/comparison-chart-renderer.ts +40 -3
- package/src/components/chart/comparison-stock-chart.tsx +218 -123
- package/src/components/chart/indicators/bands.test.ts +50 -0
- package/src/components/chart/indicators/bands.ts +29 -0
- package/src/components/chart/indicators/moving-averages.test.ts +54 -0
- package/src/components/chart/indicators/moving-averages.ts +42 -0
- package/src/components/chart/indicators/options.test.ts +32 -0
- package/src/components/chart/indicators/options.ts +107 -0
- package/src/components/chart/indicators/oscillators.test.ts +79 -0
- package/src/components/chart/indicators/oscillators.ts +92 -0
- package/src/components/chart/indicators/types.ts +36 -0
- package/src/components/chart/native/chart-rasterizer.ts +51 -0
- package/src/components/chart/native/kitty.test.ts +68 -0
- package/src/components/chart/stock-chart-renderer-switch.test.tsx +740 -9
- package/src/components/chart/stock-chart.test.ts +239 -0
- package/src/components/chart/stock-chart.tsx +2257 -372
- package/src/components/command-bar/command-bar.test.tsx +337 -5
- package/src/components/command-bar/command-bar.tsx +296 -120
- package/src/components/command-bar/helpers.ts +1 -1
- package/src/components/command-bar/root-shortcuts.ts +33 -4
- package/src/components/command-bar/view-model.test.ts +3 -3
- package/src/components/command-bar/view-model.ts +4 -4
- package/src/components/command-bar/workflow-ops.ts +10 -0
- package/src/components/layout/floating-pane.tsx +3 -1
- package/src/components/layout/header.tsx +5 -4
- package/src/components/layout/pane-sizing.ts +6 -0
- package/src/components/layout/pane.tsx +5 -1
- package/src/components/layout/shell.test.tsx +263 -32
- package/src/components/layout/shell.tsx +10 -6
- package/src/components/layout/status-bar.test.tsx +16 -7
- package/src/components/layout/status-bar.tsx +4 -1
- package/src/components/markdown-editor.tsx +1 -0
- package/src/components/pane-settings-dialog.tsx +13 -152
- package/src/components/pane-template-wizard.tsx +1 -0
- package/src/components/price-selector-dialog.tsx +12 -7
- package/src/components/ticker-badge-text.test.tsx +50 -0
- package/src/components/ticker-badge-text.tsx +17 -2
- package/src/components/ticker-badge.tsx +2 -2
- package/src/components/ticker-list-table.tsx +62 -2
- package/src/components/toggle-list.tsx +15 -2
- package/src/components/ui/data-table.tsx +82 -5
- package/src/components/ui/external-link.tsx +48 -11
- package/src/components/ui/fields.tsx +2 -1
- package/src/components/ui/index.ts +8 -1
- package/src/components/ui/multi-select-chips.tsx +69 -0
- package/src/components/ui/multi-select-dialog.tsx +267 -0
- package/src/components/ui/multi-select.ts +78 -0
- package/src/components/ui/ui.test.tsx +343 -1
- package/src/data/plugin-state-store.ts +30 -19
- package/src/data/resource-store.ts +133 -103
- package/src/data/session-store.ts +19 -12
- package/src/data/sqlite-cache.test.ts +37 -2
- package/src/data/sqlite-database.ts +8 -2
- package/src/data/sqlite-retry.test.ts +63 -0
- package/src/data/sqlite-retry.ts +66 -0
- package/src/data/ticker-store.ts +29 -18
- package/src/index.tsx +6 -1
- package/src/market-data/coordinator.test.ts +114 -5
- package/src/market-data/coordinator.ts +70 -1
- package/src/market-data/hooks.test.tsx +73 -2
- package/src/market-data/hooks.tsx +16 -4
- package/src/notifications/app-notifier.test.ts +177 -0
- package/src/notifications/app-notifier.ts +241 -0
- package/src/pane-settings.test.ts +2 -0
- package/src/plugins/builtin/ai/index.test.tsx +1 -1
- package/src/plugins/builtin/ai/screener-pane.test.tsx +2 -0
- package/src/plugins/builtin/ai/screener-pane.tsx +1 -0
- package/src/plugins/builtin/ai/ticker-context.ts +3 -2
- package/src/plugins/builtin/alerts/alert-engine.test.ts +90 -0
- package/src/plugins/builtin/alerts/alert-engine.ts +55 -0
- package/src/plugins/builtin/alerts/index.test.tsx +357 -0
- package/src/plugins/builtin/alerts/index.tsx +519 -0
- package/src/plugins/builtin/alerts/types.ts +14 -0
- package/src/plugins/builtin/analytics/index.test.tsx +300 -0
- package/src/plugins/builtin/analytics/index.tsx +775 -0
- package/src/plugins/builtin/analytics/metrics.test.ts +125 -0
- package/src/plugins/builtin/analytics/metrics.ts +155 -0
- package/src/plugins/builtin/ask-ai.test.tsx +2 -0
- package/src/plugins/builtin/chat-controller.test.ts +92 -18
- package/src/plugins/builtin/chat-controller.ts +40 -11
- package/src/plugins/builtin/chat.test.tsx +715 -4
- package/src/plugins/builtin/chat.tsx +420 -119
- package/src/plugins/builtin/comparison-chart.test.tsx +97 -57
- package/src/plugins/builtin/comparison-chart.tsx +1 -2
- package/src/plugins/builtin/correlation/compute.test.ts +147 -0
- package/src/plugins/builtin/correlation/compute.ts +115 -0
- package/src/plugins/builtin/correlation/index.tsx +385 -0
- package/src/plugins/builtin/correlation/plugin.test.ts +44 -0
- package/src/plugins/builtin/correlation/settings.test.ts +34 -0
- package/src/plugins/builtin/correlation/settings.ts +95 -0
- package/src/plugins/builtin/debug.tsx +36 -50
- package/src/plugins/builtin/earnings/earnings-cache.test.ts +181 -0
- package/src/plugins/builtin/earnings/earnings-cache.ts +136 -0
- package/src/plugins/builtin/earnings/earnings-data.test.ts +59 -0
- package/src/plugins/builtin/earnings/earnings-data.ts +50 -0
- package/src/plugins/builtin/earnings/index.tsx +337 -0
- package/src/plugins/builtin/econ/calendar-source.test.ts +219 -0
- package/src/plugins/builtin/econ/calendar-source.ts +121 -0
- package/src/plugins/builtin/econ/fred-client.test.ts +63 -0
- package/src/plugins/builtin/econ/fred-client.ts +79 -0
- package/src/plugins/builtin/econ/fred-series-map.ts +88 -0
- package/src/plugins/builtin/econ/index.tsx +999 -0
- package/src/plugins/builtin/econ/types.ts +13 -0
- package/src/plugins/builtin/fred-settings.ts +10 -0
- package/src/plugins/builtin/fx-matrix/index.tsx +178 -0
- package/src/plugins/builtin/fx-matrix/pairs.ts +30 -0
- package/src/plugins/builtin/help.test.tsx +1 -5
- package/src/plugins/builtin/help.tsx +1 -16
- package/src/plugins/builtin/insider/index.tsx +417 -0
- package/src/plugins/builtin/insider/insider-data.test.ts +55 -0
- package/src/plugins/builtin/insider/insider-data.ts +49 -0
- package/src/plugins/builtin/layout-manager.tsx +33 -16
- package/src/plugins/builtin/market-movers/index.tsx +573 -0
- package/src/plugins/builtin/market-movers/screener.test.ts +143 -0
- package/src/plugins/builtin/market-movers/screener.ts +105 -0
- package/src/plugins/builtin/options.tsx +4 -3
- package/src/plugins/builtin/portfolio-list/cli/commands.ts +11 -6
- package/src/plugins/builtin/portfolio-list/command-bar.ts +6 -3
- package/src/plugins/builtin/portfolio-list/header.tsx +3 -2
- package/src/plugins/builtin/portfolio-list/metrics.ts +22 -11
- package/src/plugins/builtin/portfolio-list/pane.tsx +3 -9
- package/src/plugins/builtin/portfolio-list/settings.ts +9 -0
- package/src/plugins/builtin/portfolio-list/table.tsx +3 -0
- package/src/plugins/builtin/portfolio-list.test.tsx +3 -1
- package/src/plugins/builtin/portfolio-metrics.test.ts +13 -1
- package/src/plugins/builtin/sec.tsx +114 -7
- package/src/plugins/builtin/sectors/index.tsx +359 -0
- package/src/plugins/builtin/sectors/sector-data.ts +18 -0
- package/src/plugins/builtin/ticker-detail/chart-tab.tsx +75 -13
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +316 -80
- package/src/plugins/builtin/ticker-detail/quote-monitor.tsx +4 -6
- package/src/plugins/builtin/ticker-detail-chart-tab-switch.test.tsx +17 -12
- package/src/plugins/builtin/ticker-detail.test.tsx +3 -3
- package/src/plugins/builtin/world-indices/index.tsx +410 -0
- package/src/plugins/builtin/world-indices/indices.ts +52 -0
- package/src/plugins/builtin/yield-curve/index.tsx +242 -0
- package/src/plugins/builtin/yield-curve/treasury-data.test.ts +212 -0
- package/src/plugins/builtin/yield-curve/treasury-data.ts +107 -0
- package/src/plugins/catalog.ts +22 -0
- package/src/plugins/gridlock-notification.ts +15 -0
- package/src/plugins/ibkr/__snapshots__/trade-tab.test.tsx.snap +5 -5
- package/src/plugins/ibkr/gateway-service.test.ts +22 -0
- package/src/plugins/ibkr/gateway-service.ts +6 -0
- package/src/plugins/ibkr/index.test.tsx +1 -1
- package/src/plugins/ibkr/index.tsx +8 -2
- package/src/plugins/ibkr/trade-tab.tsx +23 -5
- package/src/plugins/ibkr/trade-utils.test.ts +1 -1
- package/src/plugins/ibkr/trade-utils.ts +7 -6
- package/src/plugins/ibkr/trading-pane.tsx +8 -7
- package/src/plugins/plugin-runtime.test.tsx +2 -0
- package/src/plugins/plugin-runtime.tsx +10 -0
- package/src/plugins/prediction-markets/controller-data.ts +129 -14
- package/src/plugins/prediction-markets/controller.ts +1 -1
- package/src/plugins/prediction-markets/detail/shared.tsx +7 -6
- package/src/plugins/prediction-markets/index.tsx +5 -1
- package/src/plugins/prediction-markets/pane.test.tsx +78 -0
- package/src/plugins/prediction-markets/pane.tsx +7 -3
- package/src/plugins/prediction-markets/plugin.test.tsx +48 -1
- package/src/plugins/prediction-markets/services/fetch.ts +17 -7
- package/src/plugins/prediction-markets/services/polymarket-adapter.ts +29 -11
- package/src/plugins/prediction-markets/services/polymarket-ws.ts +20 -0
- package/src/plugins/prediction-markets/test-helpers.tsx +22 -5
- package/src/plugins/registry.ts +45 -13
- package/src/sources/provider-router.test.ts +3 -2
- package/src/sources/sec-edgar.ts +13 -1
- package/src/sources/yahoo-finance.ts +75 -1
- package/src/state/app-context-selector.test.tsx +17 -1
- package/src/state/app-context.test.tsx +24 -0
- package/src/state/app-context.tsx +29 -6
- package/src/theme/colors.ts +5 -0
- package/src/theme/themes.ts +25 -0
- package/src/types/config.test.ts +107 -0
- package/src/types/config.ts +30 -1
- package/src/types/data-provider.ts +13 -0
- package/src/types/plugin.ts +35 -1
- package/src/updater.test.ts +2 -0
- package/src/utils/api-client.ts +12 -0
- package/src/utils/format.test.ts +11 -1
- package/src/utils/format.ts +86 -8
- package/src/utils/inline-content-tokenizer.test.ts +22 -0
- package/src/utils/inline-content-tokenizer.ts +76 -0
- package/src/utils/link-tokenizer.test.ts +27 -0
- package/src/utils/link-tokenizer.ts +93 -0
- package/src/utils/market-format.test.ts +109 -0
- package/src/utils/market-format.ts +299 -0
- package/src/utils/market-status.ts +3 -2
- package/src/utils/selection-clipboard.test.ts +43 -0
- package/src/utils/selection-clipboard.ts +88 -0
- package/src/utils/terminal-input-reset.test.ts +50 -0
- package/src/utils/terminal-input-reset.ts +24 -0
- package/src/utils/throttled-fetch.test.ts +145 -0
- package/src/utils/throttled-fetch.ts +212 -0
- package/src/utils/ticker-tokenizer.test.ts +31 -0
- package/src/utils/ticker-tokenizer.ts +7 -42
- package/src/version.ts +1 -1
package/README.md
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
# 📉 Gloomberb
|
|
4
4
|
|
|
5
|
-
**
|
|
5
|
+
**Modern extensible financial terminal.**
|
|
6
6
|
|
|
7
|
-
>
|
|
7
|
+
> The Bloomberb Terminal for the rest of us.
|
|
8
8
|
|
|
9
9
|
<img src="https://gloomberb.com/landing-terminal.png" alt="Gloomberb terminal screenshot" width="720" />
|
|
10
10
|
|
package/package.json
CHANGED
package/src/app.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState, useEffect, useCallback, useRef, useMemo
|
|
1
|
+
import { useState, useEffect, useCallback, useRef, useMemo } from "react";
|
|
2
2
|
import { useKeyboard, useRenderer } from "@opentui/react";
|
|
3
3
|
import type { CliRenderer } from "@opentui/core";
|
|
4
4
|
import {
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
useAppState,
|
|
11
11
|
} from "./state/app-context";
|
|
12
12
|
import { bindAppActivity, useAppActive } from "./state/app-activity";
|
|
13
|
+
import { copyActiveSelection, isCopyShortcut, isPasteShortcut, pasteSystemClipboard } from "./utils/selection-clipboard";
|
|
13
14
|
import { Header } from "./components/layout/header";
|
|
14
15
|
import { StatusBar } from "./components/layout/status-bar";
|
|
15
16
|
import { Shell } from "./components/layout/shell";
|
|
@@ -20,12 +21,15 @@ import { PluginRegistry } from "./plugins/registry";
|
|
|
20
21
|
import { AppPersistence } from "./data/app-persistence";
|
|
21
22
|
import { TickerRepository } from "./data/ticker-repository";
|
|
22
23
|
import { ProviderRouter } from "./sources/provider-router";
|
|
23
|
-
import { colors,
|
|
24
|
+
import { colors, syncTheme } from "./theme/colors";
|
|
24
25
|
import {
|
|
25
26
|
createPaneInstance,
|
|
26
27
|
findPaneInstance,
|
|
28
|
+
findPrimaryPaneInstance,
|
|
27
29
|
isTickerPaneId,
|
|
28
30
|
normalizePaneLayout,
|
|
31
|
+
resolveFollowBindingInstance,
|
|
32
|
+
resolvePaneInstance,
|
|
29
33
|
type AppConfig,
|
|
30
34
|
type BrokerInstanceConfig,
|
|
31
35
|
type LayoutConfig,
|
|
@@ -51,7 +55,7 @@ import { setLayoutManagerDispatch } from "./plugins/builtin/layout-manager";
|
|
|
51
55
|
import { getLoadablePlugins } from "./plugins/catalog";
|
|
52
56
|
import { saveConfig } from "./data/config-store";
|
|
53
57
|
import { Toaster, toast } from "@opentui-ui/toast/react";
|
|
54
|
-
import { canSelfUpdate, checkForUpdateDetailed, performUpdate } from "./updater";
|
|
58
|
+
import { canSelfUpdate, checkForUpdateDetailed, performUpdate, type ReleaseInfo } from "./updater";
|
|
55
59
|
import { VERSION } from "./version";
|
|
56
60
|
import { join } from "path";
|
|
57
61
|
import {
|
|
@@ -93,6 +97,7 @@ import { debugLog } from "./utils/debug-log";
|
|
|
93
97
|
import { MarketDataCoordinator, setSharedMarketDataCoordinator } from "./market-data/coordinator";
|
|
94
98
|
import { instrumentFromTicker } from "./market-data/request-types";
|
|
95
99
|
import { syncBrokerInstance } from "./brokers/sync-broker-instance";
|
|
100
|
+
import { createAppNotifier } from "./notifications/app-notifier";
|
|
96
101
|
|
|
97
102
|
/** Global-level dedup: prevents concurrent refresh calls for the same symbol. */
|
|
98
103
|
const refreshInFlight: Set<string> = (globalThis as any).__refreshInFlight ??= new Set<string>();
|
|
@@ -100,6 +105,14 @@ const quoteRefreshInFlight: Set<string> = (globalThis as any).__quoteRefreshInFl
|
|
|
100
105
|
const PANEL_RESOLUTION_BOUNDS = { x: 0, y: 0, width: 120, height: 40 };
|
|
101
106
|
const appLog = debugLog.createLogger("app");
|
|
102
107
|
|
|
108
|
+
function isCollectionPaneInstance(instance: PaneInstanceConfig): boolean {
|
|
109
|
+
return instance.paneId === "portfolio-list";
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function isTickerContextPaneInstance(instance: PaneInstanceConfig): boolean {
|
|
113
|
+
return instance.paneId === "portfolio-list" || isTickerPaneId(instance.paneId);
|
|
114
|
+
}
|
|
115
|
+
|
|
103
116
|
function summarizeError(error: unknown): Record<string, string> {
|
|
104
117
|
if (error instanceof Error) {
|
|
105
118
|
return {
|
|
@@ -122,30 +135,45 @@ interface AppInnerProps {
|
|
|
122
135
|
function AppInner({ pluginRegistry, tickerRepository, dataProvider, marketData, sessionSnapshot = null }: AppInnerProps) {
|
|
123
136
|
const { state, dispatch } = useAppState();
|
|
124
137
|
const appActive = useAppActive();
|
|
138
|
+
const appActiveRef = useRef(appActive);
|
|
125
139
|
const renderer = useRenderer();
|
|
126
140
|
const dialog = useDialog();
|
|
127
141
|
const stateRef = useRef(state);
|
|
128
142
|
stateRef.current = state;
|
|
143
|
+
appActiveRef.current = appActive;
|
|
129
144
|
const focusedCollectionId = getFocusedCollectionId(state);
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
145
|
+
const appNotifier = useMemo(() => createAppNotifier({
|
|
146
|
+
isAppActive: () => appActiveRef.current,
|
|
147
|
+
renderToast: (notification) => {
|
|
148
|
+
const type = notification.type ?? "info";
|
|
149
|
+
let toastId: string | number | undefined;
|
|
150
|
+
const options = {
|
|
151
|
+
duration: notification.duration,
|
|
152
|
+
action: notification.action
|
|
153
|
+
? {
|
|
154
|
+
label: notification.action.label,
|
|
155
|
+
onClick: () => {
|
|
156
|
+
try {
|
|
157
|
+
notification.action?.onClick();
|
|
158
|
+
} finally {
|
|
159
|
+
if (toastId != null) toast.dismiss(toastId);
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
}
|
|
163
|
+
: undefined,
|
|
164
|
+
};
|
|
165
|
+
if (type === "success") toastId = toast.success(notification.body, options);
|
|
166
|
+
else if (type === "error") toastId = toast.error(notification.body, options);
|
|
167
|
+
else toastId = toast.info(notification.body, options);
|
|
168
|
+
},
|
|
169
|
+
}), []);
|
|
170
|
+
const notify = useCallback((body: string, options?: { type?: "info" | "success" | "error" }) => {
|
|
171
|
+
pluginRegistry.notify({ body, ...options });
|
|
172
|
+
}, [pluginRegistry]);
|
|
143
173
|
|
|
144
174
|
const resolvePaneTarget = useCallback((paneId: string, layout: LayoutConfig = state.config.layout): string | null => {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
return resolvePrimaryPaneInstanceId(paneId, layout);
|
|
148
|
-
}, [resolvePrimaryPaneInstanceId, state.config.layout]);
|
|
175
|
+
return resolvePaneInstance(layout, paneId)?.instanceId ?? null;
|
|
176
|
+
}, [state.config.layout]);
|
|
149
177
|
|
|
150
178
|
const getPreferredPortfolio = useCallback((ticker: TickerRecord | null) => {
|
|
151
179
|
const focusedPortfolio = state.config.portfolios.find((portfolio) => portfolio.id === focusedCollectionId);
|
|
@@ -197,39 +225,17 @@ function AppInner({ pluginRegistry, tickerRepository, dataProvider, marketData,
|
|
|
197
225
|
}, [dialog]);
|
|
198
226
|
|
|
199
227
|
const resolveCollectionSourcePaneId = useCallback((preferredPaneId?: string | null) => {
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
if (!instance) return null;
|
|
204
|
-
if (instance.paneId === "portfolio-list") return instance.instanceId;
|
|
205
|
-
if (instance.binding?.kind === "follow") {
|
|
206
|
-
return tryResolve(instance.binding.sourceInstanceId);
|
|
207
|
-
}
|
|
208
|
-
return null;
|
|
209
|
-
};
|
|
210
|
-
|
|
211
|
-
return tryResolve(preferredPaneId)
|
|
212
|
-
?? tryResolve(state.focusedPaneId)
|
|
213
|
-
?? state.config.layout.instances.find((instance) => instance.paneId === "portfolio-list")?.instanceId
|
|
228
|
+
return resolveFollowBindingInstance(state.config.layout, preferredPaneId, isCollectionPaneInstance)?.instanceId
|
|
229
|
+
?? resolveFollowBindingInstance(state.config.layout, state.focusedPaneId, isCollectionPaneInstance)?.instanceId
|
|
230
|
+
?? findPrimaryPaneInstance(state.config.layout, "portfolio-list")?.instanceId
|
|
214
231
|
?? null;
|
|
215
232
|
}, [state.config.layout, state.focusedPaneId]);
|
|
216
233
|
|
|
217
234
|
const resolveTickerContextSourcePaneId = useCallback((preferredPaneId?: string | null) => {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
if (instance.paneId === "portfolio-list" || isTickerPaneId(instance.paneId)) return instance.instanceId;
|
|
223
|
-
if (instance.binding?.kind === "follow") {
|
|
224
|
-
return tryResolve(instance.binding.sourceInstanceId);
|
|
225
|
-
}
|
|
226
|
-
return null;
|
|
227
|
-
};
|
|
228
|
-
|
|
229
|
-
return tryResolve(preferredPaneId)
|
|
230
|
-
?? tryResolve(state.focusedPaneId)
|
|
231
|
-
?? state.config.layout.instances.find((instance) => instance.paneId === "ticker-detail" && instance.binding?.kind !== "fixed")?.instanceId
|
|
232
|
-
?? state.config.layout.instances.find((instance) => instance.paneId === "portfolio-list")?.instanceId
|
|
235
|
+
return resolveFollowBindingInstance(state.config.layout, preferredPaneId, isTickerContextPaneInstance)?.instanceId
|
|
236
|
+
?? resolveFollowBindingInstance(state.config.layout, state.focusedPaneId, isTickerContextPaneInstance)?.instanceId
|
|
237
|
+
?? findPrimaryPaneInstance(state.config.layout, "ticker-detail")?.instanceId
|
|
238
|
+
?? findPrimaryPaneInstance(state.config.layout, "portfolio-list")?.instanceId
|
|
233
239
|
?? null;
|
|
234
240
|
}, [state.config.layout, state.focusedPaneId]);
|
|
235
241
|
|
|
@@ -273,9 +279,9 @@ function AppInner({ pluginRegistry, tickerRepository, dataProvider, marketData,
|
|
|
273
279
|
|
|
274
280
|
const switchDetailTab = useCallback((tabId: string, preferredPaneId?: string | null) => {
|
|
275
281
|
const targetPaneId = (() => {
|
|
276
|
-
const target = preferredPaneId ?
|
|
282
|
+
const target = preferredPaneId ? resolvePaneInstance(state.config.layout, preferredPaneId) : null;
|
|
277
283
|
if (target?.paneId === "ticker-detail") return target.instanceId;
|
|
278
|
-
const focused = state.focusedPaneId ?
|
|
284
|
+
const focused = state.focusedPaneId ? resolvePaneInstance(state.config.layout, state.focusedPaneId) : null;
|
|
279
285
|
if (focused?.paneId === "ticker-detail") return focused.instanceId;
|
|
280
286
|
const sourcePaneId = resolveCollectionSourcePaneId(preferredPaneId);
|
|
281
287
|
if (!sourcePaneId) return null;
|
|
@@ -525,6 +531,13 @@ function AppInner({ pluginRegistry, tickerRepository, dataProvider, marketData,
|
|
|
525
531
|
}
|
|
526
532
|
}, [state.config.brokerInstances, importBrokerPositions]);
|
|
527
533
|
|
|
534
|
+
const startUpdate = useCallback((release: ReleaseInfo) => {
|
|
535
|
+
dispatch({ type: "SET_UPDATE_PROGRESS", progress: { phase: "downloading", percent: 0 } });
|
|
536
|
+
void performUpdate(release, (progress) => {
|
|
537
|
+
dispatch({ type: "SET_UPDATE_PROGRESS", progress });
|
|
538
|
+
});
|
|
539
|
+
}, [dispatch]);
|
|
540
|
+
|
|
528
541
|
const runUpdateCheck = useCallback(async (manual = false) => {
|
|
529
542
|
if (manual) {
|
|
530
543
|
dispatch({ type: "SET_UPDATE_CHECK_IN_PROGRESS", checking: true });
|
|
@@ -566,6 +579,12 @@ function AppInner({ pluginRegistry, tickerRepository, dataProvider, marketData,
|
|
|
566
579
|
void runUpdateCheck(false);
|
|
567
580
|
}, [runUpdateCheck]);
|
|
568
581
|
|
|
582
|
+
useEffect(() => {
|
|
583
|
+
if (!state.updateAvailable || state.updateProgress || state.updateCheckInProgress) return;
|
|
584
|
+
if (!canSelfUpdate(state.updateAvailable)) return;
|
|
585
|
+
startUpdate(state.updateAvailable);
|
|
586
|
+
}, [startUpdate, state.updateAvailable, state.updateCheckInProgress, state.updateProgress]);
|
|
587
|
+
|
|
569
588
|
// Load tickers on mount
|
|
570
589
|
useEffect(() => {
|
|
571
590
|
if (state.initialized || (globalThis as any).__gloomInitStarted) return;
|
|
@@ -783,6 +802,14 @@ function AppInner({ pluginRegistry, tickerRepository, dataProvider, marketData,
|
|
|
783
802
|
pluginRegistry.switchPanelFn = (panel) => dispatch({ type: "SET_ACTIVE_PANEL", panel });
|
|
784
803
|
pluginRegistry.switchTabFn = (tabId, paneId) => switchDetailTab(tabId, paneId);
|
|
785
804
|
pluginRegistry.openCommandBarFn = (query) => dispatch({ type: "SET_COMMAND_BAR", open: true, query });
|
|
805
|
+
pluginRegistry.openPluginCommandWorkflowFn = (commandId) => {
|
|
806
|
+
dispatch({
|
|
807
|
+
type: "SET_COMMAND_BAR",
|
|
808
|
+
open: true,
|
|
809
|
+
query: "",
|
|
810
|
+
launch: { kind: "plugin-command", commandId },
|
|
811
|
+
});
|
|
812
|
+
};
|
|
786
813
|
const persistLayout = (layout: LayoutConfig, options?: { pushHistory?: boolean }) => {
|
|
787
814
|
const currentState = stateRef.current;
|
|
788
815
|
const normalizedLayout = normalizePaneLayout(layout);
|
|
@@ -881,7 +908,7 @@ function AppInner({ pluginRegistry, tickerRepository, dataProvider, marketData,
|
|
|
881
908
|
if (paneId === "ticker-detail") {
|
|
882
909
|
const sourcePaneId = resolveCollectionSourcePaneId();
|
|
883
910
|
if (!sourcePaneId) {
|
|
884
|
-
|
|
911
|
+
notify("Open a collection pane first to inspect a ticker.");
|
|
885
912
|
return;
|
|
886
913
|
}
|
|
887
914
|
const ensured = ensureInspectorPane(sourcePaneId);
|
|
@@ -904,7 +931,7 @@ function AppInner({ pluginRegistry, tickerRepository, dataProvider, marketData,
|
|
|
904
931
|
: buildPaneInstance(paneId);
|
|
905
932
|
if (!instance) {
|
|
906
933
|
if (isTickerPaneId(paneId)) {
|
|
907
|
-
|
|
934
|
+
notify("Open a ticker or collection context first.");
|
|
908
935
|
}
|
|
909
936
|
return;
|
|
910
937
|
}
|
|
@@ -947,7 +974,7 @@ function AppInner({ pluginRegistry, tickerRepository, dataProvider, marketData,
|
|
|
947
974
|
placePaneInstance,
|
|
948
975
|
});
|
|
949
976
|
} catch (error) {
|
|
950
|
-
|
|
977
|
+
notify(
|
|
951
978
|
error instanceof Error ? error.message : `Could not create ${getPaneTemplateDisplayLabel(template).toLowerCase()}.`,
|
|
952
979
|
{ type: "info" },
|
|
953
980
|
);
|
|
@@ -956,6 +983,7 @@ function AppInner({ pluginRegistry, tickerRepository, dataProvider, marketData,
|
|
|
956
983
|
buildPaneInstance,
|
|
957
984
|
dataProvider,
|
|
958
985
|
dispatch,
|
|
986
|
+
notify,
|
|
959
987
|
placePaneInstance,
|
|
960
988
|
pluginRegistry,
|
|
961
989
|
runPaneTemplateWizard,
|
|
@@ -1067,6 +1095,90 @@ function AppInner({ pluginRegistry, tickerRepository, dataProvider, marketData,
|
|
|
1067
1095
|
activatePane(instance.instanceId, nextLayout);
|
|
1068
1096
|
};
|
|
1069
1097
|
|
|
1098
|
+
pluginRegistry.navigateTickerFn = (rawSymbol) => {
|
|
1099
|
+
(async () => {
|
|
1100
|
+
try {
|
|
1101
|
+
// Resolve or create the ticker in the local database
|
|
1102
|
+
const resolved = await resolveTickerSearch({
|
|
1103
|
+
query: rawSymbol,
|
|
1104
|
+
activeTicker: null,
|
|
1105
|
+
tickers: stateRef.current.tickers,
|
|
1106
|
+
dataProvider,
|
|
1107
|
+
});
|
|
1108
|
+
|
|
1109
|
+
let symbol = rawSymbol;
|
|
1110
|
+
if (resolved?.kind === "local") {
|
|
1111
|
+
symbol = resolved.symbol;
|
|
1112
|
+
} else if (resolved?.kind === "provider" && resolved.result) {
|
|
1113
|
+
const { ticker, created } = await upsertTickerFromSearchResult(tickerRepository, resolved.result);
|
|
1114
|
+
symbol = ticker.metadata.ticker;
|
|
1115
|
+
dispatch({ type: "UPDATE_TICKER", ticker });
|
|
1116
|
+
if (created) {
|
|
1117
|
+
pluginRegistry.events.emit("ticker:added", { symbol, ticker });
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
// Active panel resolution — navigate the focused or linked detail pane:
|
|
1122
|
+
// 1. If the focused pane IS a ticker-detail, retarget it directly
|
|
1123
|
+
// 2. If a ticker-detail follows the focused pane, retarget that
|
|
1124
|
+
// 3. Any follow-mode ticker-detail in the layout
|
|
1125
|
+
// 4. Any ticker-detail in the layout
|
|
1126
|
+
// 5. Fall back to pinning a new pane
|
|
1127
|
+
const currentState = stateRef.current;
|
|
1128
|
+
const currentLayout = currentState.config.layout;
|
|
1129
|
+
const focused = currentState.focusedPaneId;
|
|
1130
|
+
|
|
1131
|
+
const focusedInstance = focused
|
|
1132
|
+
? findPaneInstance(currentLayout, focused)
|
|
1133
|
+
: null;
|
|
1134
|
+
|
|
1135
|
+
const detailPane =
|
|
1136
|
+
(focusedInstance?.paneId === "ticker-detail" && isPaneInLayout(currentLayout, focusedInstance.instanceId)
|
|
1137
|
+
? focusedInstance
|
|
1138
|
+
: null)
|
|
1139
|
+
?? currentLayout.instances.find((inst) =>
|
|
1140
|
+
inst.paneId === "ticker-detail"
|
|
1141
|
+
&& inst.binding?.kind === "follow"
|
|
1142
|
+
&& inst.binding.sourceInstanceId === focused
|
|
1143
|
+
&& isPaneInLayout(currentLayout, inst.instanceId),
|
|
1144
|
+
)
|
|
1145
|
+
?? currentLayout.instances.find((inst) =>
|
|
1146
|
+
inst.paneId === "ticker-detail"
|
|
1147
|
+
&& inst.binding?.kind === "follow"
|
|
1148
|
+
&& isPaneInLayout(currentLayout, inst.instanceId),
|
|
1149
|
+
)
|
|
1150
|
+
?? currentLayout.instances.find((inst) =>
|
|
1151
|
+
inst.paneId === "ticker-detail"
|
|
1152
|
+
&& isPaneInLayout(currentLayout, inst.instanceId),
|
|
1153
|
+
);
|
|
1154
|
+
|
|
1155
|
+
if (detailPane) {
|
|
1156
|
+
if (detailPane.binding?.kind === "follow") {
|
|
1157
|
+
const sourceId = detailPane.binding.sourceInstanceId;
|
|
1158
|
+
dispatch({ type: "UPDATE_PANE_STATE", paneId: sourceId, patch: { cursorSymbol: symbol } });
|
|
1159
|
+
activatePane(detailPane.instanceId, currentLayout);
|
|
1160
|
+
} else {
|
|
1161
|
+
const nextLayout = {
|
|
1162
|
+
...currentLayout,
|
|
1163
|
+
instances: currentLayout.instances.map((instance) => (
|
|
1164
|
+
instance.instanceId === detailPane.instanceId
|
|
1165
|
+
? { ...instance, title: symbol, binding: { kind: "fixed" as const, symbol } }
|
|
1166
|
+
: instance
|
|
1167
|
+
)),
|
|
1168
|
+
};
|
|
1169
|
+
persistLayout(nextLayout);
|
|
1170
|
+
activatePane(detailPane.instanceId, nextLayout);
|
|
1171
|
+
}
|
|
1172
|
+
} else {
|
|
1173
|
+
pluginRegistry.pinTickerFn(symbol, { floating: false });
|
|
1174
|
+
}
|
|
1175
|
+
} catch (err) {
|
|
1176
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1177
|
+
pluginRegistry.notify({ body: `Failed to navigate to ${rawSymbol}: ${message}`, type: "error" });
|
|
1178
|
+
}
|
|
1179
|
+
})();
|
|
1180
|
+
};
|
|
1181
|
+
|
|
1070
1182
|
setLayoutManagerDispatch(dispatch, () => ({
|
|
1071
1183
|
layout: state.config.layout,
|
|
1072
1184
|
termWidth: pluginRegistry.getTermSizeFn().width,
|
|
@@ -1074,14 +1186,8 @@ function AppInner({ pluginRegistry, tickerRepository, dataProvider, marketData,
|
|
|
1074
1186
|
focusedPaneId: state.focusedPaneId,
|
|
1075
1187
|
}));
|
|
1076
1188
|
|
|
1077
|
-
// Wire up
|
|
1078
|
-
pluginRegistry.
|
|
1079
|
-
const type = options?.type ?? "info";
|
|
1080
|
-
const duration = options?.duration;
|
|
1081
|
-
if (type === "success") toast.success(message, { duration });
|
|
1082
|
-
else if (type === "error") toast.error(message, { duration });
|
|
1083
|
-
else toast.info(message, { duration });
|
|
1084
|
-
};
|
|
1189
|
+
// Wire up app-level notifications.
|
|
1190
|
+
pluginRegistry.notifyFn = appNotifier.notify;
|
|
1085
1191
|
|
|
1086
1192
|
// Persist layout changes (switching, saving, deleting, renaming layouts)
|
|
1087
1193
|
const prevLayouts = useRef(state.config.layouts);
|
|
@@ -1111,6 +1217,17 @@ function AppInner({ pluginRegistry, tickerRepository, dataProvider, marketData,
|
|
|
1111
1217
|
|
|
1112
1218
|
// Global keyboard shortcuts
|
|
1113
1219
|
useKeyboard((event) => {
|
|
1220
|
+
if (isCopyShortcut(event) && copyActiveSelection(renderer)) {
|
|
1221
|
+
event.preventDefault();
|
|
1222
|
+
event.stopPropagation();
|
|
1223
|
+
return;
|
|
1224
|
+
}
|
|
1225
|
+
if (isPasteShortcut(event) && pasteSystemClipboard(renderer)) {
|
|
1226
|
+
event.preventDefault();
|
|
1227
|
+
event.stopPropagation();
|
|
1228
|
+
return;
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1114
1231
|
// Skip all global shortcuts when a dialog is open
|
|
1115
1232
|
if (dialogOpen) return;
|
|
1116
1233
|
|
|
@@ -1172,9 +1289,7 @@ function AppInner({ pluginRegistry, tickerRepository, dataProvider, marketData,
|
|
|
1172
1289
|
dispatch({ type: "SET_COMMAND_BAR", open: true, query: "" });
|
|
1173
1290
|
}
|
|
1174
1291
|
} else if (event.name === "u" && state.updateAvailable && !state.updateProgress && !state.updateCheckInProgress && canSelfUpdate(state.updateAvailable)) {
|
|
1175
|
-
|
|
1176
|
-
dispatch({ type: "SET_UPDATE_PROGRESS", progress });
|
|
1177
|
-
});
|
|
1292
|
+
startUpdate(state.updateAvailable);
|
|
1178
1293
|
} else {
|
|
1179
1294
|
// Plugin keyboard shortcuts (built-ins take priority)
|
|
1180
1295
|
const disabledPlugins = new Set(state.config.disabledPlugins || []);
|
|
@@ -1239,11 +1354,10 @@ export function App({
|
|
|
1239
1354
|
});
|
|
1240
1355
|
const [showOnboarding, setShowOnboarding] = useState(!initialConfig.onboardingComplete);
|
|
1241
1356
|
|
|
1242
|
-
|
|
1357
|
+
// Keep the shared palette aligned before this render builds any JSX that reads `colors`.
|
|
1358
|
+
syncTheme(config.theme);
|
|
1243
1359
|
|
|
1244
|
-
|
|
1245
|
-
if (config.theme) applyTheme(config.theme);
|
|
1246
|
-
}, [config.theme]);
|
|
1360
|
+
useEffect(() => bindAppActivity(renderer), [renderer]);
|
|
1247
1361
|
|
|
1248
1362
|
const services = useMemo(() => {
|
|
1249
1363
|
const dbPath = join(config.dataDir, ".gloomberb-cache.db");
|
|
@@ -1278,6 +1392,7 @@ export function App({
|
|
|
1278
1392
|
useEffect(() => {
|
|
1279
1393
|
return () => {
|
|
1280
1394
|
setSharedMarketDataCoordinator(null);
|
|
1395
|
+
services.pluginRegistry.destroy();
|
|
1281
1396
|
services.persistence.close();
|
|
1282
1397
|
};
|
|
1283
1398
|
}, [services]);
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
formatNumber,
|
|
5
5
|
formatPercent,
|
|
6
6
|
} from "../../utils/format";
|
|
7
|
+
import { formatMarketCostWithCurrency, formatMarketPriceWithCurrency, formatMarketQuantity } from "../../utils/market-format";
|
|
7
8
|
import {
|
|
8
9
|
cliStyles,
|
|
9
10
|
colorBySign,
|
|
@@ -77,6 +78,30 @@ function appendTextSection(lines: string[], title: string, content: string | und
|
|
|
77
78
|
lines.push(text);
|
|
78
79
|
}
|
|
79
80
|
|
|
81
|
+
function normalizeTimestamp(value: Date | string | number | undefined): number | null {
|
|
82
|
+
if (value instanceof Date) {
|
|
83
|
+
const timestamp = value.getTime();
|
|
84
|
+
return Number.isNaN(timestamp) ? null : timestamp;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (typeof value === "string" || typeof value === "number") {
|
|
88
|
+
const timestamp = new Date(value).getTime();
|
|
89
|
+
return Number.isNaN(timestamp) ? null : timestamp;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function formatFeedDate(value: Date | string | number | undefined): string {
|
|
96
|
+
const timestamp = normalizeTimestamp(value);
|
|
97
|
+
if (timestamp == null) return "";
|
|
98
|
+
return new Date(timestamp).toLocaleDateString("en-US", {
|
|
99
|
+
month: "short",
|
|
100
|
+
day: "numeric",
|
|
101
|
+
year: "numeric",
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
80
105
|
function appendFeedSection(
|
|
81
106
|
lines: string[],
|
|
82
107
|
title: string,
|
|
@@ -215,16 +240,16 @@ export async function buildTickerReport({
|
|
|
215
240
|
: "—";
|
|
216
241
|
|
|
217
242
|
appendMetricSection(lines, "Quote", [
|
|
218
|
-
["Last", colorBySign(
|
|
243
|
+
["Last", colorBySign(formatMarketPriceWithCurrency(quote.price, quote.currency, { assetCategory: tickerFile?.metadata.assetCategory }), quote.change)],
|
|
219
244
|
["Change", colorBySign(`${formatSignedCurrency(quote.change, quote.currency)} (${formatSignedPercentRaw(quote.changePercent)})`, quote.change)],
|
|
220
|
-
["Open", quote.open != null ?
|
|
245
|
+
["Open", quote.open != null ? formatMarketPriceWithCurrency(quote.open, quote.currency, { assetCategory: tickerFile?.metadata.assetCategory }) : "—"],
|
|
221
246
|
["Day Range", quote.low != null || quote.high != null
|
|
222
|
-
? `${quote.low != null ?
|
|
247
|
+
? `${quote.low != null ? formatMarketPriceWithCurrency(quote.low, quote.currency, { assetCategory: tickerFile?.metadata.assetCategory }) : "—"} - ${quote.high != null ? formatMarketPriceWithCurrency(quote.high, quote.currency, { assetCategory: tickerFile?.metadata.assetCategory }) : "—"}`
|
|
223
248
|
: "—"],
|
|
224
249
|
["52W Range", quote.low52w != null || quote.high52w != null
|
|
225
|
-
? `${quote.low52w != null ?
|
|
250
|
+
? `${quote.low52w != null ? formatMarketPriceWithCurrency(quote.low52w, quote.currency, { assetCategory: tickerFile?.metadata.assetCategory }) : "—"} - ${quote.high52w != null ? formatMarketPriceWithCurrency(quote.high52w, quote.currency, { assetCategory: tickerFile?.metadata.assetCategory }) : "—"}`
|
|
226
251
|
: "—"],
|
|
227
|
-
["Bid / Ask", formatBidAsk(quote.bid, quote.ask, quote.bidSize, quote.askSize, quote.currency)],
|
|
252
|
+
["Bid / Ask", formatBidAsk(quote.bid, quote.ask, quote.bidSize, quote.askSize, quote.currency, tickerFile?.metadata.assetCategory)],
|
|
228
253
|
["Volume", quote.volume != null ? formatNumber(quote.volume, 0) : "—"],
|
|
229
254
|
["Updated", formatTimestamp(quote.lastUpdated)],
|
|
230
255
|
]);
|
|
@@ -232,13 +257,13 @@ export async function buildTickerReport({
|
|
|
232
257
|
appendMetricSection(lines, "Extended Hours", [
|
|
233
258
|
["Pre-Market", quote.preMarketPrice != null
|
|
234
259
|
? colorBySign(
|
|
235
|
-
`${
|
|
260
|
+
`${formatMarketPriceWithCurrency(quote.preMarketPrice, quote.currency, { assetCategory: tickerFile?.metadata.assetCategory })} (${formatSignedPercentRaw(quote.preMarketChangePercent ?? 0)})`,
|
|
236
261
|
quote.preMarketChange ?? 0,
|
|
237
262
|
)
|
|
238
263
|
: "—"],
|
|
239
264
|
["After Hours", quote.postMarketPrice != null
|
|
240
265
|
? colorBySign(
|
|
241
|
-
`${
|
|
266
|
+
`${formatMarketPriceWithCurrency(quote.postMarketPrice, quote.currency, { assetCategory: tickerFile?.metadata.assetCategory })} (${formatSignedPercentRaw(quote.postMarketChangePercent ?? 0)})`,
|
|
242
267
|
quote.postMarketChange ?? 0,
|
|
243
268
|
)
|
|
244
269
|
: "—"],
|
|
@@ -288,18 +313,20 @@ export async function buildTickerReport({
|
|
|
288
313
|
title: item.title,
|
|
289
314
|
meta: [
|
|
290
315
|
item.source,
|
|
291
|
-
|
|
316
|
+
(() => {
|
|
317
|
+
const publishedAt = normalizeTimestamp(item.publishedAt as Date | string | number | undefined);
|
|
318
|
+
return publishedAt == null ? "" : formatTimestamp(publishedAt);
|
|
319
|
+
})(),
|
|
292
320
|
],
|
|
293
321
|
body: item.summary,
|
|
294
322
|
link: item.url,
|
|
295
323
|
})));
|
|
296
324
|
|
|
297
325
|
appendFeedSection(lines, "Recent SEC Filings", recentSecFilings.map((filing) => ({
|
|
298
|
-
title:
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
})}`,
|
|
326
|
+
title: (() => {
|
|
327
|
+
const filingDate = formatFeedDate(filing.filingDate as Date | string | number | undefined);
|
|
328
|
+
return filingDate ? `${filing.form} | ${filingDate}` : filing.form;
|
|
329
|
+
})(),
|
|
303
330
|
meta: [
|
|
304
331
|
filing.items ? `Items ${filing.items}` : "",
|
|
305
332
|
getFilingDescription(filing) ?? "",
|
|
@@ -320,12 +347,15 @@ export async function buildTickerReport({
|
|
|
320
347
|
const pnl = marketValueBase - costBasisBase;
|
|
321
348
|
|
|
322
349
|
lines.push(cliStyles.bold(`${portfolioName} (${position.broker})`));
|
|
323
|
-
lines.push(renderStat(
|
|
350
|
+
lines.push(renderStat(
|
|
351
|
+
"Position",
|
|
352
|
+
`${formatMarketQuantity(position.shares, { assetCategory: tickerFile.metadata.assetCategory, multiplier: position.multiplier })} ${multiplier > 1 ? "contracts" : "shares"} @ ${formatMarketCostWithCurrency(position.avgCost, positionCurrency, { assetCategory: tickerFile.metadata.assetCategory, multiplier: position.multiplier })}`,
|
|
353
|
+
));
|
|
324
354
|
lines.push(renderStat("Cost Basis", formatCurrency(costBasisBase, config.baseCurrency)));
|
|
325
355
|
lines.push(renderStat("Market Value", formatCurrency(marketValueBase, config.baseCurrency)));
|
|
326
356
|
lines.push(renderStat("P&L", colorBySign(formatSignedCurrency(pnl, config.baseCurrency), pnl)));
|
|
327
357
|
if (position.markPrice != null) {
|
|
328
|
-
lines.push(renderStat("Mark",
|
|
358
|
+
lines.push(renderStat("Mark", formatMarketPriceWithCurrency(position.markPrice, positionCurrency, { assetCategory: tickerFile.metadata.assetCategory, multiplier: position.multiplier })));
|
|
329
359
|
}
|
|
330
360
|
if (index < tickerFile.metadata.positions.length - 1) {
|
|
331
361
|
lines.push(cliStyles.muted("-".repeat(24)));
|
package/src/cli/helpers.ts
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
formatNumber,
|
|
5
5
|
formatPercentRaw,
|
|
6
6
|
} from "../utils/format";
|
|
7
|
+
import { formatMarketPriceWithCurrency } from "../utils/market-format";
|
|
7
8
|
export { slugifyName } from "../utils/slugify";
|
|
8
9
|
import type { AppConfig } from "../types/config";
|
|
9
10
|
import type { Watchlist, TickerRecord } from "../types/ticker";
|
|
@@ -42,13 +43,14 @@ export function formatBidAsk(
|
|
|
42
43
|
bidSize: number | undefined,
|
|
43
44
|
askSize: number | undefined,
|
|
44
45
|
currency: string,
|
|
46
|
+
assetCategory?: string,
|
|
45
47
|
): string {
|
|
46
48
|
if (bid == null && ask == null) return "—";
|
|
47
49
|
const bidText = bid != null
|
|
48
|
-
? `${
|
|
50
|
+
? `${formatMarketPriceWithCurrency(bid, currency, { assetCategory })}${bidSize != null ? ` x ${formatNumber(bidSize, 0)}` : ""}`
|
|
49
51
|
: "—";
|
|
50
52
|
const askText = ask != null
|
|
51
|
-
? `${
|
|
53
|
+
? `${formatMarketPriceWithCurrency(ask, currency, { assetCategory })}${askSize != null ? ` x ${formatNumber(askSize, 0)}` : ""}`
|
|
52
54
|
: "—";
|
|
53
55
|
return `${bidText} / ${askText}`;
|
|
54
56
|
}
|