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
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { parseScreenerResponse, parseTrendingResponse } from "./screener";
|
|
3
|
+
|
|
4
|
+
const SAMPLE_SCREENER_RESPONSE = {
|
|
5
|
+
finance: {
|
|
6
|
+
result: [
|
|
7
|
+
{
|
|
8
|
+
quotes: [
|
|
9
|
+
{
|
|
10
|
+
symbol: "AAPL",
|
|
11
|
+
shortName: "Apple Inc.",
|
|
12
|
+
regularMarketPrice: 185.5,
|
|
13
|
+
regularMarketChange: 3.25,
|
|
14
|
+
regularMarketChangePercent: 1.78,
|
|
15
|
+
regularMarketVolume: 52_000_000,
|
|
16
|
+
averageDailyVolume3Month: 20_000_000,
|
|
17
|
+
marketCap: 2_900_000_000_000,
|
|
18
|
+
currency: "USD",
|
|
19
|
+
fiftyTwoWeekHigh: 199.62,
|
|
20
|
+
fiftyTwoWeekLow: 140.0,
|
|
21
|
+
regularMarketDayHigh: 186.0,
|
|
22
|
+
regularMarketDayLow: 182.0,
|
|
23
|
+
fullExchangeName: "NasdaqGS",
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
symbol: "MSFT",
|
|
27
|
+
shortName: "Microsoft Corporation",
|
|
28
|
+
regularMarketPrice: 415.0,
|
|
29
|
+
regularMarketChange: -2.1,
|
|
30
|
+
regularMarketChangePercent: -0.5,
|
|
31
|
+
regularMarketVolume: 18_000_000,
|
|
32
|
+
averageDailyVolume3Month: 25_000_000,
|
|
33
|
+
marketCap: 3_100_000_000_000,
|
|
34
|
+
currency: "USD",
|
|
35
|
+
fullExchangeName: "NasdaqGS",
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
error: null,
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const SAMPLE_TRENDING_RESPONSE = {
|
|
45
|
+
finance: {
|
|
46
|
+
result: [
|
|
47
|
+
{
|
|
48
|
+
quotes: [
|
|
49
|
+
{ symbol: "NVDA" },
|
|
50
|
+
{ symbol: "TSLA" },
|
|
51
|
+
],
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
error: null,
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
describe("parseScreenerResponse", () => {
|
|
59
|
+
test("maps Yahoo Finance fields to ScreenerQuote", () => {
|
|
60
|
+
const results = parseScreenerResponse(SAMPLE_SCREENER_RESPONSE);
|
|
61
|
+
expect(results).toHaveLength(2);
|
|
62
|
+
|
|
63
|
+
const apple = results[0]!;
|
|
64
|
+
expect(apple.symbol).toBe("AAPL");
|
|
65
|
+
expect(apple.name).toBe("Apple Inc.");
|
|
66
|
+
expect(apple.price).toBe(185.5);
|
|
67
|
+
expect(apple.change).toBe(3.25);
|
|
68
|
+
expect(apple.changePercent).toBe(1.78);
|
|
69
|
+
expect(apple.volume).toBe(52_000_000);
|
|
70
|
+
expect(apple.avgVolume).toBe(20_000_000);
|
|
71
|
+
expect(apple.volumeRatio).toBeCloseTo(2.6, 1);
|
|
72
|
+
expect(apple.marketCap).toBe(2_900_000_000_000);
|
|
73
|
+
expect(apple.currency).toBe("USD");
|
|
74
|
+
expect(apple.fiftyTwoWeekHigh).toBe(199.62);
|
|
75
|
+
expect(apple.fiftyTwoWeekLow).toBe(140.0);
|
|
76
|
+
expect(apple.exchange).toBe("NasdaqGS");
|
|
77
|
+
|
|
78
|
+
const msft = results[1]!;
|
|
79
|
+
expect(msft.symbol).toBe("MSFT");
|
|
80
|
+
expect(msft.change).toBe(-2.1);
|
|
81
|
+
expect(msft.changePercent).toBe(-0.5);
|
|
82
|
+
expect(msft.volumeRatio).toBeCloseTo(0.72, 1);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("returns [] for missing finance.result", () => {
|
|
86
|
+
expect(parseScreenerResponse({})).toEqual([]);
|
|
87
|
+
expect(parseScreenerResponse(null)).toEqual([]);
|
|
88
|
+
expect(parseScreenerResponse({ finance: {} })).toEqual([]);
|
|
89
|
+
expect(parseScreenerResponse({ finance: { result: [] } })).toEqual([]);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("returns [] for non-array quotes", () => {
|
|
93
|
+
expect(parseScreenerResponse({ finance: { result: [{ quotes: null }] } })).toEqual([]);
|
|
94
|
+
expect(parseScreenerResponse({ finance: { result: [{}] } })).toEqual([]);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("skips entries without a string symbol", () => {
|
|
98
|
+
const data = {
|
|
99
|
+
finance: {
|
|
100
|
+
result: [{ quotes: [{ symbol: 123 }, { symbol: "AMD", regularMarketPrice: 100 }] }],
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
const results = parseScreenerResponse(data);
|
|
104
|
+
expect(results).toHaveLength(1);
|
|
105
|
+
expect(results[0]!.symbol).toBe("AMD");
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("falls back to symbol when shortName and longName are absent", () => {
|
|
109
|
+
const data = {
|
|
110
|
+
finance: {
|
|
111
|
+
result: [{ quotes: [{ symbol: "XYZ", regularMarketPrice: 10 }] }],
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
const results = parseScreenerResponse(data);
|
|
115
|
+
expect(results[0]!.name).toBe("XYZ");
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe("parseTrendingResponse", () => {
|
|
120
|
+
test("extracts symbols from trending response", () => {
|
|
121
|
+
const results = parseTrendingResponse(SAMPLE_TRENDING_RESPONSE);
|
|
122
|
+
expect(results).toHaveLength(2);
|
|
123
|
+
expect(results[0]!.symbol).toBe("NVDA");
|
|
124
|
+
expect(results[1]!.symbol).toBe("TSLA");
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("returns [] for malformed input", () => {
|
|
128
|
+
expect(parseTrendingResponse({})).toEqual([]);
|
|
129
|
+
expect(parseTrendingResponse(null)).toEqual([]);
|
|
130
|
+
expect(parseTrendingResponse({ finance: { result: [{ quotes: "bad" }] } })).toEqual([]);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("skips entries without a string symbol", () => {
|
|
134
|
+
const data = {
|
|
135
|
+
finance: {
|
|
136
|
+
result: [{ quotes: [{ symbol: null }, { symbol: "SPY" }] }],
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
const results = parseTrendingResponse(data);
|
|
140
|
+
expect(results).toHaveLength(1);
|
|
141
|
+
expect(results[0]!.symbol).toBe("SPY");
|
|
142
|
+
});
|
|
143
|
+
});
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { createThrottledFetch } from "../../../utils/throttled-fetch";
|
|
2
|
+
|
|
3
|
+
const screenerClient = createThrottledFetch({
|
|
4
|
+
requestsPerMinute: 15,
|
|
5
|
+
maxRetries: 2,
|
|
6
|
+
timeoutMs: 10_000,
|
|
7
|
+
defaultHeaders: {
|
|
8
|
+
"User-Agent": "Gloomberb/0.4.1",
|
|
9
|
+
Accept: "application/json",
|
|
10
|
+
},
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
export type ScreenerCategory = "day_gainers" | "day_losers" | "most_actives";
|
|
14
|
+
|
|
15
|
+
export interface ScreenerQuote {
|
|
16
|
+
symbol: string;
|
|
17
|
+
name: string;
|
|
18
|
+
price: number;
|
|
19
|
+
change: number;
|
|
20
|
+
changePercent: number;
|
|
21
|
+
volume: number;
|
|
22
|
+
avgVolume: number;
|
|
23
|
+
volumeRatio: number; // volume / avgVolume
|
|
24
|
+
marketCap: number | undefined;
|
|
25
|
+
currency: string;
|
|
26
|
+
fiftyTwoWeekHigh: number | undefined;
|
|
27
|
+
fiftyTwoWeekLow: number | undefined;
|
|
28
|
+
dayHigh: number | undefined;
|
|
29
|
+
dayLow: number | undefined;
|
|
30
|
+
exchange: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface TrendingSymbol {
|
|
34
|
+
symbol: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface MarketSummaryQuote {
|
|
38
|
+
symbol: string;
|
|
39
|
+
name: string;
|
|
40
|
+
price: number;
|
|
41
|
+
change: number;
|
|
42
|
+
changePercent: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function parseScreenerResponse(data: any): ScreenerQuote[] {
|
|
46
|
+
try {
|
|
47
|
+
const quotes = data?.finance?.result?.[0]?.quotes;
|
|
48
|
+
if (!Array.isArray(quotes)) return [];
|
|
49
|
+
const result: ScreenerQuote[] = [];
|
|
50
|
+
for (const q of quotes) {
|
|
51
|
+
if (!q || typeof q.symbol !== "string") continue;
|
|
52
|
+
const volume = q.regularMarketVolume ?? 0;
|
|
53
|
+
const avgVolume = q.averageDailyVolume3Month ?? q.averageDailyVolume10Day ?? 0;
|
|
54
|
+
result.push({
|
|
55
|
+
symbol: q.symbol,
|
|
56
|
+
name: q.shortName ?? q.longName ?? q.symbol,
|
|
57
|
+
price: q.regularMarketPrice ?? 0,
|
|
58
|
+
change: q.regularMarketChange ?? 0,
|
|
59
|
+
changePercent: q.regularMarketChangePercent ?? 0,
|
|
60
|
+
volume,
|
|
61
|
+
avgVolume,
|
|
62
|
+
volumeRatio: avgVolume > 0 ? volume / avgVolume : 0,
|
|
63
|
+
marketCap: q.marketCap ?? undefined,
|
|
64
|
+
currency: q.currency ?? "USD",
|
|
65
|
+
fiftyTwoWeekHigh: q.fiftyTwoWeekHigh ?? undefined,
|
|
66
|
+
fiftyTwoWeekLow: q.fiftyTwoWeekLow ?? undefined,
|
|
67
|
+
dayHigh: q.regularMarketDayHigh ?? undefined,
|
|
68
|
+
dayLow: q.regularMarketDayLow ?? undefined,
|
|
69
|
+
exchange: q.fullExchangeName ?? q.exchange ?? "",
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
return result;
|
|
73
|
+
} catch {
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function fetchScreener(category: ScreenerCategory, count = 25): Promise<ScreenerQuote[]> {
|
|
79
|
+
const url = `https://query2.finance.yahoo.com/v1/finance/screener/predefined/saved?scrIds=${category}&count=${count}`;
|
|
80
|
+
const data = await screenerClient.fetchJson(url);
|
|
81
|
+
return parseScreenerResponse(data);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function parseTrendingResponse(data: any): TrendingSymbol[] {
|
|
85
|
+
try {
|
|
86
|
+
const quotes = data?.finance?.result?.[0]?.quotes;
|
|
87
|
+
if (!Array.isArray(quotes)) return [];
|
|
88
|
+
const result: TrendingSymbol[] = [];
|
|
89
|
+
for (const q of quotes) {
|
|
90
|
+
if (!q || typeof q.symbol !== "string") continue;
|
|
91
|
+
result.push({ symbol: q.symbol });
|
|
92
|
+
}
|
|
93
|
+
return result;
|
|
94
|
+
} catch {
|
|
95
|
+
return [];
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export async function fetchTrending(count = 25): Promise<TrendingSymbol[]> {
|
|
100
|
+
const url = `https://query2.finance.yahoo.com/v1/finance/trending/US?count=${count}`;
|
|
101
|
+
const data = await screenerClient.fetchJson(url);
|
|
102
|
+
return parseTrendingResponse(data);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export const MARKET_SUMMARY_SYMBOLS = ["^GSPC", "^DJI", "^IXIC", "^RUT"] as const;
|
|
@@ -7,6 +7,7 @@ import { usePaneTicker } from "../../state/app-context";
|
|
|
7
7
|
import { ListView, type ListViewItem } from "../../components/ui";
|
|
8
8
|
import { colors, hoverBg } from "../../theme/colors";
|
|
9
9
|
import { padTo, formatCompact, formatNumber } from "../../utils/format";
|
|
10
|
+
import { formatMarketPrice } from "../../utils/market-format";
|
|
10
11
|
import { formatExpDate, resolveOptionsTarget } from "../../utils/options";
|
|
11
12
|
import { useOptionsQuery, useResolvedEntryValue } from "../../market-data/hooks";
|
|
12
13
|
import { Spinner } from "../../components/spinner";
|
|
@@ -338,9 +339,9 @@ function formatContractRow(contract: { lastPrice: number; bid: number; ask: numb
|
|
|
338
339
|
return padTo("—", w.last) + " " + padTo("—", w.bid) + " " + padTo("—", w.ask) + " " + padTo("—", w.vol) + " " + padTo("—", w.oi);
|
|
339
340
|
}
|
|
340
341
|
return (
|
|
341
|
-
padTo(contract.lastPrice.
|
|
342
|
-
padTo(contract.bid.
|
|
343
|
-
padTo(contract.ask.
|
|
342
|
+
padTo(formatMarketPrice(contract.lastPrice, { assetCategory: "OPT", maxWidth: w.last }), w.last) + " " +
|
|
343
|
+
padTo(formatMarketPrice(contract.bid, { assetCategory: "OPT", maxWidth: w.bid }), w.bid) + " " +
|
|
344
|
+
padTo(formatMarketPrice(contract.ask, { assetCategory: "OPT", maxWidth: w.ask }), w.ask) + " " +
|
|
344
345
|
padTo(formatCompact(contract.volume), w.vol) + " " +
|
|
345
346
|
padTo(formatCompact(contract.openInterest), w.oi)
|
|
346
347
|
);
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
} from "../../../../utils/cli-output";
|
|
18
18
|
import { exchangeShortName } from "../../../../utils/market-status";
|
|
19
19
|
import { formatCompact, formatCurrency } from "../../../../utils/format";
|
|
20
|
+
import { formatMarketCostWithCurrency, formatMarketPriceWithCurrency, formatMarketQuantity } from "../../../../utils/market-format";
|
|
20
21
|
import type { AppConfig } from "../../../../types/config";
|
|
21
22
|
import type { CliCommandContext, CliCommandDef } from "../../../../types/plugin";
|
|
22
23
|
import type { TickerRecord } from "../../../../types/ticker";
|
|
@@ -134,7 +135,9 @@ async function showCollection(name: string, ctx: CliCommandContext) {
|
|
|
134
135
|
for (const ticker of filtered) {
|
|
135
136
|
const quote = quotes.get(ticker.metadata.ticker);
|
|
136
137
|
const positions = ticker.metadata.positions.filter((position) => position.portfolio === id);
|
|
137
|
-
const priceText = quote
|
|
138
|
+
const priceText = quote
|
|
139
|
+
? colorBySign(formatMarketPriceWithCurrency(quote.price, quote.currency, { assetCategory: ticker.metadata.assetCategory }), quote.change)
|
|
140
|
+
: "—";
|
|
138
141
|
const changeText = quote ? colorBySign(formatSignedPercentRaw(quote.changePercent), quote.change) : "—";
|
|
139
142
|
|
|
140
143
|
if (positions.length === 0) {
|
|
@@ -157,8 +160,8 @@ async function showCollection(name: string, ctx: CliCommandContext) {
|
|
|
157
160
|
ticker.metadata.ticker,
|
|
158
161
|
priceText,
|
|
159
162
|
changeText,
|
|
160
|
-
|
|
161
|
-
|
|
163
|
+
formatMarketQuantity(position.shares, { assetCategory: ticker.metadata.assetCategory, multiplier: position.multiplier }),
|
|
164
|
+
formatMarketCostWithCurrency(position.avgCost, positionCurrency, { assetCategory: ticker.metadata.assetCategory, multiplier: position.multiplier }),
|
|
162
165
|
pnl == null ? "—" : colorBySign(formatSignedCurrency(pnl, baseCurrency), pnl),
|
|
163
166
|
]);
|
|
164
167
|
}
|
|
@@ -181,7 +184,9 @@ async function showCollection(name: string, ctx: CliCommandContext) {
|
|
|
181
184
|
const rows: string[][] = [];
|
|
182
185
|
for (const ticker of filtered) {
|
|
183
186
|
const quote = quotes.get(ticker.metadata.ticker);
|
|
184
|
-
const priceText = quote
|
|
187
|
+
const priceText = quote
|
|
188
|
+
? colorBySign(formatMarketPriceWithCurrency(quote.price, quote.currency, { assetCategory: ticker.metadata.assetCategory }), quote.change)
|
|
189
|
+
: "—";
|
|
185
190
|
const changeText = quote ? colorBySign(formatSignedPercentRaw(quote.changePercent), quote.change) : "—";
|
|
186
191
|
const marketCapText = quote?.marketCap != null
|
|
187
192
|
? `${formatCompact(await toBase(quote.marketCap, quote.currency || ticker.metadata.currency || baseCurrency))} ${baseCurrency}`
|
|
@@ -343,8 +348,8 @@ async function setPositionCommand(
|
|
|
343
348
|
});
|
|
344
349
|
await store.saveTicker(result.ticker);
|
|
345
350
|
console.log(cliStyles.success(`Set position for ${result.ticker.metadata.ticker} in "${portfolio.name}".`));
|
|
346
|
-
console.log(renderStat("Shares",
|
|
347
|
-
console.log(renderStat("Average Cost",
|
|
351
|
+
console.log(renderStat("Shares", formatMarketQuantity(shares, { assetCategory: result.ticker.metadata.assetCategory })));
|
|
352
|
+
console.log(renderStat("Average Cost", formatMarketCostWithCurrency(avgCost, currency, { assetCategory: result.ticker.metadata.assetCategory })));
|
|
348
353
|
console.log(renderStat("Currency", currency));
|
|
349
354
|
} catch (error: any) {
|
|
350
355
|
ctx.closeAndFail(persistence, error?.message || `Failed to set position for ${symbol} in "${portfolioName}".`);
|
|
@@ -15,6 +15,7 @@ interface ManualPortfolioPositionWorkflowOptions {
|
|
|
15
15
|
pendingLabel: string;
|
|
16
16
|
positionOptional?: boolean;
|
|
17
17
|
defaultAvgCost?: number | null;
|
|
18
|
+
hidePortfolioFieldWhenSingleOption?: boolean;
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
function buildManualPortfolioPositionWorkflow(
|
|
@@ -28,19 +29,20 @@ function buildManualPortfolioPositionWorkflow(
|
|
|
28
29
|
const preferredPosition = options.ticker
|
|
29
30
|
? getManualPortfolioPosition(options.ticker, preferredPortfolio.id)
|
|
30
31
|
: null;
|
|
32
|
+
const showPortfolioField = !options.hidePortfolioFieldWhenSingleOption || manualPortfolios.length > 1;
|
|
31
33
|
|
|
32
34
|
const fields: CommandBarWorkflowField[] = [
|
|
33
|
-
{
|
|
35
|
+
...(showPortfolioField ? [{
|
|
34
36
|
id: "portfolioId",
|
|
35
37
|
label: "Portfolio",
|
|
36
|
-
type: "select",
|
|
38
|
+
type: "select" as const,
|
|
37
39
|
options: manualPortfolios.map((portfolio) => ({
|
|
38
40
|
label: portfolio.name,
|
|
39
41
|
value: portfolio.id,
|
|
40
42
|
description: portfolio.currency,
|
|
41
43
|
})),
|
|
42
44
|
required: true,
|
|
43
|
-
},
|
|
45
|
+
}] : []),
|
|
44
46
|
{
|
|
45
47
|
id: "ticker",
|
|
46
48
|
label: "Ticker",
|
|
@@ -118,5 +120,6 @@ export function buildAddToPortfolioWorkflow(
|
|
|
118
120
|
pendingLabel: "Adding to portfolio…",
|
|
119
121
|
positionOptional: true,
|
|
120
122
|
defaultAvgCost: options.defaultAvgCost,
|
|
123
|
+
hidePortfolioFieldWhenSingleOption: true,
|
|
121
124
|
});
|
|
122
125
|
}
|
|
@@ -4,7 +4,8 @@ import type { AppState } from "../../../state/app-context";
|
|
|
4
4
|
import { colors, priceColor } from "../../../theme/colors";
|
|
5
5
|
import type { TickerFinancials } from "../../../types/financials";
|
|
6
6
|
import type { Portfolio, TickerRecord } from "../../../types/ticker";
|
|
7
|
-
import { formatCompact,
|
|
7
|
+
import { formatCompact, formatPercentRaw, padTo } from "../../../utils/format";
|
|
8
|
+
import { formatMarketQuantity } from "../../../utils/market-format";
|
|
8
9
|
import { getMostRecentQuoteUpdate } from "../../../utils/quote-time";
|
|
9
10
|
import { ibkrGatewayManager } from "../../ibkr/gateway-service";
|
|
10
11
|
import { calculatePortfolioSummaryTotals } from "./metrics";
|
|
@@ -110,7 +111,7 @@ export function PortfolioCashMarginDrawer({
|
|
|
110
111
|
<box key={balance.currency} height={1} flexDirection="row">
|
|
111
112
|
<text fg={colors.textBright}>{padTo(balance.currency, 4)}</text>
|
|
112
113
|
<text fg={colors.textDim}>{" qty "}</text>
|
|
113
|
-
<text fg={colors.text}>{padTo(
|
|
114
|
+
<text fg={colors.text}>{padTo(formatMarketQuantity(balance.quantity, { isCashBalance: true, maxWidth: 14 }), 14, "right")}</text>
|
|
114
115
|
<text fg={colors.textDim}>{" value "}</text>
|
|
115
116
|
<text fg={colors.text}>{padTo(balance.baseValue != null ? formatCompact(balance.baseValue) : "—", 10, "right")}</text>
|
|
116
117
|
</box>
|
|
@@ -4,7 +4,8 @@ import type { TickerFinancials } from "../../../types/financials";
|
|
|
4
4
|
import type { TickerRecord } from "../../../types/ticker";
|
|
5
5
|
import { priceColor } from "../../../theme/colors";
|
|
6
6
|
import { clampQuoteTimestamp, formatQuoteAgeWithSource } from "../../../utils/quote-time";
|
|
7
|
-
import { convertCurrency, formatCompact,
|
|
7
|
+
import { convertCurrency, formatCompact, formatNumber, formatPercentRaw } from "../../../utils/format";
|
|
8
|
+
import { formatMarketCost, formatMarketPrice, formatMarketQuantity, formatSignedMarketPrice, type MarketFormatOptions } from "../../../utils/market-format";
|
|
8
9
|
import { getActiveQuoteDisplay, marketStateDot, type ActiveQuoteDisplay } from "../../../utils/market-status";
|
|
9
10
|
import { formatOptionTicker } from "../../../utils/options";
|
|
10
11
|
|
|
@@ -21,6 +22,7 @@ interface PortfolioPositionMetrics {
|
|
|
21
22
|
totalCost: number;
|
|
22
23
|
totalCostUnits: number;
|
|
23
24
|
totalPriceUnits: number;
|
|
25
|
+
multiplierHint: number;
|
|
24
26
|
brokerMktValue: number;
|
|
25
27
|
hasBrokerMktValue: boolean;
|
|
26
28
|
brokerPnl: number;
|
|
@@ -95,6 +97,7 @@ function getPortfolioPositionMetrics(
|
|
|
95
97
|
let totalCost = 0;
|
|
96
98
|
let totalCostUnits = 0;
|
|
97
99
|
let totalPriceUnits = 0;
|
|
100
|
+
let multiplierHint = 1;
|
|
98
101
|
let brokerMktValue = 0;
|
|
99
102
|
let hasBrokerMktValue = false;
|
|
100
103
|
let brokerPnl = 0;
|
|
@@ -103,6 +106,7 @@ function getPortfolioPositionMetrics(
|
|
|
103
106
|
const direction = position.side === "short" ? -1 : 1;
|
|
104
107
|
const priceMultiplier = normalizePositionMultiplier(position.multiplier);
|
|
105
108
|
const costMultiplier = resolvePositionCostMultiplier(position);
|
|
109
|
+
multiplierHint = Math.max(multiplierHint, priceMultiplier, costMultiplier);
|
|
106
110
|
|
|
107
111
|
totalShares += position.shares * direction;
|
|
108
112
|
totalCost += position.shares * position.avgCost * costMultiplier;
|
|
@@ -125,6 +129,7 @@ function getPortfolioPositionMetrics(
|
|
|
125
129
|
totalCost,
|
|
126
130
|
totalCostUnits,
|
|
127
131
|
totalPriceUnits,
|
|
132
|
+
multiplierHint,
|
|
128
133
|
brokerMktValue,
|
|
129
134
|
hasBrokerMktValue,
|
|
130
135
|
brokerPnl,
|
|
@@ -155,15 +160,17 @@ function resolveBrokerFallbackPnl(metrics: PortfolioPositionMetrics, brokerMarke
|
|
|
155
160
|
export function resolvePortfolioPriceValue(
|
|
156
161
|
activeQuote: ActiveQuoteDisplay | null,
|
|
157
162
|
brokerMarkPrice: number | undefined,
|
|
163
|
+
formatOptions: MarketFormatOptions,
|
|
164
|
+
maxWidth?: number,
|
|
158
165
|
): { text: string; color?: string } {
|
|
159
166
|
if (activeQuote) {
|
|
160
167
|
return {
|
|
161
|
-
text:
|
|
168
|
+
text: formatMarketPrice(activeQuote.price, { ...formatOptions, maxWidth }),
|
|
162
169
|
color: priceColor(activeQuote.change),
|
|
163
170
|
};
|
|
164
171
|
}
|
|
165
172
|
if (brokerMarkPrice != null) {
|
|
166
|
-
return { text:
|
|
173
|
+
return { text: formatMarketPrice(brokerMarkPrice, { ...formatOptions, maxWidth }) };
|
|
167
174
|
}
|
|
168
175
|
return { text: "—" };
|
|
169
176
|
}
|
|
@@ -180,13 +187,17 @@ export function getColumnValue(
|
|
|
180
187
|
const quoteCurrency = quote?.currency || ticker.metadata.currency || "USD";
|
|
181
188
|
|
|
182
189
|
const positionMetrics = getPortfolioPositionMetrics(ticker, ctx.activeTab, quoteCurrency);
|
|
183
|
-
const { positionCurrency, totalShares, totalCost, totalCostUnits, totalPriceUnits, brokerMarkPrice } = positionMetrics;
|
|
190
|
+
const { positionCurrency, totalShares, totalCost, totalCostUnits, totalPriceUnits, multiplierHint, brokerMarkPrice } = positionMetrics;
|
|
184
191
|
const brokerFallbackMktValue = resolveBrokerFallbackMarketValue(positionMetrics);
|
|
185
192
|
const brokerFallbackPnl = resolveBrokerFallbackPnl(positionMetrics, brokerFallbackMktValue);
|
|
186
193
|
const toBaseQuote = (value: number) =>
|
|
187
194
|
convertCurrency(value, quoteCurrency, ctx.baseCurrency, ctx.exchangeRates);
|
|
188
195
|
const toBasePosition = (value: number) =>
|
|
189
196
|
convertCurrency(value, positionCurrency, ctx.baseCurrency, ctx.exchangeRates);
|
|
197
|
+
const formatOptions: MarketFormatOptions = {
|
|
198
|
+
assetCategory: ticker.metadata.assetCategory,
|
|
199
|
+
multiplier: multiplierHint,
|
|
200
|
+
};
|
|
190
201
|
|
|
191
202
|
switch (col.id) {
|
|
192
203
|
case "ticker": {
|
|
@@ -198,21 +209,21 @@ export function getColumnValue(
|
|
|
198
209
|
return { text: `${statusDot} ${displayName}` };
|
|
199
210
|
}
|
|
200
211
|
case "price":
|
|
201
|
-
return resolvePortfolioPriceValue(activeQuote, brokerMarkPrice);
|
|
212
|
+
return resolvePortfolioPriceValue(activeQuote, brokerMarkPrice, formatOptions, col.width);
|
|
202
213
|
case "change":
|
|
203
214
|
if (!activeQuote) return { text: "—" };
|
|
204
215
|
return {
|
|
205
|
-
text: (activeQuote.change
|
|
216
|
+
text: formatSignedMarketPrice(activeQuote.change, { ...formatOptions, maxWidth: col.width }),
|
|
206
217
|
color: priceColor(activeQuote.change),
|
|
207
218
|
};
|
|
208
219
|
case "bid":
|
|
209
|
-
return { text: quote?.bid != null ?
|
|
220
|
+
return { text: quote?.bid != null ? formatMarketPrice(quote.bid, { ...formatOptions, maxWidth: col.width }) : "—" };
|
|
210
221
|
case "ask":
|
|
211
|
-
return { text: quote?.ask != null ?
|
|
222
|
+
return { text: quote?.ask != null ? formatMarketPrice(quote.ask, { ...formatOptions, maxWidth: col.width }) : "—" };
|
|
212
223
|
case "spread":
|
|
213
224
|
return {
|
|
214
225
|
text: quote?.bid != null && quote?.ask != null
|
|
215
|
-
?
|
|
226
|
+
? formatMarketPrice(quote.ask - quote.bid, { ...formatOptions, maxWidth: col.width })
|
|
216
227
|
: "—",
|
|
217
228
|
};
|
|
218
229
|
case "change_pct":
|
|
@@ -241,10 +252,10 @@ export function getColumnValue(
|
|
|
241
252
|
}
|
|
242
253
|
return { text: "—" };
|
|
243
254
|
case "shares":
|
|
244
|
-
return { text: totalShares !== 0 ?
|
|
255
|
+
return { text: totalShares !== 0 ? formatMarketQuantity(totalShares, { ...formatOptions, maxWidth: col.width }) : "—" };
|
|
245
256
|
case "avg_cost":
|
|
246
257
|
if (totalCostUnits === 0) return { text: "—" };
|
|
247
|
-
return { text:
|
|
258
|
+
return { text: formatMarketCost(totalCost / Math.abs(totalCostUnits), { ...formatOptions, maxWidth: col.width }) };
|
|
248
259
|
case "cost_basis":
|
|
249
260
|
if (totalCost === 0) return { text: "—" };
|
|
250
261
|
return { text: formatCompact(toBasePosition(totalCost)) };
|
|
@@ -323,16 +323,10 @@ export function PortfolioListPane({ focused, width, height }: PaneProps) {
|
|
|
323
323
|
|
|
324
324
|
flushCursorSymbol(cursorSymbol);
|
|
325
325
|
|
|
326
|
-
const
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
&& instance.binding.sourceInstanceId === paneId,
|
|
330
|
-
);
|
|
331
|
-
if (follower) {
|
|
332
|
-
registry?.focusPaneFn(follower.instanceId);
|
|
333
|
-
return;
|
|
326
|
+
const ticker = sortedTickers[safeSelectedIdx];
|
|
327
|
+
if (ticker) {
|
|
328
|
+
registry?.navigateTickerFn(ticker.metadata.ticker);
|
|
334
329
|
}
|
|
335
|
-
registry?.showPaneFn("ticker-detail");
|
|
336
330
|
}, [
|
|
337
331
|
cashDrawerExpanded,
|
|
338
332
|
currentTabIdx,
|
|
@@ -11,6 +11,7 @@ export interface PortfolioPaneSettings {
|
|
|
11
11
|
hideHeader: boolean;
|
|
12
12
|
hideCash: boolean;
|
|
13
13
|
lockedCollectionId: string;
|
|
14
|
+
showSparklines?: boolean;
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
export interface CollectionEntry {
|
|
@@ -117,6 +118,7 @@ export function getPortfolioPaneSettings(settings: Record<string, unknown> | und
|
|
|
117
118
|
hideHeader: settings?.hideHeader === true,
|
|
118
119
|
hideCash: settings?.hideCash === true,
|
|
119
120
|
lockedCollectionId: typeof settings?.lockedCollectionId === "string" ? settings.lockedCollectionId : "",
|
|
121
|
+
showSparklines: settings?.showSparklines === true,
|
|
120
122
|
};
|
|
121
123
|
}
|
|
122
124
|
|
|
@@ -129,6 +131,7 @@ export function createPortfolioPaneSettings(overrides: Partial<PortfolioPaneSett
|
|
|
129
131
|
hideHeader: overrides.hideHeader ?? false,
|
|
130
132
|
hideCash: overrides.hideCash ?? false,
|
|
131
133
|
lockedCollectionId: overrides.lockedCollectionId ?? "",
|
|
134
|
+
showSparklines: overrides.showSparklines ?? false,
|
|
132
135
|
};
|
|
133
136
|
}
|
|
134
137
|
|
|
@@ -240,6 +243,12 @@ export function buildPortfolioPaneSettingsDef(config: AppConfig, settings: Portf
|
|
|
240
243
|
description: "Hide the cash & margin drawer at the bottom of the pane.",
|
|
241
244
|
type: "toggle",
|
|
242
245
|
});
|
|
246
|
+
fields.push({
|
|
247
|
+
key: "showSparklines",
|
|
248
|
+
label: "Sparklines",
|
|
249
|
+
description: "Show a tiny price history chart in each row.",
|
|
250
|
+
type: "toggle",
|
|
251
|
+
});
|
|
243
252
|
|
|
244
253
|
if (settings.hideTabs && lockedCollectionOptions.length > 0) {
|
|
245
254
|
fields.push({
|
|
@@ -25,6 +25,7 @@ export function PortfolioTickerTable({
|
|
|
25
25
|
financialsMap,
|
|
26
26
|
columnContext,
|
|
27
27
|
flashSymbols,
|
|
28
|
+
showSparklines,
|
|
28
29
|
}: {
|
|
29
30
|
columns: ColumnConfig[];
|
|
30
31
|
sortColumnId: string | null;
|
|
@@ -42,6 +43,7 @@ export function PortfolioTickerTable({
|
|
|
42
43
|
financialsMap: Map<string, TickerFinancials>;
|
|
43
44
|
columnContext: ColumnContext;
|
|
44
45
|
flashSymbols: Map<string, QuoteFlashDirection>;
|
|
46
|
+
showSparklines?: boolean;
|
|
45
47
|
}) {
|
|
46
48
|
const resolveCell = useCallback(
|
|
47
49
|
(column: ColumnConfig, ticker: TickerRecord, financials: TickerFinancials | undefined) => (
|
|
@@ -68,6 +70,7 @@ export function PortfolioTickerTable({
|
|
|
68
70
|
sortColumnId={sortColumnId}
|
|
69
71
|
sortDirection={sortDirection}
|
|
70
72
|
onHeaderClick={onHeaderClick}
|
|
73
|
+
showSparklines={showSparklines}
|
|
71
74
|
/>
|
|
72
75
|
);
|
|
73
76
|
}
|
|
@@ -463,6 +463,7 @@ describe("PortfolioListPane cash and margin UI", () => {
|
|
|
463
463
|
netLiquidation: 125000,
|
|
464
464
|
cashBalances: [
|
|
465
465
|
{ currency: "USD", quantity: -50000, baseValue: -50000, baseCurrency: "USD" },
|
|
466
|
+
{ currency: "EUR", quantity: -351957.025, baseValue: -381000, baseCurrency: "USD" },
|
|
466
467
|
{ currency: "JPY", quantity: 0, baseValue: 0, baseCurrency: "USD" },
|
|
467
468
|
],
|
|
468
469
|
}],
|
|
@@ -478,6 +479,7 @@ describe("PortfolioListPane cash and margin UI", () => {
|
|
|
478
479
|
expect(frame).toContain("Cash & Margin");
|
|
479
480
|
expect(frame).toContain("Net Liq");
|
|
480
481
|
expect(frame).toContain("Flex Mar 27");
|
|
482
|
+
expect(frame).toContain("-351,957.025");
|
|
481
483
|
expect(frame).not.toContain("Avail");
|
|
482
484
|
expect(frame).not.toContain("JPY");
|
|
483
485
|
});
|
|
@@ -654,7 +656,7 @@ describe("PortfolioListPane cash and margin UI", () => {
|
|
|
654
656
|
expect(frame).toContain("SPREAD");
|
|
655
657
|
expect(frame).toContain("124.95");
|
|
656
658
|
expect(frame).toContain("125.05");
|
|
657
|
-
expect(frame).toContain("0.
|
|
659
|
+
expect(frame).toContain("0.1");
|
|
658
660
|
});
|
|
659
661
|
|
|
660
662
|
test("warms full financials for visible rows when only quote data is loaded", async () => {
|
|
@@ -196,7 +196,7 @@ describe("portfolio-metrics", () => {
|
|
|
196
196
|
const pnlColumn: ColumnConfig = { id: "pnl", label: "P&L", width: 10, align: "right", format: "compact" };
|
|
197
197
|
|
|
198
198
|
expect(getColumnValue(avgCostColumn, ticker, financials, defaultColumnContext)).toEqual({
|
|
199
|
-
text: "5,095.
|
|
199
|
+
text: "5,095.073",
|
|
200
200
|
});
|
|
201
201
|
expect(getColumnValue(pnlColumn, ticker, financials, defaultColumnContext)).toEqual({
|
|
202
202
|
text: "+7.9k",
|
|
@@ -204,6 +204,18 @@ describe("portfolio-metrics", () => {
|
|
|
204
204
|
});
|
|
205
205
|
});
|
|
206
206
|
|
|
207
|
+
test("formats equity average cost with tighter precision than quote prices", () => {
|
|
208
|
+
const ticker = createTicker({
|
|
209
|
+
assetCategory: "STK",
|
|
210
|
+
positions: [{ portfolio: "main", shares: 10, avgCost: 119.3687, broker: "manual", currency: "HKD" }],
|
|
211
|
+
});
|
|
212
|
+
const avgCostColumn: ColumnConfig = { id: "avg_cost", label: "AVG COST", width: 10, align: "right", format: "currency" };
|
|
213
|
+
|
|
214
|
+
expect(getColumnValue(avgCostColumn, ticker, undefined, defaultColumnContext)).toEqual({
|
|
215
|
+
text: "119.37",
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
|
|
207
219
|
test("formats portfolio-only column values and sort keys consistently", () => {
|
|
208
220
|
const ticker = createTicker({
|
|
209
221
|
positions: [{ portfolio: "main", shares: 10, avgCost: 100, broker: "manual" }],
|