gloomberb 0.3.7 → 0.4.1
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 +19 -8
- package/package.json +1 -1
- package/src/app.tsx +45 -69
- package/src/cli/base-converter.ts +31 -0
- package/src/cli/commands/search.ts +11 -3
- package/src/cli/commands/ticker.ts +22 -11
- package/src/cli/context.ts +27 -32
- package/src/cli/helpers.ts +1 -4
- package/src/cli/index.ts +140 -123
- package/src/cli/registry.test.ts +226 -0
- package/src/cli/registry.ts +201 -0
- package/src/cli.test.ts +219 -0
- package/src/components/chart/chart-controller.ts +309 -0
- package/src/components/chart/chart-data.ts +5 -6
- package/src/components/chart/chart-pane-settings.ts +27 -0
- package/src/components/chart/chart-resolution.test.ts +56 -0
- package/src/components/chart/chart-resolution.ts +362 -0
- package/src/components/chart/chart-types.ts +12 -4
- package/src/components/chart/chart-viewport.test.ts +25 -0
- package/src/components/chart/chart-viewport.ts +28 -0
- package/src/components/chart/comparison-chart-data.ts +20 -27
- package/src/components/chart/comparison-stock-chart.tsx +374 -99
- package/src/components/chart/stock-chart-renderer-switch.test.tsx +124 -0
- package/src/components/chart/stock-chart.tsx +455 -106
- package/src/components/command-bar/__snapshots__/command-bar.test.tsx.snap +4 -4
- package/src/components/command-bar/command-bar.test.tsx +650 -5
- package/src/components/command-bar/command-bar.tsx +363 -63
- package/src/components/command-bar/command-registry.ts +8 -0
- package/src/components/command-bar/helpers.test.ts +23 -1
- package/src/components/command-bar/helpers.ts +14 -5
- package/src/components/command-bar/pane-template-display.ts +12 -0
- package/src/components/command-bar/root-shortcuts.ts +2 -1
- package/src/components/command-bar/workflow-ops.ts +4 -2
- package/src/components/detail-feed-view.test.tsx +124 -49
- package/src/components/detail-feed-view.tsx +396 -160
- package/src/components/layout/floating-pane.tsx +4 -2
- package/src/components/layout/header.tsx +3 -3
- package/src/components/layout/pane-header.tsx +26 -2
- package/src/components/layout/pane-sizing.ts +6 -0
- package/src/components/layout/pane.tsx +5 -1
- package/src/components/layout/shell.test.tsx +77 -5
- package/src/components/layout/shell.tsx +67 -52
- package/src/components/layout/status-bar.test.tsx +66 -0
- package/src/components/layout/status-bar.tsx +26 -5
- package/src/components/onboarding/onboarding-wizard.test.tsx +65 -0
- package/src/components/onboarding/onboarding-wizard.tsx +5 -6
- package/src/components/ticker-list-table.test.tsx +83 -0
- package/src/components/ticker-list-table.tsx +181 -97
- package/src/components/ui/data-table.tsx +50 -5
- package/src/components/ui/index.ts +1 -0
- package/src/components/ui/page-stack-view.tsx +60 -0
- package/src/components/use-double-click-activation.ts +31 -0
- package/src/index.tsx +10 -10
- package/src/market-data/coordinator.test.ts +92 -4
- package/src/market-data/coordinator.ts +32 -5
- package/src/market-data/hooks.test.tsx +120 -0
- package/src/market-data/hooks.tsx +53 -16
- package/src/market-data/request-types.ts +4 -2
- package/src/market-data/selectors.ts +24 -58
- package/src/plugins/builtin/chat-controller.test.ts +400 -3
- package/src/plugins/builtin/chat-controller.ts +328 -35
- package/src/plugins/builtin/chat.test.tsx +118 -6
- package/src/plugins/builtin/chat.tsx +180 -116
- package/src/plugins/builtin/comparison-chart.test.tsx +66 -5
- package/src/plugins/builtin/comparison-chart.tsx +21 -1
- package/src/plugins/builtin/layout-manager.tsx +9 -0
- package/src/plugins/builtin/news.tsx +1 -11
- package/src/plugins/builtin/portfolio-list/cli/commands.ts +656 -0
- package/src/plugins/builtin/portfolio-list/command-bar.ts +122 -0
- package/src/plugins/builtin/portfolio-list/header.tsx +16 -34
- package/src/plugins/builtin/portfolio-list/index.tsx +5 -0
- package/src/plugins/builtin/portfolio-list/metrics.test.ts +61 -0
- package/src/plugins/builtin/portfolio-list/metrics.ts +2 -2
- package/src/plugins/builtin/portfolio-list/mutations.test.ts +187 -0
- package/src/plugins/builtin/portfolio-list/mutations.ts +226 -0
- package/src/plugins/builtin/portfolio-list/pane.tsx +39 -17
- package/src/plugins/builtin/portfolio-list/table.tsx +11 -2
- package/src/plugins/builtin/portfolio-list/use-throttled-cursor-symbol.test.tsx +163 -0
- package/src/plugins/builtin/portfolio-list/use-throttled-cursor-symbol.ts +81 -0
- package/src/plugins/builtin/sec.tsx +1 -11
- package/src/plugins/builtin/ticker-detail/chart-tab.tsx +13 -2
- package/src/plugins/builtin/ticker-detail/financials-tab.tsx +21 -0
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +45 -10
- package/src/plugins/builtin/ticker-detail/pane.tsx +35 -17
- package/src/plugins/builtin/ticker-detail/settings.ts +19 -1
- package/src/plugins/builtin/ticker-detail.test.tsx +47 -0
- package/src/plugins/catalog.ts +65 -0
- package/src/plugins/ibkr/broker-adapter.ts +28 -0
- package/src/plugins/ibkr/gateway-service.test.ts +67 -1
- package/src/plugins/ibkr/gateway-service.ts +147 -7
- package/src/plugins/ibkr/index.tsx +7 -1
- package/src/plugins/pane-manager.test.ts +1 -1
- package/src/plugins/plugin-runtime.tsx +27 -7
- package/src/plugins/prediction-markets/cli.ts +28 -0
- package/src/plugins/prediction-markets/controller-data.ts +132 -17
- package/src/plugins/prediction-markets/controller.ts +142 -122
- package/src/plugins/prediction-markets/index.tsx +7 -1
- package/src/plugins/prediction-markets/launch.ts +51 -16
- package/src/plugins/prediction-markets/metrics.ts +9 -5
- package/src/plugins/prediction-markets/pane.test.tsx +197 -57
- package/src/plugins/prediction-markets/pane.tsx +80 -155
- package/src/plugins/prediction-markets/services/fetch.ts +4 -0
- package/src/plugins/prediction-markets/table.tsx +6 -3
- package/src/plugins/prediction-markets/test-helpers.tsx +20 -5
- package/src/plugins/prediction-markets/types.ts +0 -1
- package/src/plugins/registry.ts +23 -0
- package/src/sources/gloomberb-cloud.test.ts +29 -0
- package/src/sources/gloomberb-cloud.ts +89 -1
- package/src/sources/provider-router.test.ts +265 -3
- package/src/sources/provider-router.ts +239 -11
- package/src/sources/yahoo-finance.test.ts +26 -0
- package/src/sources/yahoo-finance.ts +54 -3
- package/src/state/app-context-selector.test.tsx +99 -0
- package/src/state/app-context.test.ts +18 -0
- package/src/state/app-context.tsx +170 -65
- package/src/state/selectors.ts +25 -0
- package/src/theme/colors.ts +8 -8
- package/src/theme/themes.test.ts +2 -0
- package/src/theme/themes.ts +22 -10
- package/src/types/broker.ts +21 -0
- package/src/types/config.ts +17 -1
- package/src/types/data-provider.ts +18 -0
- package/src/types/financials.ts +31 -1
- package/src/types/plugin.ts +70 -0
- package/src/updater.test.ts +2 -0
- package/src/utils/api-client.test.ts +57 -0
- package/src/utils/api-client.ts +47 -12
- package/src/utils/back-navigation.ts +27 -0
- package/src/utils/format.test.ts +14 -0
- package/src/utils/format.ts +3 -3
- package/src/utils/market-status.ts +46 -1
- package/src/utils/quote-freshness.ts +127 -0
- package/src/utils/quote-resolution.test.ts +277 -0
- package/src/utils/quote-resolution.ts +641 -0
- package/src/utils/slugify.ts +3 -0
- package/src/utils/timestamp.ts +25 -0
- package/src/version.ts +1 -1
- package/src/cli/commands/portfolio.ts +0 -210
- package/src/cli/commands/watchlist.ts +0 -210
- package/src/plugins/builtin/data-providers.ts +0 -16
- package/src/sources/exchange-rates.ts +0 -18
- package/src/utils/ascii-chart.ts +0 -172
- package/src/utils/ticker-tokenizer.test.ts +0 -48
package/README.md
CHANGED
|
@@ -36,7 +36,7 @@ Then run `gloomberb`.
|
|
|
36
36
|
|
|
37
37
|
Everything, from the portfolio list to broker integrations, is a plugin.
|
|
38
38
|
|
|
39
|
-
Plugins can add tabs, columns, commands, status bar widgets, and more.
|
|
39
|
+
Plugins can add tabs, columns, command bar commands, CLI commands, status bar widgets, and more.
|
|
40
40
|
|
|
41
41
|
See **[PLUGINS.md](PLUGINS.md)** for the plugin API and the shared UI surface available through `gloomberb/components`.
|
|
42
42
|
|
|
@@ -56,12 +56,6 @@ See **[PLUGINS.md](PLUGINS.md)** for the plugin API and the shared UI surface av
|
|
|
56
56
|
| **Alerts** | WIP |
|
|
57
57
|
| **Scanner** | WIP |
|
|
58
58
|
|
|
59
|
-
#### AI plugin
|
|
60
|
-
|
|
61
|
-
- The ticker detail pane keeps its `Ask AI` tab for per-company questions.
|
|
62
|
-
- The `AI Screener` pane lets you create multiple prompt-based screening tabs, refresh them, force reruns, edit the prompt, and review the last run time.
|
|
63
|
-
- Use the `AI <prompt>` shortcut from the command bar to open a new screener pane seeded with a prompt.
|
|
64
|
-
|
|
65
59
|
### Data providers
|
|
66
60
|
|
|
67
61
|
| Provider | Description |
|
|
@@ -78,6 +72,23 @@ See **[PLUGINS.md](PLUGINS.md)** for the plugin API and the shared UI surface av
|
|
|
78
72
|
|
|
79
73
|
Toggleable plugins can be enabled/disabled from the command bar screen (`Ctrl+p`).
|
|
80
74
|
|
|
75
|
+
## 💻 CLI
|
|
76
|
+
|
|
77
|
+
Running `gloomberb` with no arguments launches the terminal UI. Use `gloomberb help` to see the full command list.
|
|
78
|
+
Feature-owned root commands such as `portfolio`, `watchlist`, and `predictions` are registered by their plugins rather than hardcoded in the main CLI switch.
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
gloomberb
|
|
82
|
+
gloomberb help
|
|
83
|
+
gloomberb search NVDA
|
|
84
|
+
gloomberb ticker AAPL
|
|
85
|
+
gloomberb portfolio create Research
|
|
86
|
+
gloomberb portfolio position set Research NVDA 10 400
|
|
87
|
+
gloomberb watchlist list
|
|
88
|
+
gloomberb plugins
|
|
89
|
+
gloomberb predictions world
|
|
90
|
+
```
|
|
91
|
+
|
|
81
92
|
## ⌨️ Keyboard Shortcuts
|
|
82
93
|
|
|
83
94
|
| Key | Action |
|
|
@@ -85,7 +96,7 @@ Toggleable plugins can be enabled/disabled from the command bar screen (`Ctrl+p`
|
|
|
85
96
|
| `Ctrl+P` or `` ` `` | Open command bar |
|
|
86
97
|
| `Ctrl+x` | Close current window |
|
|
87
98
|
| `Tab` | Switch between panels |
|
|
88
|
-
`j` / `k` | Navigate ticker list |
|
|
99
|
+
| `j` / `k` | Navigate ticker list |
|
|
89
100
|
| `h` / `l` | Switch tabs |
|
|
90
101
|
| `Ctrl+,` | Open settings |
|
|
91
102
|
| `m` | Cycle chart mode in the chart tab |
|
package/package.json
CHANGED
package/src/app.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState, useEffect, useCallback, useRef, useMemo } from "react";
|
|
1
|
+
import { useState, useEffect, useCallback, useRef, useMemo, useLayoutEffect } from "react";
|
|
2
2
|
import { useKeyboard, useRenderer } from "@opentui/react";
|
|
3
3
|
import type { CliRenderer } from "@opentui/core";
|
|
4
4
|
import {
|
|
@@ -32,7 +32,7 @@ import {
|
|
|
32
32
|
type PaneBinding,
|
|
33
33
|
type PaneInstanceConfig,
|
|
34
34
|
} from "./types/config";
|
|
35
|
-
import type { PaneTemplateCreateOptions, PaneTemplateInstanceConfig, WizardStep } from "./types/plugin";
|
|
35
|
+
import type { CliLaunchRequest, PaneTemplateCreateOptions, PaneTemplateInstanceConfig, WizardStep } from "./types/plugin";
|
|
36
36
|
import type { PaneSettingField } from "./types/plugin";
|
|
37
37
|
import type { TickerRecord } from "./types/ticker";
|
|
38
38
|
import type { DataProvider } from "./types/data-provider";
|
|
@@ -41,36 +41,17 @@ import type { BrokerAccount } from "./types/trading";
|
|
|
41
41
|
import { resolveTickerSearch, upsertTickerFromSearchResult } from "./utils/ticker-search";
|
|
42
42
|
|
|
43
43
|
// Built-in plugins
|
|
44
|
-
import { portfolioListPlugin } from "./plugins/builtin/portfolio-list";
|
|
45
|
-
import { tickerDetailPlugin } from "./plugins/builtin/ticker-detail";
|
|
46
|
-
import { manualEntryPlugin } from "./plugins/builtin/manual-entry";
|
|
47
|
-
import { ibkrPlugin } from "./plugins/ibkr";
|
|
48
44
|
import {
|
|
49
45
|
clearPersistedIbkrAccounts,
|
|
50
46
|
loadPersistedIbkrAccountMap,
|
|
51
47
|
} from "./plugins/ibkr/account-cache";
|
|
52
48
|
import { getIbkrConfigIdentity } from "./plugins/ibkr/config";
|
|
53
|
-
import { newsPlugin } from "./plugins/builtin/news";
|
|
54
|
-
import { secPlugin } from "./plugins/builtin/sec";
|
|
55
|
-
import { optionsPlugin } from "./plugins/builtin/options";
|
|
56
|
-
import { notesPlugin } from "./plugins/builtin/notes";
|
|
57
|
-
import { aiPlugin } from "./plugins/builtin/ai/index";
|
|
58
|
-
import { gloomberbCloudPlugin } from "./plugins/builtin/chat";
|
|
59
49
|
import { chatController } from "./plugins/builtin/chat-controller";
|
|
60
|
-
import {
|
|
61
|
-
import {
|
|
62
|
-
import { debugPlugin } from "./plugins/builtin/debug";
|
|
63
|
-
import { layoutManagerPlugin, setLayoutManagerDispatch } from "./plugins/builtin/layout-manager";
|
|
64
|
-
import { yahooPlugin } from "./plugins/builtin/yahoo";
|
|
65
|
-
import { predictionMarketsPlugin } from "./plugins/prediction-markets";
|
|
66
|
-
import {
|
|
67
|
-
applyPredictionLaunchIntentToConfig,
|
|
68
|
-
applyPredictionLaunchIntentToSessionSnapshot,
|
|
69
|
-
type PredictionLaunchIntent,
|
|
70
|
-
} from "./plugins/prediction-markets/launch";
|
|
50
|
+
import { setLayoutManagerDispatch } from "./plugins/builtin/layout-manager";
|
|
51
|
+
import { getLoadablePlugins } from "./plugins/catalog";
|
|
71
52
|
import { saveConfig } from "./data/config-store";
|
|
72
53
|
import { Toaster, toast } from "@opentui-ui/toast/react";
|
|
73
|
-
import { canSelfUpdate, checkForUpdateDetailed, performUpdate } from "./updater";
|
|
54
|
+
import { canSelfUpdate, checkForUpdateDetailed, performUpdate, type ReleaseInfo } from "./updater";
|
|
74
55
|
import { VERSION } from "./version";
|
|
75
56
|
import { join } from "path";
|
|
76
57
|
import {
|
|
@@ -107,6 +88,7 @@ import {
|
|
|
107
88
|
applyPaneSettingFieldValue as applyPaneSettingFieldValueShared,
|
|
108
89
|
createPaneTemplateOrThrow,
|
|
109
90
|
} from "./components/command-bar/workflow-ops";
|
|
91
|
+
import { getPaneTemplateDisplayLabel } from "./components/command-bar/pane-template-display";
|
|
110
92
|
import { debugLog } from "./utils/debug-log";
|
|
111
93
|
import { MarketDataCoordinator, setSharedMarketDataCoordinator } from "./market-data/coordinator";
|
|
112
94
|
import { instrumentFromTicker } from "./market-data/request-types";
|
|
@@ -543,6 +525,13 @@ function AppInner({ pluginRegistry, tickerRepository, dataProvider, marketData,
|
|
|
543
525
|
}
|
|
544
526
|
}, [state.config.brokerInstances, importBrokerPositions]);
|
|
545
527
|
|
|
528
|
+
const startUpdate = useCallback((release: ReleaseInfo) => {
|
|
529
|
+
dispatch({ type: "SET_UPDATE_PROGRESS", progress: { phase: "downloading", percent: 0 } });
|
|
530
|
+
void performUpdate(release, (progress) => {
|
|
531
|
+
dispatch({ type: "SET_UPDATE_PROGRESS", progress });
|
|
532
|
+
});
|
|
533
|
+
}, [dispatch]);
|
|
534
|
+
|
|
546
535
|
const runUpdateCheck = useCallback(async (manual = false) => {
|
|
547
536
|
if (manual) {
|
|
548
537
|
dispatch({ type: "SET_UPDATE_CHECK_IN_PROGRESS", checking: true });
|
|
@@ -584,6 +573,12 @@ function AppInner({ pluginRegistry, tickerRepository, dataProvider, marketData,
|
|
|
584
573
|
void runUpdateCheck(false);
|
|
585
574
|
}, [runUpdateCheck]);
|
|
586
575
|
|
|
576
|
+
useEffect(() => {
|
|
577
|
+
if (!state.updateAvailable || state.updateProgress || state.updateCheckInProgress) return;
|
|
578
|
+
if (!canSelfUpdate(state.updateAvailable)) return;
|
|
579
|
+
startUpdate(state.updateAvailable);
|
|
580
|
+
}, [startUpdate, state.updateAvailable, state.updateCheckInProgress, state.updateProgress]);
|
|
581
|
+
|
|
587
582
|
// Load tickers on mount
|
|
588
583
|
useEffect(() => {
|
|
589
584
|
if (state.initialized || (globalThis as any).__gloomInitStarted) return;
|
|
@@ -966,7 +961,7 @@ function AppInner({ pluginRegistry, tickerRepository, dataProvider, marketData,
|
|
|
966
961
|
});
|
|
967
962
|
} catch (error) {
|
|
968
963
|
pluginRegistry.showToastFn(
|
|
969
|
-
error instanceof Error ? error.message : `Could not create ${template.
|
|
964
|
+
error instanceof Error ? error.message : `Could not create ${getPaneTemplateDisplayLabel(template).toLowerCase()}.`,
|
|
970
965
|
{ type: "info" },
|
|
971
966
|
);
|
|
972
967
|
}
|
|
@@ -1190,9 +1185,7 @@ function AppInner({ pluginRegistry, tickerRepository, dataProvider, marketData,
|
|
|
1190
1185
|
dispatch({ type: "SET_COMMAND_BAR", open: true, query: "" });
|
|
1191
1186
|
}
|
|
1192
1187
|
} else if (event.name === "u" && state.updateAvailable && !state.updateProgress && !state.updateCheckInProgress && canSelfUpdate(state.updateAvailable)) {
|
|
1193
|
-
|
|
1194
|
-
dispatch({ type: "SET_UPDATE_PROGRESS", progress });
|
|
1195
|
-
});
|
|
1188
|
+
startUpdate(state.updateAvailable);
|
|
1196
1189
|
} else {
|
|
1197
1190
|
// Plugin keyboard shortcuts (built-ins take priority)
|
|
1198
1191
|
const disabledPlugins = new Set(state.config.disabledPlugins || []);
|
|
@@ -1232,32 +1225,36 @@ interface AppProps {
|
|
|
1232
1225
|
config: AppConfig;
|
|
1233
1226
|
renderer: CliRenderer;
|
|
1234
1227
|
externalPlugins?: import("./plugins/loader").LoadedExternalPlugin[];
|
|
1235
|
-
|
|
1228
|
+
cliLaunchRequest?: CliLaunchRequest | null;
|
|
1236
1229
|
}
|
|
1237
1230
|
|
|
1238
1231
|
export function App({
|
|
1239
1232
|
config: initialConfig,
|
|
1240
1233
|
renderer,
|
|
1241
1234
|
externalPlugins = [],
|
|
1242
|
-
|
|
1235
|
+
cliLaunchRequest = null,
|
|
1243
1236
|
}: AppProps) {
|
|
1244
|
-
const
|
|
1245
|
-
if (!
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1237
|
+
const initialCliLaunch = useMemo(() => {
|
|
1238
|
+
if (!cliLaunchRequest) {
|
|
1239
|
+
return { config: initialConfig, launchState: undefined };
|
|
1240
|
+
}
|
|
1241
|
+
return cliLaunchRequest.applyConfig(initialConfig, {
|
|
1242
|
+
terminalWidth: renderer.terminalWidth,
|
|
1243
|
+
terminalHeight: renderer.terminalHeight,
|
|
1244
|
+
});
|
|
1245
|
+
}, [cliLaunchRequest, initialConfig, renderer.terminalHeight, renderer.terminalWidth]);
|
|
1246
|
+
const cliLaunchStateRef = useRef(initialCliLaunch.launchState);
|
|
1251
1247
|
|
|
1252
1248
|
const [config, setConfig] = useState(() => {
|
|
1253
|
-
return
|
|
1249
|
+
return initialCliLaunch.config;
|
|
1254
1250
|
});
|
|
1255
1251
|
const [showOnboarding, setShowOnboarding] = useState(!initialConfig.onboardingComplete);
|
|
1256
1252
|
|
|
1257
1253
|
useEffect(() => bindAppActivity(renderer), [renderer]);
|
|
1258
1254
|
|
|
1259
|
-
|
|
1260
|
-
|
|
1255
|
+
useLayoutEffect(() => {
|
|
1256
|
+
if (config.theme) applyTheme(config.theme);
|
|
1257
|
+
}, [config.theme]);
|
|
1261
1258
|
|
|
1262
1259
|
const services = useMemo(() => {
|
|
1263
1260
|
const dbPath = join(config.dataDir, ".gloomberb-cache.db");
|
|
@@ -1271,25 +1268,8 @@ export function App({
|
|
|
1271
1268
|
pluginRegistry.getConfigFn = () => config;
|
|
1272
1269
|
pluginRegistry.getLayoutFn = () => config.layout;
|
|
1273
1270
|
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
pluginRegistry.register(portfolioListPlugin);
|
|
1277
|
-
pluginRegistry.register(tickerDetailPlugin);
|
|
1278
|
-
pluginRegistry.register(manualEntryPlugin);
|
|
1279
|
-
pluginRegistry.register(ibkrPlugin);
|
|
1280
|
-
pluginRegistry.register(layoutManagerPlugin);
|
|
1281
|
-
pluginRegistry.register(newsPlugin);
|
|
1282
|
-
pluginRegistry.register(secPlugin);
|
|
1283
|
-
pluginRegistry.register(optionsPlugin);
|
|
1284
|
-
pluginRegistry.register(notesPlugin);
|
|
1285
|
-
pluginRegistry.register(aiPlugin);
|
|
1286
|
-
pluginRegistry.register(helpPlugin);
|
|
1287
|
-
pluginRegistry.register(comparisonChartPlugin);
|
|
1288
|
-
pluginRegistry.register(predictionMarketsPlugin);
|
|
1289
|
-
pluginRegistry.register(debugPlugin);
|
|
1290
|
-
|
|
1291
|
-
for (const { plugin, error } of externalPlugins) {
|
|
1292
|
-
if (!error) pluginRegistry.register(plugin);
|
|
1271
|
+
for (const plugin of getLoadablePlugins(externalPlugins)) {
|
|
1272
|
+
pluginRegistry.register(plugin);
|
|
1293
1273
|
}
|
|
1294
1274
|
|
|
1295
1275
|
return {
|
|
@@ -1309,6 +1289,7 @@ export function App({
|
|
|
1309
1289
|
useEffect(() => {
|
|
1310
1290
|
return () => {
|
|
1311
1291
|
setSharedMarketDataCoordinator(null);
|
|
1292
|
+
services.pluginRegistry.destroy();
|
|
1312
1293
|
services.persistence.close();
|
|
1313
1294
|
};
|
|
1314
1295
|
}, [services]);
|
|
@@ -1316,20 +1297,15 @@ export function App({
|
|
|
1316
1297
|
const sessionSnapshot = useMemo(() => {
|
|
1317
1298
|
const persisted = services.persistence.sessions.get<AppSessionSnapshot>(APP_SESSION_ID, APP_SESSION_SCHEMA_VERSION)?.value ?? null;
|
|
1318
1299
|
const reconciled = reconcileAppSessionSnapshot(config, persisted);
|
|
1319
|
-
if (!
|
|
1320
|
-
return reconciled;
|
|
1321
|
-
}
|
|
1322
|
-
const paneInstanceId = config.layout.instances.find((instance) => instance.paneId === "prediction-markets")?.instanceId;
|
|
1323
|
-
if (!paneInstanceId) {
|
|
1300
|
+
if (!cliLaunchRequest?.applySessionSnapshot) {
|
|
1324
1301
|
return reconciled;
|
|
1325
1302
|
}
|
|
1326
|
-
return
|
|
1303
|
+
return cliLaunchRequest.applySessionSnapshot(
|
|
1327
1304
|
config,
|
|
1328
1305
|
reconciled,
|
|
1329
|
-
|
|
1330
|
-
predictionLaunchIntent,
|
|
1306
|
+
cliLaunchStateRef.current,
|
|
1331
1307
|
);
|
|
1332
|
-
}, [
|
|
1308
|
+
}, [cliLaunchRequest, config, services.persistence.sessions]);
|
|
1333
1309
|
|
|
1334
1310
|
if (showOnboarding) {
|
|
1335
1311
|
return (
|
|
@@ -1337,7 +1313,7 @@ export function App({
|
|
|
1337
1313
|
config={config}
|
|
1338
1314
|
pluginRegistry={services.pluginRegistry}
|
|
1339
1315
|
onComplete={(updatedConfig) => {
|
|
1340
|
-
setConfig(
|
|
1316
|
+
setConfig(updatedConfig);
|
|
1341
1317
|
setShowOnboarding(false);
|
|
1342
1318
|
}}
|
|
1343
1319
|
/>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { DataProvider } from "../types/data-provider";
|
|
2
|
+
|
|
3
|
+
export function createBaseConverter(dataProvider: Pick<DataProvider, "getExchangeRate">, baseCurrency: string) {
|
|
4
|
+
const rateCache = new Map<string, number>([["USD", 1]]);
|
|
5
|
+
|
|
6
|
+
const getRate = async (currency: string): Promise<number> => {
|
|
7
|
+
const normalizedCurrency = currency.toUpperCase();
|
|
8
|
+
const cached = rateCache.get(normalizedCurrency);
|
|
9
|
+
if (cached != null) return cached;
|
|
10
|
+
try {
|
|
11
|
+
const rate = await dataProvider.getExchangeRate(normalizedCurrency);
|
|
12
|
+
rateCache.set(normalizedCurrency, rate);
|
|
13
|
+
return rate;
|
|
14
|
+
} catch {
|
|
15
|
+
return 1;
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
return async (value: number, fromCurrency: string): Promise<number> => {
|
|
20
|
+
const normalizedFrom = fromCurrency.toUpperCase();
|
|
21
|
+
const normalizedBase = baseCurrency.toUpperCase();
|
|
22
|
+
if (normalizedFrom === normalizedBase) return value;
|
|
23
|
+
|
|
24
|
+
const [fromRate, baseRate] = await Promise.all([
|
|
25
|
+
getRate(normalizedFrom),
|
|
26
|
+
getRate(normalizedBase),
|
|
27
|
+
]);
|
|
28
|
+
if (baseRate === 0) return value;
|
|
29
|
+
return (value * fromRate) / baseRate;
|
|
30
|
+
};
|
|
31
|
+
}
|
|
@@ -11,6 +11,12 @@ import {
|
|
|
11
11
|
} from "../../utils/ticker-search";
|
|
12
12
|
import { initMarketData } from "../context";
|
|
13
13
|
import { fail } from "../errors";
|
|
14
|
+
import type { MarketContext } from "../types";
|
|
15
|
+
|
|
16
|
+
interface SearchCommandDependencies {
|
|
17
|
+
initMarketData?: () => Promise<MarketContext>;
|
|
18
|
+
fail?: (message: string, details?: string) => never;
|
|
19
|
+
}
|
|
14
20
|
|
|
15
21
|
export async function searchCandidatesForCli({
|
|
16
22
|
query,
|
|
@@ -97,13 +103,15 @@ export function buildSearchReport({
|
|
|
97
103
|
return lines.join("\n");
|
|
98
104
|
}
|
|
99
105
|
|
|
100
|
-
export async function search(query: string) {
|
|
106
|
+
export async function search(query: string, dependencies: SearchCommandDependencies = {}) {
|
|
101
107
|
const trimmedQuery = query.trim();
|
|
108
|
+
const failCommand = dependencies.fail ?? fail;
|
|
109
|
+
const initMarketDataFn = dependencies.initMarketData ?? initMarketData;
|
|
102
110
|
if (!trimmedQuery) {
|
|
103
|
-
|
|
111
|
+
failCommand("Usage: gloomberb search <query>");
|
|
104
112
|
}
|
|
105
113
|
|
|
106
|
-
const { store, dataProvider, persistence } = await
|
|
114
|
+
const { store, dataProvider, persistence } = await initMarketDataFn();
|
|
107
115
|
const tickers = await store.loadAllTickers();
|
|
108
116
|
const candidates = await searchCandidatesForCli({
|
|
109
117
|
query: trimmedQuery,
|
|
@@ -15,8 +15,10 @@ import type { AppConfig } from "../../types/config";
|
|
|
15
15
|
import type { FinancialStatement, TickerFinancials } from "../../types/financials";
|
|
16
16
|
import type { NewsItem, SecFilingItem } from "../../types/data-provider";
|
|
17
17
|
import type { TickerRecord } from "../../types/ticker";
|
|
18
|
-
import { createBaseConverter
|
|
18
|
+
import { createBaseConverter } from "../base-converter";
|
|
19
|
+
import { initMarketData } from "../context";
|
|
19
20
|
import { closeAndFail } from "../errors";
|
|
21
|
+
import type { MarketContext } from "../types";
|
|
20
22
|
import {
|
|
21
23
|
formatBidAsk,
|
|
22
24
|
formatNullableCompact,
|
|
@@ -33,6 +35,11 @@ import { isUsEquityTicker } from "../../utils/sec";
|
|
|
33
35
|
const NEWS_ITEM_LIMIT = 5;
|
|
34
36
|
const SEC_FILING_LIMIT = 5;
|
|
35
37
|
|
|
38
|
+
interface TickerCommandDependencies {
|
|
39
|
+
initMarketData?: () => Promise<MarketContext>;
|
|
40
|
+
closeAndFail?: typeof closeAndFail;
|
|
41
|
+
}
|
|
42
|
+
|
|
36
43
|
function appendMetricSection(lines: string[], title: string, metrics: Array<[string, string]>) {
|
|
37
44
|
const populated = metrics.filter(([, value]) => value !== "—");
|
|
38
45
|
if (populated.length === 0) return;
|
|
@@ -329,30 +336,34 @@ export async function buildTickerReport({
|
|
|
329
336
|
return lines.join("\n");
|
|
330
337
|
}
|
|
331
338
|
|
|
332
|
-
export async function ticker(symbol: string) {
|
|
333
|
-
const
|
|
339
|
+
export async function ticker(symbol: string, dependencies: TickerCommandDependencies = {}) {
|
|
340
|
+
const initMarketDataFn = dependencies.initMarketData ?? initMarketData;
|
|
341
|
+
const closeAndFailCommand = dependencies.closeAndFail ?? closeAndFail;
|
|
342
|
+
const { config, store, dataProvider, persistence, dataDir } = await initMarketDataFn();
|
|
334
343
|
const normalized = symbol.trim().toUpperCase();
|
|
335
344
|
const tickerFile = await store.loadTicker(normalized);
|
|
336
345
|
const exchange = tickerFile?.metadata.exchange ?? "";
|
|
337
346
|
const toBase = createBaseConverter(dataProvider, config.baseCurrency);
|
|
338
347
|
|
|
339
|
-
let financials: TickerFinancials;
|
|
348
|
+
let financials: TickerFinancials | null = null;
|
|
340
349
|
try {
|
|
341
350
|
financials = await dataProvider.getTickerFinancials(normalized, exchange);
|
|
342
351
|
} catch (err: any) {
|
|
343
|
-
|
|
352
|
+
closeAndFailCommand(persistence, `Failed to fetch data for ${normalized}.`, err?.message);
|
|
344
353
|
}
|
|
345
354
|
|
|
346
|
-
if (!financials
|
|
347
|
-
|
|
355
|
+
if (!financials?.quote) {
|
|
356
|
+
closeAndFailCommand(persistence, `No quote data available for ${normalized}.`);
|
|
348
357
|
}
|
|
358
|
+
const resolvedFinancials = financials as TickerFinancials;
|
|
359
|
+
const quote = resolvedFinancials.quote!;
|
|
349
360
|
|
|
350
361
|
const notesFiles = new NotesFiles(dataDir);
|
|
351
362
|
const [notesResult, newsResult, secFilingsResult] = await Promise.allSettled([
|
|
352
363
|
notesFiles.load(normalized),
|
|
353
|
-
dataProvider.getNews(normalized, NEWS_ITEM_LIMIT, exchange ||
|
|
354
|
-
shouldFetchSecFilings(tickerFile,
|
|
355
|
-
? dataProvider.getSecFilings(normalized, SEC_FILING_LIMIT, exchange ||
|
|
364
|
+
dataProvider.getNews(normalized, NEWS_ITEM_LIMIT, exchange || quote.exchangeName || ""),
|
|
365
|
+
shouldFetchSecFilings(tickerFile, resolvedFinancials) && dataProvider.getSecFilings
|
|
366
|
+
? dataProvider.getSecFilings(normalized, SEC_FILING_LIMIT, exchange || quote.exchangeName || "")
|
|
356
367
|
: Promise.resolve([]),
|
|
357
368
|
]);
|
|
358
369
|
|
|
@@ -363,7 +374,7 @@ export async function ticker(symbol: string) {
|
|
|
363
374
|
console.log(await buildTickerReport({
|
|
364
375
|
symbol: normalized,
|
|
365
376
|
tickerFile,
|
|
366
|
-
financials,
|
|
377
|
+
financials: resolvedFinancials,
|
|
367
378
|
config,
|
|
368
379
|
toBase,
|
|
369
380
|
notes,
|
package/src/cli/context.ts
CHANGED
|
@@ -5,10 +5,32 @@ import { AppPersistence } from "../data/app-persistence";
|
|
|
5
5
|
import { TickerRepository } from "../data/ticker-repository";
|
|
6
6
|
import { ProviderRouter } from "../sources/provider-router";
|
|
7
7
|
import type { DataProvider } from "../types/data-provider";
|
|
8
|
-
import {
|
|
8
|
+
import type { AppConfig } from "../types/config";
|
|
9
|
+
import type { GloomPlugin } from "../types/plugin";
|
|
10
|
+
import { getLoadablePlugins } from "../plugins/catalog";
|
|
11
|
+
import { createBaseConverter } from "./base-converter";
|
|
9
12
|
import { fail } from "./errors";
|
|
10
13
|
import type { ConfigContext, MarketContext } from "./types";
|
|
11
14
|
|
|
15
|
+
interface CliContextOptions {
|
|
16
|
+
plugins?: GloomPlugin[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function resolveCliDataProviders(config: AppConfig, plugins: GloomPlugin[]): DataProvider[] {
|
|
20
|
+
const disabledPlugins = new Set(config.disabledPlugins ?? []);
|
|
21
|
+
return plugins
|
|
22
|
+
.filter((plugin) => !disabledPlugins.has(plugin.id) && !!plugin.dataProvider)
|
|
23
|
+
.map((plugin) => plugin.dataProvider as DataProvider);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function loadCliConfigIfAvailable(): Promise<AppConfig | null> {
|
|
27
|
+
const dataDir = await getDataDir();
|
|
28
|
+
if (!dataDir || !existsSync(dataDir)) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
return loadConfig(dataDir);
|
|
32
|
+
}
|
|
33
|
+
|
|
12
34
|
export async function initConfigData(): Promise<ConfigContext> {
|
|
13
35
|
const dataDir = await getDataDir();
|
|
14
36
|
if (!dataDir || !existsSync(dataDir)) {
|
|
@@ -21,42 +43,15 @@ export async function initConfigData(): Promise<ConfigContext> {
|
|
|
21
43
|
return { config, persistence, store, dataDir };
|
|
22
44
|
}
|
|
23
45
|
|
|
24
|
-
export async function initMarketData(): Promise<MarketContext> {
|
|
46
|
+
export async function initMarketData(options: CliContextOptions = {}): Promise<MarketContext> {
|
|
25
47
|
const context = await initConfigData();
|
|
48
|
+
const plugins = options.plugins ?? getLoadablePlugins();
|
|
26
49
|
const dataProvider = new ProviderRouter(
|
|
27
50
|
null,
|
|
28
|
-
|
|
51
|
+
resolveCliDataProviders(context.config, plugins),
|
|
29
52
|
context.persistence.resources,
|
|
30
53
|
);
|
|
31
54
|
return { ...context, dataProvider };
|
|
32
55
|
}
|
|
33
56
|
|
|
34
|
-
export
|
|
35
|
-
const rateCache = new Map<string, number>([["USD", 1]]);
|
|
36
|
-
|
|
37
|
-
const getRate = async (currency: string): Promise<number> => {
|
|
38
|
-
const normalizedCurrency = currency.toUpperCase();
|
|
39
|
-
const cached = rateCache.get(normalizedCurrency);
|
|
40
|
-
if (cached != null) return cached;
|
|
41
|
-
try {
|
|
42
|
-
const rate = await dataProvider.getExchangeRate(normalizedCurrency);
|
|
43
|
-
rateCache.set(normalizedCurrency, rate);
|
|
44
|
-
return rate;
|
|
45
|
-
} catch {
|
|
46
|
-
return 1;
|
|
47
|
-
}
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
return async (value: number, fromCurrency: string): Promise<number> => {
|
|
51
|
-
const normalizedFrom = fromCurrency.toUpperCase();
|
|
52
|
-
const normalizedBase = baseCurrency.toUpperCase();
|
|
53
|
-
if (normalizedFrom === normalizedBase) return value;
|
|
54
|
-
|
|
55
|
-
const [fromRate, baseRate] = await Promise.all([
|
|
56
|
-
getRate(normalizedFrom),
|
|
57
|
-
getRate(normalizedBase),
|
|
58
|
-
]);
|
|
59
|
-
if (baseRate === 0) return value;
|
|
60
|
-
return (value * fromRate) / baseRate;
|
|
61
|
-
};
|
|
62
|
-
}
|
|
57
|
+
export { createBaseConverter };
|
package/src/cli/helpers.ts
CHANGED
|
@@ -4,13 +4,10 @@ import {
|
|
|
4
4
|
formatNumber,
|
|
5
5
|
formatPercentRaw,
|
|
6
6
|
} from "../utils/format";
|
|
7
|
+
export { slugifyName } from "../utils/slugify";
|
|
7
8
|
import type { AppConfig } from "../types/config";
|
|
8
9
|
import type { Watchlist, TickerRecord } from "../types/ticker";
|
|
9
10
|
|
|
10
|
-
export function slugifyName(name: string, fallbackPrefix: string): string {
|
|
11
|
-
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || `${fallbackPrefix}-${Date.now()}`;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
11
|
export function formatSignedCurrency(value: number, currency: string): string {
|
|
15
12
|
return value > 0 ? `+${formatCurrency(value, currency)}` : formatCurrency(value, currency);
|
|
16
13
|
}
|