gloomberb 0.3.7 → 0.4.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 +19 -8
- package/package.json +1 -1
- package/src/app.tsx +29 -65
- 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 +359 -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 +1 -1
- package/src/components/layout/pane-header.tsx +26 -2
- package/src/components/layout/shell.tsx +62 -50
- 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 +174 -97
- package/src/components/ui/data-table.tsx +27 -2
- 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 +370 -3
- package/src/plugins/builtin/chat-controller.ts +310 -35
- package/src/plugins/builtin/chat.test.tsx +118 -6
- package/src/plugins/builtin/chat.tsx +175 -116
- package/src/plugins/builtin/comparison-chart.test.tsx +66 -5
- package/src/plugins/builtin/comparison-chart.tsx +21 -1
- 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 +45 -1
- package/src/plugins/ibkr/gateway-service.ts +141 -7
- 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 +11 -3
- package/src/plugins/prediction-markets/controller.ts +141 -121
- package/src/plugins/prediction-markets/index.tsx +2 -0
- 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 +119 -57
- package/src/plugins/prediction-markets/pane.tsx +74 -153
- package/src/plugins/prediction-markets/table.tsx +6 -3
- package/src/plugins/prediction-markets/types.ts +0 -1
- 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/utils/api-client.test.ts +57 -0
- package/src/utils/api-client.ts +35 -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,33 +41,14 @@ 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
54
|
import { canSelfUpdate, checkForUpdateDetailed, performUpdate } from "./updater";
|
|
@@ -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";
|
|
@@ -966,7 +948,7 @@ function AppInner({ pluginRegistry, tickerRepository, dataProvider, marketData,
|
|
|
966
948
|
});
|
|
967
949
|
} catch (error) {
|
|
968
950
|
pluginRegistry.showToastFn(
|
|
969
|
-
error instanceof Error ? error.message : `Could not create ${template.
|
|
951
|
+
error instanceof Error ? error.message : `Could not create ${getPaneTemplateDisplayLabel(template).toLowerCase()}.`,
|
|
970
952
|
{ type: "info" },
|
|
971
953
|
);
|
|
972
954
|
}
|
|
@@ -1232,32 +1214,36 @@ interface AppProps {
|
|
|
1232
1214
|
config: AppConfig;
|
|
1233
1215
|
renderer: CliRenderer;
|
|
1234
1216
|
externalPlugins?: import("./plugins/loader").LoadedExternalPlugin[];
|
|
1235
|
-
|
|
1217
|
+
cliLaunchRequest?: CliLaunchRequest | null;
|
|
1236
1218
|
}
|
|
1237
1219
|
|
|
1238
1220
|
export function App({
|
|
1239
1221
|
config: initialConfig,
|
|
1240
1222
|
renderer,
|
|
1241
1223
|
externalPlugins = [],
|
|
1242
|
-
|
|
1224
|
+
cliLaunchRequest = null,
|
|
1243
1225
|
}: AppProps) {
|
|
1244
|
-
const
|
|
1245
|
-
if (!
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1226
|
+
const initialCliLaunch = useMemo(() => {
|
|
1227
|
+
if (!cliLaunchRequest) {
|
|
1228
|
+
return { config: initialConfig, launchState: undefined };
|
|
1229
|
+
}
|
|
1230
|
+
return cliLaunchRequest.applyConfig(initialConfig, {
|
|
1231
|
+
terminalWidth: renderer.terminalWidth,
|
|
1232
|
+
terminalHeight: renderer.terminalHeight,
|
|
1233
|
+
});
|
|
1234
|
+
}, [cliLaunchRequest, initialConfig, renderer.terminalHeight, renderer.terminalWidth]);
|
|
1235
|
+
const cliLaunchStateRef = useRef(initialCliLaunch.launchState);
|
|
1251
1236
|
|
|
1252
1237
|
const [config, setConfig] = useState(() => {
|
|
1253
|
-
return
|
|
1238
|
+
return initialCliLaunch.config;
|
|
1254
1239
|
});
|
|
1255
1240
|
const [showOnboarding, setShowOnboarding] = useState(!initialConfig.onboardingComplete);
|
|
1256
1241
|
|
|
1257
1242
|
useEffect(() => bindAppActivity(renderer), [renderer]);
|
|
1258
1243
|
|
|
1259
|
-
|
|
1260
|
-
|
|
1244
|
+
useLayoutEffect(() => {
|
|
1245
|
+
if (config.theme) applyTheme(config.theme);
|
|
1246
|
+
}, [config.theme]);
|
|
1261
1247
|
|
|
1262
1248
|
const services = useMemo(() => {
|
|
1263
1249
|
const dbPath = join(config.dataDir, ".gloomberb-cache.db");
|
|
@@ -1271,25 +1257,8 @@ export function App({
|
|
|
1271
1257
|
pluginRegistry.getConfigFn = () => config;
|
|
1272
1258
|
pluginRegistry.getLayoutFn = () => config.layout;
|
|
1273
1259
|
|
|
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);
|
|
1260
|
+
for (const plugin of getLoadablePlugins(externalPlugins)) {
|
|
1261
|
+
pluginRegistry.register(plugin);
|
|
1293
1262
|
}
|
|
1294
1263
|
|
|
1295
1264
|
return {
|
|
@@ -1316,20 +1285,15 @@ export function App({
|
|
|
1316
1285
|
const sessionSnapshot = useMemo(() => {
|
|
1317
1286
|
const persisted = services.persistence.sessions.get<AppSessionSnapshot>(APP_SESSION_ID, APP_SESSION_SCHEMA_VERSION)?.value ?? null;
|
|
1318
1287
|
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) {
|
|
1288
|
+
if (!cliLaunchRequest?.applySessionSnapshot) {
|
|
1324
1289
|
return reconciled;
|
|
1325
1290
|
}
|
|
1326
|
-
return
|
|
1291
|
+
return cliLaunchRequest.applySessionSnapshot(
|
|
1327
1292
|
config,
|
|
1328
1293
|
reconciled,
|
|
1329
|
-
|
|
1330
|
-
predictionLaunchIntent,
|
|
1294
|
+
cliLaunchStateRef.current,
|
|
1331
1295
|
);
|
|
1332
|
-
}, [
|
|
1296
|
+
}, [cliLaunchRequest, config, services.persistence.sessions]);
|
|
1333
1297
|
|
|
1334
1298
|
if (showOnboarding) {
|
|
1335
1299
|
return (
|
|
@@ -1337,7 +1301,7 @@ export function App({
|
|
|
1337
1301
|
config={config}
|
|
1338
1302
|
pluginRegistry={services.pluginRegistry}
|
|
1339
1303
|
onComplete={(updatedConfig) => {
|
|
1340
|
-
setConfig(
|
|
1304
|
+
setConfig(updatedConfig);
|
|
1341
1305
|
setShowOnboarding(false);
|
|
1342
1306
|
}}
|
|
1343
1307
|
/>
|
|
@@ -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
|
}
|