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
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import type { CommandBarFieldValue, CommandBarWorkflowField } from "../../../components/command-bar/workflow-types";
|
|
2
|
+
import type { AppConfig } from "../../../types/config";
|
|
3
|
+
import type { TickerRecord } from "../../../types/ticker";
|
|
4
|
+
import { getManualPortfolioPosition, isManualPortfolio } from "./mutations";
|
|
5
|
+
|
|
6
|
+
export interface SetPortfolioPositionWorkflowState {
|
|
7
|
+
fields: CommandBarWorkflowField[];
|
|
8
|
+
values: Record<string, CommandBarFieldValue>;
|
|
9
|
+
pendingLabel: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface ManualPortfolioPositionWorkflowOptions {
|
|
13
|
+
preferredPortfolioId?: string | null;
|
|
14
|
+
ticker: TickerRecord | null;
|
|
15
|
+
pendingLabel: string;
|
|
16
|
+
positionOptional?: boolean;
|
|
17
|
+
defaultAvgCost?: number | null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function buildManualPortfolioPositionWorkflow(
|
|
21
|
+
config: AppConfig,
|
|
22
|
+
options: ManualPortfolioPositionWorkflowOptions,
|
|
23
|
+
): SetPortfolioPositionWorkflowState | null {
|
|
24
|
+
const manualPortfolios = config.portfolios.filter(isManualPortfolio);
|
|
25
|
+
if (manualPortfolios.length === 0) return null;
|
|
26
|
+
|
|
27
|
+
const preferredPortfolio = manualPortfolios.find((portfolio) => portfolio.id === options.preferredPortfolioId) ?? manualPortfolios[0]!;
|
|
28
|
+
const preferredPosition = options.ticker
|
|
29
|
+
? getManualPortfolioPosition(options.ticker, preferredPortfolio.id)
|
|
30
|
+
: null;
|
|
31
|
+
|
|
32
|
+
const fields: CommandBarWorkflowField[] = [
|
|
33
|
+
{
|
|
34
|
+
id: "portfolioId",
|
|
35
|
+
label: "Portfolio",
|
|
36
|
+
type: "select",
|
|
37
|
+
options: manualPortfolios.map((portfolio) => ({
|
|
38
|
+
label: portfolio.name,
|
|
39
|
+
value: portfolio.id,
|
|
40
|
+
description: portfolio.currency,
|
|
41
|
+
})),
|
|
42
|
+
required: true,
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
id: "ticker",
|
|
46
|
+
label: "Ticker",
|
|
47
|
+
type: "text",
|
|
48
|
+
placeholder: "AAPL",
|
|
49
|
+
required: true,
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
id: "shares",
|
|
53
|
+
label: "Shares",
|
|
54
|
+
type: "number",
|
|
55
|
+
placeholder: "10",
|
|
56
|
+
required: !options.positionOptional,
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
id: "avgCost",
|
|
60
|
+
label: "Avg Cost",
|
|
61
|
+
type: "number",
|
|
62
|
+
placeholder: "180",
|
|
63
|
+
required: !options.positionOptional,
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
id: "currency",
|
|
67
|
+
label: "Currency",
|
|
68
|
+
type: "text",
|
|
69
|
+
placeholder: preferredPortfolio.currency,
|
|
70
|
+
required: false,
|
|
71
|
+
},
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
const values: Record<string, CommandBarFieldValue> = {
|
|
75
|
+
portfolioId: preferredPortfolio.id,
|
|
76
|
+
ticker: options.ticker?.metadata.ticker ?? "",
|
|
77
|
+
shares: preferredPosition ? String(preferredPosition.shares) : "",
|
|
78
|
+
avgCost: preferredPosition
|
|
79
|
+
? String(preferredPosition.avgCost)
|
|
80
|
+
: Number.isFinite(options.defaultAvgCost)
|
|
81
|
+
? String(options.defaultAvgCost)
|
|
82
|
+
: "",
|
|
83
|
+
currency: preferredPosition?.currency ?? "",
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
fields,
|
|
88
|
+
values,
|
|
89
|
+
pendingLabel: options.pendingLabel,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function buildSetPortfolioPositionWorkflow(
|
|
94
|
+
config: AppConfig,
|
|
95
|
+
options: {
|
|
96
|
+
activeCollectionId: string | null;
|
|
97
|
+
activeTicker: TickerRecord | null;
|
|
98
|
+
},
|
|
99
|
+
): SetPortfolioPositionWorkflowState | null {
|
|
100
|
+
return buildManualPortfolioPositionWorkflow(config, {
|
|
101
|
+
preferredPortfolioId: options.activeCollectionId,
|
|
102
|
+
ticker: options.activeTicker,
|
|
103
|
+
pendingLabel: "Saving position…",
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function buildAddToPortfolioWorkflow(
|
|
108
|
+
config: AppConfig,
|
|
109
|
+
options: {
|
|
110
|
+
preferredPortfolioId?: string | null;
|
|
111
|
+
ticker: TickerRecord | null;
|
|
112
|
+
defaultAvgCost?: number | null;
|
|
113
|
+
},
|
|
114
|
+
): SetPortfolioPositionWorkflowState | null {
|
|
115
|
+
return buildManualPortfolioPositionWorkflow(config, {
|
|
116
|
+
preferredPortfolioId: options.preferredPortfolioId,
|
|
117
|
+
ticker: options.ticker,
|
|
118
|
+
pendingLabel: "Adding to portfolio…",
|
|
119
|
+
positionOptional: true,
|
|
120
|
+
defaultAvgCost: options.defaultAvgCost,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
@@ -1,13 +1,11 @@
|
|
|
1
|
-
import { useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
1
|
+
import { memo, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
2
2
|
import { TextAttributes } from "@opentui/core";
|
|
3
3
|
import type { AppState } from "../../../state/app-context";
|
|
4
|
-
import { useFxRatesMap } from "../../../market-data/hooks";
|
|
5
4
|
import { colors, priceColor } from "../../../theme/colors";
|
|
6
5
|
import type { TickerFinancials } from "../../../types/financials";
|
|
7
6
|
import type { Portfolio, TickerRecord } from "../../../types/ticker";
|
|
8
7
|
import { formatCompact, formatNumber, formatPercentRaw, padTo } from "../../../utils/format";
|
|
9
8
|
import { getMostRecentQuoteUpdate } from "../../../utils/quote-time";
|
|
10
|
-
import { selectEffectiveExchangeRates } from "../../../utils/exchange-rate-map";
|
|
11
9
|
import { ibkrGatewayManager } from "../../ibkr/gateway-service";
|
|
12
10
|
import { calculatePortfolioSummaryTotals } from "./metrics";
|
|
13
11
|
import {
|
|
@@ -18,22 +16,6 @@ import {
|
|
|
18
16
|
type ResolvedPortfolioAccountState,
|
|
19
17
|
} from "./summary";
|
|
20
18
|
|
|
21
|
-
function getSummaryCurrencies(
|
|
22
|
-
tickers: TickerRecord[],
|
|
23
|
-
financialsMap: Map<string, TickerFinancials>,
|
|
24
|
-
baseCurrency: string,
|
|
25
|
-
accountState: ResolvedPortfolioAccountState | null,
|
|
26
|
-
): string[] {
|
|
27
|
-
return [
|
|
28
|
-
baseCurrency,
|
|
29
|
-
...tickers.map((ticker) => ticker.metadata.currency),
|
|
30
|
-
...tickers.map((ticker) => financialsMap.get(ticker.metadata.ticker)?.quote?.currency),
|
|
31
|
-
...tickers.flatMap((ticker) => ticker.metadata.positions.map((position) => position.currency)),
|
|
32
|
-
...(accountState?.visibleCashBalances.map((balance) => balance.currency) ?? []),
|
|
33
|
-
...(accountState?.visibleCashBalances.map((balance) => balance.baseCurrency) ?? []),
|
|
34
|
-
];
|
|
35
|
-
}
|
|
36
|
-
|
|
37
19
|
export function shouldToggleCashMarginDrawer(key: string | undefined, showCashDrawer: boolean): boolean {
|
|
38
20
|
return key === "c" && showCashDrawer;
|
|
39
21
|
}
|
|
@@ -49,7 +31,7 @@ export function usePortfolioAccountState(
|
|
|
49
31
|
);
|
|
50
32
|
return useMemo(
|
|
51
33
|
() => resolvePortfolioAccountState(portfolio, state, snapshot),
|
|
52
|
-
[portfolio, snapshot, state],
|
|
34
|
+
[portfolio, snapshot, state.brokerAccounts, state.config],
|
|
53
35
|
);
|
|
54
36
|
}
|
|
55
37
|
|
|
@@ -139,10 +121,12 @@ export function PortfolioCashMarginDrawer({
|
|
|
139
121
|
);
|
|
140
122
|
}
|
|
141
123
|
|
|
142
|
-
export function PortfolioSummaryBar({
|
|
124
|
+
export const PortfolioSummaryBar = memo(function PortfolioSummaryBar({
|
|
143
125
|
tickers,
|
|
144
126
|
financialsMap,
|
|
145
|
-
|
|
127
|
+
baseCurrency,
|
|
128
|
+
exchangeRates,
|
|
129
|
+
refreshingCount,
|
|
146
130
|
isPortfolio,
|
|
147
131
|
collectionId,
|
|
148
132
|
width,
|
|
@@ -150,7 +134,9 @@ export function PortfolioSummaryBar({
|
|
|
150
134
|
}: {
|
|
151
135
|
tickers: TickerRecord[];
|
|
152
136
|
financialsMap: Map<string, TickerFinancials>;
|
|
153
|
-
|
|
137
|
+
baseCurrency: string;
|
|
138
|
+
exchangeRates: Map<string, number>;
|
|
139
|
+
refreshingCount: number;
|
|
154
140
|
isPortfolio: boolean;
|
|
155
141
|
collectionId: string | null;
|
|
156
142
|
width: number;
|
|
@@ -163,7 +149,7 @@ export function PortfolioSummaryBar({
|
|
|
163
149
|
const wasRefreshing = useRef(false);
|
|
164
150
|
|
|
165
151
|
useEffect(() => {
|
|
166
|
-
if (
|
|
152
|
+
if (refreshingCount > 0) {
|
|
167
153
|
wasRefreshing.current = true;
|
|
168
154
|
return;
|
|
169
155
|
}
|
|
@@ -171,7 +157,7 @@ export function PortfolioSummaryBar({
|
|
|
171
157
|
wasRefreshing.current = false;
|
|
172
158
|
setLastRefresh(new Date());
|
|
173
159
|
}
|
|
174
|
-
}, [
|
|
160
|
+
}, [refreshingCount]);
|
|
175
161
|
|
|
176
162
|
useEffect(() => {
|
|
177
163
|
if (financialsMap.size > 0 && !lastRefresh) {
|
|
@@ -179,27 +165,23 @@ export function PortfolioSummaryBar({
|
|
|
179
165
|
}
|
|
180
166
|
}, [financialsMap.size, lastRefresh]);
|
|
181
167
|
|
|
182
|
-
const exchangeRates = useFxRatesMap(
|
|
183
|
-
getSummaryCurrencies(tickers, financialsMap, state.config.baseCurrency, accountState),
|
|
184
|
-
);
|
|
185
|
-
const effectiveExchangeRates = selectEffectiveExchangeRates(exchangeRates, state.exchangeRates);
|
|
186
168
|
const totals = useMemo(
|
|
187
169
|
() => calculatePortfolioSummaryTotals(
|
|
188
170
|
tickers,
|
|
189
171
|
financialsMap,
|
|
190
|
-
|
|
191
|
-
|
|
172
|
+
baseCurrency,
|
|
173
|
+
exchangeRates,
|
|
192
174
|
isPortfolio,
|
|
193
175
|
collectionId,
|
|
194
176
|
),
|
|
195
|
-
[
|
|
177
|
+
[baseCurrency, collectionId, exchangeRates, financialsMap, isPortfolio, tickers],
|
|
196
178
|
);
|
|
197
179
|
|
|
198
180
|
const refreshTimestamp = lastRefreshTimestamp ?? lastRefresh?.getTime() ?? null;
|
|
199
181
|
const refreshText = refreshTimestamp != null
|
|
200
182
|
? new Date(refreshTimestamp).toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit" })
|
|
201
183
|
: "—";
|
|
202
|
-
const isRefreshing =
|
|
184
|
+
const isRefreshing = refreshingCount > 0;
|
|
203
185
|
|
|
204
186
|
if (!isPortfolio) {
|
|
205
187
|
if (totals.watchlistCount === 0) return null;
|
|
@@ -224,4 +206,4 @@ export function PortfolioSummaryBar({
|
|
|
224
206
|
});
|
|
225
207
|
|
|
226
208
|
return <box height={1}>{renderSummarySegments(segments, width)}</box>;
|
|
227
|
-
}
|
|
209
|
+
});
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
getPortfolioPaneSettings,
|
|
8
8
|
resolveCollectionPaneId,
|
|
9
9
|
} from "./settings";
|
|
10
|
+
import { portfolioCliCommand, watchlistCliCommand } from "./cli/commands";
|
|
10
11
|
|
|
11
12
|
export { shouldToggleCashMarginDrawer };
|
|
12
13
|
|
|
@@ -29,6 +30,10 @@ export const portfolioListPlugin: GloomPlugin = {
|
|
|
29
30
|
),
|
|
30
31
|
},
|
|
31
32
|
],
|
|
33
|
+
cliCommands: [
|
|
34
|
+
portfolioCliCommand,
|
|
35
|
+
watchlistCliCommand,
|
|
36
|
+
],
|
|
32
37
|
paneTemplates: [
|
|
33
38
|
{
|
|
34
39
|
id: "new-collection-pane",
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { getColumnValue, type ColumnContext } from "./metrics";
|
|
3
|
+
import type { ColumnConfig } from "../../../types/config";
|
|
4
|
+
import type { TickerFinancials } from "../../../types/financials";
|
|
5
|
+
import type { TickerRecord } from "../../../types/ticker";
|
|
6
|
+
|
|
7
|
+
const TICKER_COLUMN: ColumnConfig = {
|
|
8
|
+
id: "ticker",
|
|
9
|
+
label: "Ticker",
|
|
10
|
+
width: 12,
|
|
11
|
+
align: "left",
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const TEST_CONTEXT: ColumnContext = {
|
|
15
|
+
baseCurrency: "USD",
|
|
16
|
+
exchangeRates: new Map(),
|
|
17
|
+
now: Date.now(),
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
function makeTicker(): TickerRecord {
|
|
21
|
+
return {
|
|
22
|
+
metadata: {
|
|
23
|
+
ticker: "AAPL",
|
|
24
|
+
exchange: "NASDAQ",
|
|
25
|
+
currency: "USD",
|
|
26
|
+
name: "Apple",
|
|
27
|
+
portfolios: [],
|
|
28
|
+
watchlists: [],
|
|
29
|
+
positions: [],
|
|
30
|
+
custom: {},
|
|
31
|
+
tags: [],
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function makeFinancials(marketState?: TickerFinancials["quote"]["marketState"]): TickerFinancials {
|
|
37
|
+
return {
|
|
38
|
+
annualStatements: [],
|
|
39
|
+
quarterlyStatements: [],
|
|
40
|
+
priceHistory: [],
|
|
41
|
+
quote: {
|
|
42
|
+
symbol: "AAPL",
|
|
43
|
+
price: 100,
|
|
44
|
+
currency: "USD",
|
|
45
|
+
change: 1,
|
|
46
|
+
changePercent: 1,
|
|
47
|
+
lastUpdated: Date.now(),
|
|
48
|
+
marketState,
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
describe("portfolio-list metrics", () => {
|
|
54
|
+
test("maps session states to the portfolio status glyphs", () => {
|
|
55
|
+
expect(getColumnValue(TICKER_COLUMN, makeTicker(), makeFinancials("REGULAR"), TEST_CONTEXT).text).toBe("● AAPL");
|
|
56
|
+
expect(getColumnValue(TICKER_COLUMN, makeTicker(), makeFinancials("PRE"), TEST_CONTEXT).text).toBe("◐ AAPL");
|
|
57
|
+
expect(getColumnValue(TICKER_COLUMN, makeTicker(), makeFinancials("POST"), TEST_CONTEXT).text).toBe("◐ AAPL");
|
|
58
|
+
expect(getColumnValue(TICKER_COLUMN, makeTicker(), makeFinancials("CLOSED"), TEST_CONTEXT).text).toBe("○ AAPL");
|
|
59
|
+
expect(getColumnValue(TICKER_COLUMN, makeTicker(), makeFinancials(undefined), TEST_CONTEXT).text).toBe("◌ AAPL");
|
|
60
|
+
});
|
|
61
|
+
});
|
|
@@ -5,7 +5,7 @@ import type { TickerRecord } from "../../../types/ticker";
|
|
|
5
5
|
import { priceColor } from "../../../theme/colors";
|
|
6
6
|
import { clampQuoteTimestamp, formatQuoteAgeWithSource } from "../../../utils/quote-time";
|
|
7
7
|
import { convertCurrency, formatCompact, formatCurrency, formatNumber, formatPercentRaw, formatPrice } from "../../../utils/format";
|
|
8
|
-
import { getActiveQuoteDisplay, type ActiveQuoteDisplay } from "../../../utils/market-status";
|
|
8
|
+
import { getActiveQuoteDisplay, marketStateDot, type ActiveQuoteDisplay } from "../../../utils/market-status";
|
|
9
9
|
import { formatOptionTicker } from "../../../utils/options";
|
|
10
10
|
|
|
11
11
|
export interface ColumnContext {
|
|
@@ -191,7 +191,7 @@ export function getColumnValue(
|
|
|
191
191
|
switch (col.id) {
|
|
192
192
|
case "ticker": {
|
|
193
193
|
const marketState = quote?.marketState;
|
|
194
|
-
const statusDot = marketState
|
|
194
|
+
const statusDot = marketStateDot(marketState);
|
|
195
195
|
const displayName = ticker.metadata.assetCategory === "OPT"
|
|
196
196
|
? formatOptionTicker(ticker.metadata.ticker)
|
|
197
197
|
: ticker.metadata.ticker;
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { createDefaultConfig } from "../../../types/config";
|
|
3
|
+
import type { TickerRecord } from "../../../types/ticker";
|
|
4
|
+
import {
|
|
5
|
+
createManualPortfolio,
|
|
6
|
+
deleteManualPortfolio,
|
|
7
|
+
isManualPortfolio,
|
|
8
|
+
removeTickerFromPortfolio,
|
|
9
|
+
resolveManualPositionCurrency,
|
|
10
|
+
setManualPortfolioPosition,
|
|
11
|
+
} from "./mutations";
|
|
12
|
+
|
|
13
|
+
function makeTicker(overrides: Partial<TickerRecord["metadata"]> = {}): TickerRecord {
|
|
14
|
+
return {
|
|
15
|
+
metadata: {
|
|
16
|
+
ticker: "AAPL",
|
|
17
|
+
exchange: "NASDAQ",
|
|
18
|
+
currency: "USD",
|
|
19
|
+
name: "Apple Inc.",
|
|
20
|
+
portfolios: [],
|
|
21
|
+
watchlists: [],
|
|
22
|
+
positions: [],
|
|
23
|
+
custom: {},
|
|
24
|
+
tags: [],
|
|
25
|
+
...overrides,
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe("portfolio-list mutations", () => {
|
|
31
|
+
test("identifies manual portfolios", () => {
|
|
32
|
+
const config = createDefaultConfig("/tmp/gloomberb-mutations");
|
|
33
|
+
expect(isManualPortfolio(config.portfolios[0]!)).toBe(true);
|
|
34
|
+
expect(isManualPortfolio({
|
|
35
|
+
id: "broker:ibkr",
|
|
36
|
+
name: "IBKR",
|
|
37
|
+
currency: "USD",
|
|
38
|
+
brokerId: "ibkr",
|
|
39
|
+
brokerInstanceId: "ibkr-live",
|
|
40
|
+
})).toBe(false);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("rejects duplicate manual portfolio names", () => {
|
|
44
|
+
const config = createDefaultConfig("/tmp/gloomberb-mutations");
|
|
45
|
+
expect(() => createManualPortfolio(config, "Main Portfolio", config.baseCurrency)).toThrow('Portfolio "Main Portfolio" already exists.');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("deletes a manual portfolio and cleans memberships plus positions", () => {
|
|
49
|
+
const config = createDefaultConfig("/tmp/gloomberb-mutations");
|
|
50
|
+
config.portfolios.push({ id: "research", name: "Research", currency: "USD" });
|
|
51
|
+
|
|
52
|
+
const firstTicker = makeTicker({
|
|
53
|
+
ticker: "AAPL",
|
|
54
|
+
portfolios: ["research"],
|
|
55
|
+
positions: [{
|
|
56
|
+
portfolio: "research",
|
|
57
|
+
shares: 10,
|
|
58
|
+
avgCost: 180,
|
|
59
|
+
currency: "USD",
|
|
60
|
+
broker: "manual",
|
|
61
|
+
}],
|
|
62
|
+
});
|
|
63
|
+
const secondTicker = makeTicker({
|
|
64
|
+
ticker: "MSFT",
|
|
65
|
+
portfolios: ["research", "main"],
|
|
66
|
+
positions: [{
|
|
67
|
+
portfolio: "research",
|
|
68
|
+
shares: 5,
|
|
69
|
+
avgCost: 300,
|
|
70
|
+
currency: "USD",
|
|
71
|
+
broker: "manual",
|
|
72
|
+
}, {
|
|
73
|
+
portfolio: "main",
|
|
74
|
+
shares: 3,
|
|
75
|
+
avgCost: 200,
|
|
76
|
+
currency: "USD",
|
|
77
|
+
broker: "manual",
|
|
78
|
+
}],
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const result = deleteManualPortfolio(config, [firstTicker, secondTicker], "research");
|
|
82
|
+
|
|
83
|
+
expect(result.config.portfolios.map((portfolio) => portfolio.id)).toEqual(["main"]);
|
|
84
|
+
expect(result.cleanedTickerCount).toBe(2);
|
|
85
|
+
expect(result.removedPositionCount).toBe(2);
|
|
86
|
+
expect(result.tickers).toEqual([
|
|
87
|
+
makeTicker({
|
|
88
|
+
ticker: "AAPL",
|
|
89
|
+
portfolios: [],
|
|
90
|
+
}),
|
|
91
|
+
makeTicker({
|
|
92
|
+
ticker: "MSFT",
|
|
93
|
+
portfolios: ["main"],
|
|
94
|
+
positions: [{
|
|
95
|
+
portfolio: "main",
|
|
96
|
+
shares: 3,
|
|
97
|
+
avgCost: 200,
|
|
98
|
+
currency: "USD",
|
|
99
|
+
broker: "manual",
|
|
100
|
+
}],
|
|
101
|
+
}),
|
|
102
|
+
]);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("removing a ticker from a portfolio also removes its positions", () => {
|
|
106
|
+
const ticker = makeTicker({
|
|
107
|
+
portfolios: ["main", "research"],
|
|
108
|
+
positions: [{
|
|
109
|
+
portfolio: "research",
|
|
110
|
+
shares: 4,
|
|
111
|
+
avgCost: 100,
|
|
112
|
+
currency: "USD",
|
|
113
|
+
broker: "manual",
|
|
114
|
+
}, {
|
|
115
|
+
portfolio: "main",
|
|
116
|
+
shares: 1,
|
|
117
|
+
avgCost: 90,
|
|
118
|
+
currency: "USD",
|
|
119
|
+
broker: "manual",
|
|
120
|
+
}],
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
const result = removeTickerFromPortfolio(ticker, "research");
|
|
124
|
+
|
|
125
|
+
expect(result.changed).toBe(true);
|
|
126
|
+
expect(result.removedPositionCount).toBe(1);
|
|
127
|
+
expect(result.ticker.metadata.portfolios).toEqual(["main"]);
|
|
128
|
+
expect(result.ticker.metadata.positions).toEqual([{
|
|
129
|
+
portfolio: "main",
|
|
130
|
+
shares: 1,
|
|
131
|
+
avgCost: 90,
|
|
132
|
+
currency: "USD",
|
|
133
|
+
broker: "manual",
|
|
134
|
+
}]);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("setting a manual position replaces the aggregate entry for that portfolio", () => {
|
|
138
|
+
const ticker = makeTicker({
|
|
139
|
+
portfolios: ["main"],
|
|
140
|
+
positions: [{
|
|
141
|
+
portfolio: "main",
|
|
142
|
+
shares: 1,
|
|
143
|
+
avgCost: 100,
|
|
144
|
+
currency: "USD",
|
|
145
|
+
broker: "manual",
|
|
146
|
+
}, {
|
|
147
|
+
portfolio: "other",
|
|
148
|
+
shares: 2,
|
|
149
|
+
avgCost: 50,
|
|
150
|
+
currency: "USD",
|
|
151
|
+
broker: "manual",
|
|
152
|
+
}],
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const result = setManualPortfolioPosition(ticker, "main", {
|
|
156
|
+
shares: 10,
|
|
157
|
+
avgCost: 150,
|
|
158
|
+
currency: "USD",
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
expect(result.addedMembership).toBe(false);
|
|
162
|
+
expect(result.replacedPositionCount).toBe(1);
|
|
163
|
+
expect(result.ticker.metadata.portfolios).toEqual(["main"]);
|
|
164
|
+
expect(result.ticker.metadata.positions).toEqual([{
|
|
165
|
+
portfolio: "other",
|
|
166
|
+
shares: 2,
|
|
167
|
+
avgCost: 50,
|
|
168
|
+
currency: "USD",
|
|
169
|
+
broker: "manual",
|
|
170
|
+
}, {
|
|
171
|
+
portfolio: "main",
|
|
172
|
+
shares: 10,
|
|
173
|
+
avgCost: 150,
|
|
174
|
+
currency: "USD",
|
|
175
|
+
broker: "manual",
|
|
176
|
+
}]);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test("resolves position currency from explicit, ticker, portfolio, or base currency defaults", () => {
|
|
180
|
+
const portfolio = { id: "research", name: "Research", currency: "EUR" };
|
|
181
|
+
|
|
182
|
+
expect(resolveManualPositionCurrency("jpy", makeTicker(), portfolio, "USD")).toBe("JPY");
|
|
183
|
+
expect(resolveManualPositionCurrency("", makeTicker({ currency: "CAD" }), portfolio, "USD")).toBe("CAD");
|
|
184
|
+
expect(resolveManualPositionCurrency("", makeTicker({ currency: "" }), portfolio, "USD")).toBe("EUR");
|
|
185
|
+
expect(resolveManualPositionCurrency("", makeTicker({ currency: "" }), { ...portfolio, currency: "" }, "USD")).toBe("USD");
|
|
186
|
+
});
|
|
187
|
+
});
|