gloomberb 0.16.2 → 0.16.3
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/package.json +1 -1
- package/src/brokers/account-cache.ts +45 -0
- package/src/cli/command-options.ts +74 -0
- package/src/cli/commands/command-utils.ts +29 -0
- package/src/cli/commands/market.ts +170 -54
- package/src/cli/commands/overview.ts +62 -19
- package/src/cli/commands/ticker.ts +138 -43
- package/src/cli/desktop-pane-shot.ts +3 -0
- package/src/cli/help.ts +26 -6
- package/src/cli/history-rows.ts +187 -15
- package/src/cli/index.ts +12 -7
- package/src/cli/options.ts +5 -1
- package/src/cli/pane-functions/capabilities.ts +5 -0
- package/src/cli/pane-functions/dom.ts +69 -16
- package/src/cli/pane-functions/function-help.ts +4 -2
- package/src/cli/pane-functions/headless.ts +163 -31
- package/src/cli/pane-functions/index.ts +9 -2
- package/src/cli/pane-functions/report.ts +8 -1
- package/src/cli/pane-functions/resolver.ts +28 -2
- package/src/cli/pane-functions/screenshot.ts +4 -0
- package/src/cli/registry.ts +6 -3
- package/src/cli/report-tables.ts +460 -0
- package/src/cli/result.ts +13 -3
- package/src/components/data-table/export.ts +6 -51
- package/src/components/data-table/view.tsx +3 -1
- package/src/components/layout/shell/active-drag.ts +67 -35
- package/src/components/layout/shell/cursor-occlusion.ts +6 -5
- package/src/components/layout/shell/drag/index.ts +12 -7
- package/src/components/layout/shell/drag/live.ts +122 -0
- package/src/components/layout/shell/drag/overlays.tsx +179 -35
- package/src/components/layout/shell/drag/runtime.ts +62 -50
- package/src/components/layout/shell/drag/slide.ts +10 -0
- package/src/components/layout/shell/index.tsx +37 -51
- package/src/components/layout/shell/native/pointer-runtime.ts +13 -13
- package/src/components/layout/shell/native/surfaces.ts +30 -14
- package/src/components/layout/shell/pane/layers.tsx +419 -179
- package/src/components/layout/shell/terminal-pointer-runtime.ts +18 -18
- package/src/components/layout/shell/window-mode/overlays.tsx +12 -4
- package/src/data/json-persistence.ts +17 -3
- package/src/data/json-storage.ts +2 -0
- package/src/data/memory-resource-store.ts +46 -0
- package/src/market-data/market/freshness.ts +45 -0
- package/src/market-data/quotes/freshness.ts +5 -0
- package/src/plugins/builtin/analytics/pane-model.ts +10 -6
- package/src/plugins/builtin/attention/pane.tsx +1 -1
- package/src/plugins/builtin/catalysts/detail.tsx +1 -1
- package/src/plugins/builtin/company-attention/pane.tsx +1 -1
- package/src/plugins/builtin/holders/client.ts +3 -6
- package/src/plugins/builtin/holders/format.ts +31 -1
- package/src/plugins/builtin/holders/headless.ts +26 -5
- package/src/plugins/builtin/holders/pane.tsx +22 -13
- package/src/plugins/builtin/holders/report-header.ts +46 -5
- package/src/plugins/builtin/kelly-sizer/pane.tsx +12 -6
- package/src/plugins/builtin/kelly-sizer/portfolio.ts +25 -0
- package/src/plugins/builtin/members/pane.tsx +10 -0
- package/src/plugins/builtin/perps/pane.tsx +1 -1
- package/src/plugins/builtin/portfolio-list/account-metrics.ts +16 -1
- package/src/plugins/builtin/portfolio-list/allocation.ts +3 -0
- package/src/plugins/builtin/portfolio-list/cached-account.ts +18 -9
- package/src/plugins/builtin/portfolio-list/cli/render.ts +2 -0
- package/src/plugins/builtin/portfolio-list/headless.ts +2 -0
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +7 -4
- package/src/plugins/builtin/portfolio-list/summary/index.tsx +22 -4
- package/src/plugins/builtin/portfolio-list/summary/live-accounts.ts +11 -1
- package/src/plugins/builtin/research/relative-valuation-model.ts +7 -3
- package/src/plugins/builtin/reverse-dcf/model.ts +2 -1
- package/src/plugins/builtin/shared/members-menu.tsx +260 -0
- package/src/plugins/builtin/short-volume/pane.tsx +1 -1
- package/src/plugins/builtin/social-mentions/pane.tsx +1 -1
- package/src/plugins/builtin/themes/pane.tsx +3 -0
- package/src/plugins/builtin/ticker-detail/financials/aggregation.ts +4 -0
- package/src/plugins/builtin/ticker-detail/financials/model.ts +25 -2
- package/src/plugins/builtin/ticker-detail/headless.ts +20 -4
- package/src/plugins/builtin/ticker-detail/overview/model.ts +9 -4
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +3 -2
- package/src/plugins/builtin/world-venue-map/basemap-layer.tsx +2 -0
- package/src/plugins/builtin/world-venue-map/basemap.ts +18 -0
- package/src/plugins/builtin/world-venue-map/country-labels.ts +78 -0
- package/src/plugins/builtin/world-venue-map/geo-svg.tsx +2 -1
- package/src/plugins/builtin/world-venue-map/index.ts +5 -5
- package/src/plugins/builtin/world-venue-map/layered-pane.tsx +27 -14
- package/src/plugins/builtin/world-venue-map/layers.ts +68 -10
- package/src/plugins/builtin/world-venue-map/map.tsx +198 -62
- package/src/plugins/builtin/world-venue-map/model.ts +16 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/index.chunk.ts +3 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/labels.chunk.ts +244 -0
- package/src/plugins/builtin/world-venue-map/pane.tsx +16 -10
- package/src/renderers/browser/app-services.ts +3 -1
- package/src/renderers/dom/data-table/dom.ts +31 -0
- package/src/renderers/dom/data-table/index.tsx +5 -0
- package/src/renderers/dom/host/box.tsx +113 -17
- package/src/renderers/dom/host/drag-shield.ts +24 -0
- package/src/renderers/dom/host/live-frame.ts +54 -0
- package/src/renderers/dom/host/mouse.ts +8 -1
- package/src/renderers/dom/host/scroll-box.tsx +6 -5
- package/src/renderers/dom/host/tabs.tsx +3 -2
- package/src/renderers/dom/styles.css +13 -5
- package/src/renderers/electrobun/bun/desktop/backend-requests.ts +7 -0
- package/src/renderers/electrobun/bun/desktop/backend.ts +2 -0
- package/src/renderers/electrobun/bun/desktop/initialization.ts +13 -0
- package/src/renderers/electrobun/shared/protocol.ts +10 -1
- package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +3 -0
- package/src/renderers/electrobun/view/host/native.ts +0 -10
- package/src/renderers/electrobun/view/host/window-move.ts +28 -0
- package/src/renderers/electrobun/view/remote/persistence.ts +15 -2
- package/src/renderers/electrobun/view/ui-host.tsx +2 -5
- package/src/renderers/opentui/ui-host.tsx +7 -1
- package/src/sources/gloomberb-cloud/normalizers.ts +16 -20
- package/src/sources/provider-router/batches.ts +3 -3
- package/src/sources/provider-router/financial-routes.ts +2 -2
- package/src/sources/provider-router/financials.ts +70 -7
- package/src/sources/provider-router/primary.ts +2 -2
- package/src/state/pane-scroll-registry.tsx +67 -35
- package/src/test-support/margin-account.ts +108 -0
- package/src/time-series/resolution.ts +15 -0
- package/src/types/financials.ts +32 -3
- package/src/ui/host.tsx +12 -0
- package/src/ui/index.tsx +8 -3
- package/src/ui/scroll-box-axis.ts +26 -0
- package/src/utils/currency-units.ts +8 -3
- package/src/utils/depositary-receipt.ts +54 -10
- package/src/utils/display-number.ts +63 -0
- package/src/utils/fundamentals.ts +17 -1
- package/src/utils/price-history-integrity.ts +19 -6
|
@@ -87,8 +87,8 @@ export function useShellTerminalPointerRuntime({
|
|
|
87
87
|
windowMode,
|
|
88
88
|
}: UseShellTerminalPointerRuntimeOptions) {
|
|
89
89
|
const {
|
|
90
|
-
|
|
91
|
-
|
|
90
|
+
live,
|
|
91
|
+
startDrag,
|
|
92
92
|
updateDividerPreview,
|
|
93
93
|
updateDragFloatingRect,
|
|
94
94
|
} = dragRuntime;
|
|
@@ -112,7 +112,7 @@ export function useShellTerminalPointerRuntime({
|
|
|
112
112
|
|
|
113
113
|
for (const { pane, rect: visibleRect } of sortedFloatingPanes(visibleFloatingPanes)) {
|
|
114
114
|
const paneId = pane.instance.instanceId;
|
|
115
|
-
const rect = getVisibleFloatingRect(visibleRect, paneId,
|
|
115
|
+
const rect = getVisibleFloatingRect(visibleRect, paneId, live.get().floating, width, contentHeight);
|
|
116
116
|
if (!pointInRect({ x: rect.x, y: rect.y, width: rect.width, height: rect.height }, event.x, shellY)) continue;
|
|
117
117
|
|
|
118
118
|
const relativeX = event.x - rect.x;
|
|
@@ -120,13 +120,13 @@ export function useShellTerminalPointerRuntime({
|
|
|
120
120
|
selectWindowModePane(paneId);
|
|
121
121
|
|
|
122
122
|
if (relativeX >= rect.width - 2 && relativeY >= rect.height - 1) {
|
|
123
|
-
|
|
123
|
+
startDrag({
|
|
124
124
|
type: "float-resize",
|
|
125
125
|
paneId,
|
|
126
126
|
startX: preciseX,
|
|
127
127
|
startY: preciseShellY,
|
|
128
128
|
origRect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height },
|
|
129
|
-
};
|
|
129
|
+
});
|
|
130
130
|
updateDragFloatingRect({ paneId, rect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height } });
|
|
131
131
|
}
|
|
132
132
|
|
|
@@ -138,7 +138,7 @@ export function useShellTerminalPointerRuntime({
|
|
|
138
138
|
if (!transientFocusActive) {
|
|
139
139
|
for (const divider of dockDividerLayouts) {
|
|
140
140
|
if (!pointInRect(divider.rect, event.x, shellY)) continue;
|
|
141
|
-
|
|
141
|
+
startDrag({
|
|
142
142
|
type: "divider",
|
|
143
143
|
path: divider.path,
|
|
144
144
|
axis: divider.axis,
|
|
@@ -146,7 +146,7 @@ export function useShellTerminalPointerRuntime({
|
|
|
146
146
|
startY: preciseShellY,
|
|
147
147
|
startRatio: divider.ratio,
|
|
148
148
|
bounds: divider.bounds,
|
|
149
|
-
};
|
|
149
|
+
});
|
|
150
150
|
updateDividerPreview({
|
|
151
151
|
pathKey: divider.path.join("."),
|
|
152
152
|
rect: divider.rect,
|
|
@@ -179,7 +179,7 @@ export function useShellTerminalPointerRuntime({
|
|
|
179
179
|
|
|
180
180
|
for (const { pane, rect: visibleRect } of sortedFloatingPanes(visibleFloatingPanes)) {
|
|
181
181
|
const paneId = pane.instance.instanceId;
|
|
182
|
-
const rect = getVisibleFloatingRect(visibleRect, paneId,
|
|
182
|
+
const rect = getVisibleFloatingRect(visibleRect, paneId, live.get().floating, width, contentHeight);
|
|
183
183
|
if (!pointInRect({ x: rect.x, y: rect.y, width: rect.width, height: rect.height }, event.x, shellY)) continue;
|
|
184
184
|
const relativeX = event.x - rect.x;
|
|
185
185
|
const relativeY = shellY - rect.y;
|
|
@@ -212,13 +212,13 @@ export function useShellTerminalPointerRuntime({
|
|
|
212
212
|
return;
|
|
213
213
|
}
|
|
214
214
|
if (!transientFocusActive && relativeX >= rect.width - 2 && relativeY >= rect.height - 1) {
|
|
215
|
-
|
|
215
|
+
startDrag({
|
|
216
216
|
type: "float-resize",
|
|
217
217
|
paneId,
|
|
218
218
|
startX: preciseX,
|
|
219
219
|
startY: preciseShellY,
|
|
220
220
|
origRect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height },
|
|
221
|
-
};
|
|
221
|
+
});
|
|
222
222
|
updateDragFloatingRect({ paneId, rect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height } });
|
|
223
223
|
event.stopPropagation();
|
|
224
224
|
event.preventDefault();
|
|
@@ -230,14 +230,14 @@ export function useShellTerminalPointerRuntime({
|
|
|
230
230
|
return;
|
|
231
231
|
}
|
|
232
232
|
if (relativeY === 0) {
|
|
233
|
-
|
|
233
|
+
startDrag({
|
|
234
234
|
type: "pane-drag",
|
|
235
235
|
paneId,
|
|
236
236
|
mode: "floating",
|
|
237
237
|
startX: preciseX,
|
|
238
238
|
startY: preciseShellY,
|
|
239
239
|
origRect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height },
|
|
240
|
-
};
|
|
240
|
+
});
|
|
241
241
|
updateDragFloatingRect({ paneId, rect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height } });
|
|
242
242
|
event.stopPropagation();
|
|
243
243
|
event.preventDefault();
|
|
@@ -248,7 +248,7 @@ export function useShellTerminalPointerRuntime({
|
|
|
248
248
|
|
|
249
249
|
for (const divider of dockDividerLayouts) {
|
|
250
250
|
if (!pointInRect(divider.rect, event.x, shellY)) continue;
|
|
251
|
-
|
|
251
|
+
startDrag({
|
|
252
252
|
type: "divider",
|
|
253
253
|
path: divider.path,
|
|
254
254
|
axis: divider.axis,
|
|
@@ -256,7 +256,7 @@ export function useShellTerminalPointerRuntime({
|
|
|
256
256
|
startY: preciseShellY,
|
|
257
257
|
startRatio: divider.ratio,
|
|
258
258
|
bounds: divider.bounds,
|
|
259
|
-
};
|
|
259
|
+
});
|
|
260
260
|
updateDividerPreview({
|
|
261
261
|
pathKey: divider.path.join("."),
|
|
262
262
|
rect: divider.rect,
|
|
@@ -308,14 +308,14 @@ export function useShellTerminalPointerRuntime({
|
|
|
308
308
|
return;
|
|
309
309
|
}
|
|
310
310
|
if (relativeY === 0) {
|
|
311
|
-
|
|
311
|
+
startDrag({
|
|
312
312
|
type: "pane-drag",
|
|
313
313
|
paneId: leaf.instanceId,
|
|
314
314
|
mode: "docked",
|
|
315
315
|
startX: preciseX,
|
|
316
316
|
startY: preciseShellY,
|
|
317
317
|
origRect: { x: leaf.rect.x, y: leaf.rect.y, width: leaf.rect.width, height: leaf.rect.height },
|
|
318
|
-
};
|
|
318
|
+
});
|
|
319
319
|
event.stopPropagation();
|
|
320
320
|
event.preventDefault();
|
|
321
321
|
return;
|
|
@@ -333,12 +333,11 @@ export function useShellTerminalPointerRuntime({
|
|
|
333
333
|
contentHeight,
|
|
334
334
|
dockDividerLayouts,
|
|
335
335
|
dockLeafLayouts,
|
|
336
|
-
dragFloatingRect,
|
|
337
|
-
dragRef,
|
|
338
336
|
focusPane,
|
|
339
337
|
focusedPaneId,
|
|
340
338
|
handleActiveDrag,
|
|
341
339
|
handleFloatingClose,
|
|
340
|
+
live,
|
|
342
341
|
restoreFullscreen,
|
|
343
342
|
menuState,
|
|
344
343
|
openPaneMenu,
|
|
@@ -346,6 +345,7 @@ export function useShellTerminalPointerRuntime({
|
|
|
346
345
|
selectWindowModePane,
|
|
347
346
|
setHoveredMenuItemId,
|
|
348
347
|
setMenuState,
|
|
348
|
+
startDrag,
|
|
349
349
|
transientFocusActive,
|
|
350
350
|
updateDividerPreview,
|
|
351
351
|
updateDragFloatingRect,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { useCallback } from "react";
|
|
1
2
|
import { Box, Text } from "../../../../ui";
|
|
2
3
|
import type {
|
|
3
4
|
DockGeometryOptions,
|
|
@@ -8,6 +9,7 @@ import type {
|
|
|
8
9
|
} from "../../../../layout/pane-manager";
|
|
9
10
|
import { colors } from "../../../../theme/colors";
|
|
10
11
|
import { constrainFloatingRectToBounds } from "../drag";
|
|
12
|
+
import { useLiveDrag, type LiveDragGeometry, type LiveDragStore } from "../drag/live";
|
|
11
13
|
import {
|
|
12
14
|
getFloatingResizeCornerPosition,
|
|
13
15
|
type WindowEditDockMovePreview,
|
|
@@ -27,9 +29,9 @@ interface ShellWindowModeOverlaysProps {
|
|
|
27
29
|
contentHeight: number;
|
|
28
30
|
dockGeometryOptions: DockGeometryOptions;
|
|
29
31
|
dockLeafLayouts: DockLeafLayout[];
|
|
30
|
-
dragFloatingRect: { paneId: string; rect: FloatingRect } | null;
|
|
31
32
|
focusedPaneId: string | null;
|
|
32
33
|
getPaneTitle: (pane: ResolvedPane) => string;
|
|
34
|
+
live: LiveDragStore;
|
|
33
35
|
menuOpen: boolean;
|
|
34
36
|
nativePaneChrome: boolean;
|
|
35
37
|
nativeWindowModePanelRect: LayoutBounds | null;
|
|
@@ -64,9 +66,9 @@ export function ShellWindowModeOverlays({
|
|
|
64
66
|
contentHeight,
|
|
65
67
|
dockGeometryOptions,
|
|
66
68
|
dockLeafLayouts,
|
|
67
|
-
dragFloatingRect,
|
|
68
69
|
focusedPaneId,
|
|
69
70
|
getPaneTitle,
|
|
71
|
+
live,
|
|
70
72
|
menuOpen,
|
|
71
73
|
nativePaneChrome,
|
|
72
74
|
nativeWindowModePanelRect,
|
|
@@ -84,9 +86,15 @@ export function ShellWindowModeOverlays({
|
|
|
84
86
|
const highlightedDockedLeaf = highlightedPaneId && !highlightedFloatingPane
|
|
85
87
|
? dockLeafLayouts.find((leaf) => leaf.instanceId === highlightedPaneId)
|
|
86
88
|
: null;
|
|
89
|
+
// The terminal frame follows a floating pane while it moves; desktop panes draw their own.
|
|
90
|
+
const followedPaneId = !nativePaneChrome && highlightedFloatingPane ? highlightedFloatingPane.pane.instance.instanceId : null;
|
|
91
|
+
const selectDragRect = useCallback((geometry: LiveDragGeometry) => (
|
|
92
|
+
followedPaneId && geometry.floating?.paneId === followedPaneId ? geometry.floating.rect : null
|
|
93
|
+
), [followedPaneId]);
|
|
94
|
+
const dragRect = useLiveDrag(live, selectDragRect);
|
|
87
95
|
const highlightedRect = highlightedFloatingPane
|
|
88
|
-
?
|
|
89
|
-
? constrainFloatingRectToBounds(
|
|
96
|
+
? dragRect
|
|
97
|
+
? constrainFloatingRectToBounds(dragRect, width, contentHeight)
|
|
90
98
|
: highlightedFloatingPane.rect
|
|
91
99
|
: highlightedDockedLeaf?.rect ?? null;
|
|
92
100
|
const highlightedZIndex = highlightedFloatingPane
|
|
@@ -1,21 +1,35 @@
|
|
|
1
1
|
import type { AppPersistencePort } from "../core/app-service-ports";
|
|
2
2
|
import type { PluginStateRecord } from "./plugin-state-store";
|
|
3
3
|
import type { SessionSnapshotRecord } from "./session-store";
|
|
4
|
-
import {
|
|
4
|
+
import { resourceRecordKey, WriteThroughResourceStore } from "./memory-resource-store";
|
|
5
|
+
import type { CachedResourceRecord, ResourceCacheKey } from "./resource-store";
|
|
5
6
|
import { BROWSER_STORAGE_KEYS, SafeJsonStorage, type StorageLike } from "./json-storage";
|
|
6
7
|
import { isRecord } from "../utils/guards";
|
|
7
8
|
|
|
8
9
|
type PluginState = Record<string, Record<string, PluginStateRecord>>;
|
|
9
10
|
type Sessions = Record<string, SessionSnapshotRecord>;
|
|
11
|
+
type SavedResources = Record<string, CachedResourceRecord>;
|
|
10
12
|
|
|
11
13
|
export class JsonPersistence implements AppPersistencePort {
|
|
12
|
-
|
|
14
|
+
/** In memory, except the records `keepResource` picks, which outlive a reload like the positions do. */
|
|
15
|
+
readonly resources: WriteThroughResourceStore;
|
|
13
16
|
private readonly pluginStateData: SafeJsonStorage<PluginState>;
|
|
14
17
|
private readonly sessionData: SafeJsonStorage<Sessions>;
|
|
18
|
+
private readonly resourceData: SafeJsonStorage<SavedResources>;
|
|
15
19
|
|
|
16
|
-
constructor(storage?: StorageLike) {
|
|
20
|
+
constructor(storage?: StorageLike, { keepResource = () => false }: { keepResource?: (key: ResourceCacheKey) => boolean } = {}) {
|
|
17
21
|
this.pluginStateData = new SafeJsonStorage(storage, BROWSER_STORAGE_KEYS.pluginState, {}, (value): value is PluginState => isRecord(value));
|
|
18
22
|
this.sessionData = new SafeJsonStorage(storage, BROWSER_STORAGE_KEYS.session, {}, (value): value is Sessions => isRecord(value));
|
|
23
|
+
this.resourceData = new SafeJsonStorage(storage, BROWSER_STORAGE_KEYS.resources, {}, (value): value is SavedResources => isRecord(value));
|
|
24
|
+
const saved = Object.values(this.resourceData.get()).filter((record) => isRecord(record) && typeof record.kind === "string");
|
|
25
|
+
this.resources = new WriteThroughResourceStore(saved, {
|
|
26
|
+
set: (record) => this.resourceData.set({ ...this.resourceData.get(), [resourceRecordKey(record)]: record }),
|
|
27
|
+
delete: (key) => {
|
|
28
|
+
const next = { ...this.resourceData.get() };
|
|
29
|
+
delete next[resourceRecordKey(key)];
|
|
30
|
+
this.resourceData.set(next);
|
|
31
|
+
},
|
|
32
|
+
}, keepResource);
|
|
19
33
|
}
|
|
20
34
|
|
|
21
35
|
readonly pluginState = {
|
package/src/data/json-storage.ts
CHANGED
|
@@ -47,4 +47,6 @@ export const BROWSER_STORAGE_KEYS = {
|
|
|
47
47
|
tickers: "gloomberb.web.tickers.v1",
|
|
48
48
|
pluginState: "gloomberb.web.plugin-state.v1",
|
|
49
49
|
session: "gloomberb.web.session.v1",
|
|
50
|
+
/** Broker account snapshots; other caches stay in memory. */
|
|
51
|
+
resources: "gloomberb.web.resources.v1",
|
|
50
52
|
} as const;
|
|
@@ -103,4 +103,50 @@ export class MemoryResourceStore {
|
|
|
103
103
|
delete(key: ResourceCacheKey): void {
|
|
104
104
|
this.records.delete(buildMapKey(key));
|
|
105
105
|
}
|
|
106
|
+
|
|
107
|
+
/** Puts back a record saved elsewhere, with its own times. */
|
|
108
|
+
protected restore(record: CachedResourceRecord): void {
|
|
109
|
+
this.records.set(buildMapKey(record), { ...record });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function resourceRecordKey(key: ResourceCacheKey): string {
|
|
114
|
+
return buildMapKey(key);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Where a renderer without the cache database keeps the records that must outlive a restart. */
|
|
118
|
+
export interface ResourceRecordSink {
|
|
119
|
+
set(record: CachedResourceRecord): void;
|
|
120
|
+
delete(key: ResourceCacheKey): void;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* The in-memory store, except that the records `durable` picks are handed to
|
|
125
|
+
* a sink as they change and come back from it on the next start. The desktop
|
|
126
|
+
* view and the web app have no cache database: without this a broker
|
|
127
|
+
* account's snapshot lived only until the app closed, while its positions
|
|
128
|
+
* were saved.
|
|
129
|
+
*/
|
|
130
|
+
export class WriteThroughResourceStore extends MemoryResourceStore {
|
|
131
|
+
constructor(
|
|
132
|
+
saved: readonly CachedResourceRecord[],
|
|
133
|
+
private readonly sink: ResourceRecordSink,
|
|
134
|
+
private readonly durable: (key: ResourceCacheKey) => boolean,
|
|
135
|
+
) {
|
|
136
|
+
super();
|
|
137
|
+
for (const record of saved) {
|
|
138
|
+
if (durable(record)) this.restore(record);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
override set<T>(key: ResourceCacheKey, value: T, options: SetResourceOptions): CachedResourceRecord<T> {
|
|
143
|
+
const record = super.set(key, value, options);
|
|
144
|
+
if (this.durable(key)) this.sink.set(record);
|
|
145
|
+
return record;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
override delete(key: ResourceCacheKey): void {
|
|
149
|
+
super.delete(key);
|
|
150
|
+
if (this.durable(key)) this.sink.delete(key);
|
|
151
|
+
}
|
|
106
152
|
}
|
|
@@ -414,6 +414,51 @@ export function isUsPriorSessionPremarketQuote(
|
|
|
414
414
|
return localTradingDaysBetween(canonical, timestampDate, currentDate) === 1;
|
|
415
415
|
}
|
|
416
416
|
|
|
417
|
+
/**
|
|
418
|
+
* After the regular close and before any after-hours trade, a US quote's last
|
|
419
|
+
* print is today's regular session, and that close is the current price: a thin
|
|
420
|
+
* listing may not trade again tonight, and for fifteen minutes after the close
|
|
421
|
+
* the delayed feed still shows the session's last minutes. The source labels
|
|
422
|
+
* the quote POST (or CLOSED) and has no after-hours price to show. The print
|
|
423
|
+
* must be from today's New York trading day, from the regular open through the
|
|
424
|
+
* regular close (the published early close included). An earlier day's print
|
|
425
|
+
* or this morning's pre-market is not today's close.
|
|
426
|
+
*/
|
|
427
|
+
export function isUsRegularCloseQuoteInPostSession(
|
|
428
|
+
timestampMs: number,
|
|
429
|
+
exchange: string | undefined,
|
|
430
|
+
marketState: MarketState | undefined,
|
|
431
|
+
now = Date.now(),
|
|
432
|
+
): boolean {
|
|
433
|
+
const canonical = canonicalExchange(exchange);
|
|
434
|
+
if ((marketState !== "POST" && marketState !== "CLOSED") || !isUsListingExchange(canonical)) return false;
|
|
435
|
+
if (!Number.isFinite(timestampMs) || !Number.isFinite(now) || timestampMs > now + quoteFutureToleranceMs()) return false;
|
|
436
|
+
if (!Number.isFinite(new Date(now).getTime()) || usSessionState(now) !== "POST") return false;
|
|
437
|
+
const currentDate = exchangeLocalDate(canonical, now);
|
|
438
|
+
if (!currentDate || exchangeLocalDate(canonical, timestampMs) !== currentDate) return false;
|
|
439
|
+
if (!isLocalTradingDay(canonical, currentDate)) return false;
|
|
440
|
+
const published = getPublishedUsEquitySession(canonical, currentDate);
|
|
441
|
+
if (published) return published.kind === "session" && timestampMs >= published.open && timestampMs <= published.close;
|
|
442
|
+
return usSessionState(timestampMs) === "REGULAR";
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Whether a US print belongs to the after-hours session in progress: today's,
|
|
447
|
+
* from the regular close. An earlier day's after-hours print is not.
|
|
448
|
+
*/
|
|
449
|
+
export function isUsPostSessionPrint(
|
|
450
|
+
timestampMs: number,
|
|
451
|
+
exchange: string | undefined,
|
|
452
|
+
now = Date.now(),
|
|
453
|
+
): boolean {
|
|
454
|
+
const canonical = canonicalExchange(exchange);
|
|
455
|
+
if (!isUsListingExchange(canonical)) return false;
|
|
456
|
+
if (!Number.isFinite(timestampMs) || !Number.isFinite(now) || timestampMs > now + quoteFutureToleranceMs()) return false;
|
|
457
|
+
if (!Number.isFinite(new Date(now).getTime()) || usSessionState(now) !== "POST") return false;
|
|
458
|
+
const currentDate = exchangeLocalDate(canonical, now);
|
|
459
|
+
return !!currentDate && exchangeLocalDate(canonical, timestampMs) === currentDate && usSessionState(timestampMs) === "POST";
|
|
460
|
+
}
|
|
461
|
+
|
|
417
462
|
export function isTimestampStaleForExchangeSession(
|
|
418
463
|
timestampMs: number,
|
|
419
464
|
exchange?: string,
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
activeUsExtendedHoursSession,
|
|
8
8
|
isTimestampStaleForExchangeSession,
|
|
9
9
|
isUsPriorSessionPremarketQuote,
|
|
10
|
+
isUsRegularCloseQuoteInPostSession,
|
|
10
11
|
} from "../market/freshness";
|
|
11
12
|
|
|
12
13
|
// Give the next regular session's NAV time to publish after its actual close.
|
|
@@ -61,6 +62,10 @@ function isQuoteMissingActiveSessionPrice(quote: Quote, now: number): boolean {
|
|
|
61
62
|
if (!isExtendedHoursExchange(quote)) return false;
|
|
62
63
|
const activeSession = activeUsExtendedHoursSession(now);
|
|
63
64
|
if (!activeSession) return false;
|
|
65
|
+
// No after-hours trade yet: today's regular close is the current price.
|
|
66
|
+
if (activeSession === "POST" && quote.postMarketPrice == null && isUsRegularCloseQuoteInPostSession(
|
|
67
|
+
quote.lastUpdated, quote.listingExchangeName || quote.exchangeName, quote.marketState, now,
|
|
68
|
+
)) return false;
|
|
64
69
|
if (quote.marketState !== activeSession) return true;
|
|
65
70
|
if (activeSession === "POST") return quote.postMarketPrice == null;
|
|
66
71
|
// No pre-market trade yet: the previous session's close is the current price.
|
|
@@ -12,6 +12,7 @@ import { instrumentFromTicker, type ChartRequest, type TickerInstrumentOptions }
|
|
|
12
12
|
import { buildChartKey } from "../../../market-data/selectors";
|
|
13
13
|
import {
|
|
14
14
|
resolvePortfolioAccountMetrics,
|
|
15
|
+
resolvePortfolioLeverage,
|
|
15
16
|
resolvePortfolioMarketValue,
|
|
16
17
|
resolvePortfolioNetLiquidation,
|
|
17
18
|
} from "../portfolio-list/account-metrics";
|
|
@@ -69,8 +70,8 @@ function formatAccountFreshness(account: ResolvedPortfolioAccountState["account"
|
|
|
69
70
|
}
|
|
70
71
|
|
|
71
72
|
function formatMarginLeverage(netLiquidation: number | undefined, totalMarketValue: number): string | null {
|
|
72
|
-
|
|
73
|
-
return `${
|
|
73
|
+
const leverage = resolvePortfolioLeverage(totalMarketValue, netLiquidation);
|
|
74
|
+
return leverage == null ? null : `${leverage.toFixed(1)}x`;
|
|
74
75
|
}
|
|
75
76
|
|
|
76
77
|
export function buildPortfolioChartTargets(
|
|
@@ -205,6 +206,7 @@ export function buildPortfolioBetaResult(portfolio: PortfolioReturnSeriesResult,
|
|
|
205
206
|
|
|
206
207
|
export function buildAnalyticsSummaryRows({
|
|
207
208
|
accountState,
|
|
209
|
+
activePortfolio,
|
|
208
210
|
brokerPerformance,
|
|
209
211
|
portfolioStats,
|
|
210
212
|
convertAccountValue = (value) => value,
|
|
@@ -232,6 +234,8 @@ export function buildAnalyticsSummaryRows({
|
|
|
232
234
|
const totalMarketValue = resolvePortfolioMarketValue(portfolioStats, account, convertAccountValue, basis);
|
|
233
235
|
const netLiquidation = resolvePortfolioNetLiquidation(portfolioStats, account, convertAccountValue, basis);
|
|
234
236
|
const hasMarketValue = portfolioStats.hasPositions || isFiniteNumber(account?.grossPositionValue);
|
|
237
|
+
// As in the portfolio header: a broker account is worth its equity, and its positions alone are its gross.
|
|
238
|
+
const broker = !!account || !!activePortfolio?.brokerInstanceId;
|
|
235
239
|
|
|
236
240
|
if (portfolioStats.unavailableConversions?.length || (account && !Number.isFinite(convertAccountValue(1)))) {
|
|
237
241
|
rows.push({
|
|
@@ -241,19 +245,19 @@ export function buildAnalyticsSummaryRows({
|
|
|
241
245
|
});
|
|
242
246
|
}
|
|
243
247
|
|
|
244
|
-
if (netLiquidation != null) {
|
|
248
|
+
if (netLiquidation != null || (broker && hasMarketValue)) {
|
|
245
249
|
rows.push({
|
|
246
250
|
id: "net-liquidation",
|
|
247
251
|
label: "Net Liq",
|
|
248
|
-
value: money(netLiquidation),
|
|
249
|
-
color: colors.text,
|
|
252
|
+
value: money(netLiquidation ?? Number.NaN),
|
|
253
|
+
color: netLiquidation != null ? colors.text : colors.textDim,
|
|
250
254
|
});
|
|
251
255
|
}
|
|
252
256
|
|
|
253
257
|
if (hasMarketValue) {
|
|
254
258
|
rows.push({
|
|
255
259
|
id: "total-value",
|
|
256
|
-
label: "Val",
|
|
260
|
+
label: broker || portfolioStats.hasShorts ? "Gross" : "Val",
|
|
257
261
|
value: money(totalMarketValue),
|
|
258
262
|
color: colors.text,
|
|
259
263
|
});
|
|
@@ -58,7 +58,7 @@ function Evidence({ row, data, width, height }: { row: DetailRow; data: Attentio
|
|
|
58
58
|
const sameDay = row.evidence.periodStart.slice(0, 10) === row.evidence.periodEnd.slice(0, 10);
|
|
59
59
|
const period = sameDay ? `${hour(row.evidence.periodStart)} to ${row.evidence.periodEnd.slice(11, 16)} UTC` : `${hour(row.evidence.periodStart)} to ${date(row.evidence.periodEnd)}`;
|
|
60
60
|
const latest = point?.bucketStart === row.history.at(-1)?.bucketStart;
|
|
61
|
-
return <ScrollBox width={width} height={desktop ? undefined : height} flexGrow={1} flexBasis={0} contentOptions={{ paddingX: 1 }}>
|
|
61
|
+
return <ScrollBox scrollY width={width} height={desktop ? undefined : height} flexGrow={1} flexBasis={0} contentOptions={{ paddingX: 1 }}>
|
|
62
62
|
<KeyValueRow label="Listing" value={row.symbol} detail={row.name ?? undefined} labelWidth={20} />
|
|
63
63
|
<SectionHeading title="Publication" />
|
|
64
64
|
<KeyValueRow label="Research hours" value={number(row.researchUnits)} detail={`rounded to ${data.privacy.rounding}`} labelWidth={20} />
|
|
@@ -18,7 +18,7 @@ function Evidence({ event, width, height }: { event: CatalystEvent; width: numbe
|
|
|
18
18
|
const facts = catalystFacts(event);
|
|
19
19
|
const procedures = Array.isArray(event.metadata?.proceduralStatements) ? event.metadata.proceduralStatements.filter((value): value is string => typeof value === "string") : [];
|
|
20
20
|
const documents = Array.isArray(event.metadata?.documents) ? event.metadata.documents.filter((value): value is { url: string; title?: string; label?: string } => !!value && typeof value === "object" && "url" in value && typeof value.url === "string" && /^https?:\/\//.test(value.url)) : [];
|
|
21
|
-
return <ScrollBox width={width} height={desktop ? undefined : height} flexGrow={1} flexBasis={0} contentOptions={{ paddingX: 1 }}>
|
|
21
|
+
return <ScrollBox scrollY width={width} height={desktop ? undefined : height} flexGrow={1} flexBasis={0} contentOptions={{ paddingX: 1 }}>
|
|
22
22
|
<KeyValueRow label="Classification" value={`${catalystLabel(event.type)} · ${catalystAgency(event.agency)} · ${event.country ?? event.jurisdiction}`} labelWidth={18} />
|
|
23
23
|
<KeyValueRow label="Status" value={humanLabel(event.status)} detail={`Revision ${event.revision}`} labelWidth={18} />
|
|
24
24
|
<KeyValueRow label="Announced" value={catalystDate(event, "announced")} labelWidth={18} />
|
|
@@ -33,7 +33,7 @@ const LABEL_COLUMNS = new Set(["chart"]);
|
|
|
33
33
|
function Evidence({ row, width, height }: Size & { row: AttentionRow }) {
|
|
34
34
|
const colors = useThemeColors();
|
|
35
35
|
const desktop = !!useUiCapabilities().nativePaneChrome;
|
|
36
|
-
return <ScrollBox width={width} height={desktop ? undefined : height} flexGrow={1} flexBasis={0} contentOptions={{ paddingX: 1 }}>
|
|
36
|
+
return <ScrollBox scrollY width={width} height={desktop ? undefined : height} flexGrow={1} flexBasis={0} contentOptions={{ paddingX: 1 }}>
|
|
37
37
|
{(row.details ?? Object.entries(row.values).map(([label, value]) => ({ label, value: String(value ?? "--") }))).map((entry) => <KeyValueRow key={entry.label} label={entry.label} value={entry.value} labelWidth={22} />)}
|
|
38
38
|
{row.url ? <Box paddingY={1}><Text fg={colors.textMuted}>{row.url}</Text></Box> : null}
|
|
39
39
|
</ScrollBox>;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { DataProvider, MarketDataRequestContext } from "../../../types/data-provider";
|
|
2
2
|
import type { HolderData } from "../../../types/financials";
|
|
3
|
+
import { holderStakeMarketCap, holderValueCurrency } from "./format";
|
|
3
4
|
|
|
4
5
|
export async function loadHolderData(
|
|
5
6
|
provider: DataProvider,
|
|
@@ -20,10 +21,6 @@ export async function loadHolderSnapshot(
|
|
|
20
21
|
loadHolderData(provider, symbol, exchange),
|
|
21
22
|
provider.getTickerFinancials(symbol, exchange).catch(() => null),
|
|
22
23
|
]);
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
&& financials.quote.currency !== data.currency
|
|
26
|
-
? undefined
|
|
27
|
-
: quoteMarketCap;
|
|
28
|
-
return { data, marketCap };
|
|
24
|
+
const quote = financials?.quote;
|
|
25
|
+
return { data, marketCap: holderStakeMarketCap(quote?.marketCap, quote?.currency, holderValueCurrency(data)) };
|
|
29
26
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { HolderRecord } from "../../../types/financials";
|
|
1
|
+
import type { HolderData, HolderRecord } from "../../../types/financials";
|
|
2
2
|
import { formatCompact, formatPercent, formatPercentRaw } from "../../../utils/format";
|
|
3
3
|
import type { HolderRow } from "./types";
|
|
4
4
|
|
|
@@ -16,15 +16,45 @@ export function formatMaybePercent(value: number | undefined): string {
|
|
|
16
16
|
return Math.abs(value) <= 1 ? formatPercent(value) : formatPercentRaw(value);
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* The currency holder values are in. Gloom Cloud names it when it differs
|
|
21
|
+
* from the listing's (dollars from 13F filings on a London line); an older
|
|
22
|
+
* service priced them in the listing's currency.
|
|
23
|
+
*/
|
|
24
|
+
export function holderValueCurrency(
|
|
25
|
+
data: Pick<HolderData, "valueCurrency" | "currency"> | null | undefined,
|
|
26
|
+
fallback?: string,
|
|
27
|
+
): string | undefined {
|
|
28
|
+
return data?.valueCurrency?.trim() || data?.currency?.trim() || fallback;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The market cap a holding's value can be divided by: only one in the
|
|
33
|
+
* values' own currency. A cap whose currency, or values whose currency, is
|
|
34
|
+
* not known is taken as it was before currencies were stated.
|
|
35
|
+
*/
|
|
36
|
+
export function holderStakeMarketCap(
|
|
37
|
+
marketCap: number | undefined,
|
|
38
|
+
marketCapCurrency: string | null | undefined,
|
|
39
|
+
valueCurrency: string | null | undefined,
|
|
40
|
+
): number | undefined {
|
|
41
|
+
if (marketCap == null) return undefined;
|
|
42
|
+
if (marketCapCurrency && valueCurrency && marketCapCurrency !== valueCurrency) return undefined;
|
|
43
|
+
return marketCap;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** The reported stake, else the value over a market cap `holderStakeMarketCap` allowed. */
|
|
19
47
|
export function resolveHolderOwnershipPercent(row: Pick<HolderRecord, "percentHeld" | "value">, marketCap: number | undefined): number | undefined {
|
|
20
48
|
if (row.percentHeld != null) return row.percentHeld;
|
|
21
49
|
if (row.value == null || row.value < 0 || marketCap == null || marketCap <= 0) return undefined;
|
|
22
50
|
return row.value / marketCap;
|
|
23
51
|
}
|
|
24
52
|
|
|
53
|
+
/** A stake too small for two decimals reads "<0.01%", not a zero. */
|
|
25
54
|
export function formatHolderOwnershipPercent(value: number | undefined): string {
|
|
26
55
|
if (value == null) return "-";
|
|
27
56
|
const percent = Math.abs(value) <= 1 ? value * 100 : value;
|
|
57
|
+
if (percent > 0 && percent < 0.005) return "<0.01%";
|
|
28
58
|
return `${percent.toFixed(2)}%`;
|
|
29
59
|
}
|
|
30
60
|
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
formatMaybePercent,
|
|
13
13
|
formatMoneyCompact,
|
|
14
14
|
formatSignedCompact,
|
|
15
|
+
holderValueCurrency,
|
|
15
16
|
resolveHolderOwnershipPercent,
|
|
16
17
|
} from "./format";
|
|
17
18
|
import { buildRows, sortRows } from "./table-model";
|
|
@@ -19,6 +20,8 @@ import type { HolderData } from "../../../types/financials";
|
|
|
19
20
|
import type { HolderColumnId } from "./types";
|
|
20
21
|
import { SEC_FILINGS } from "../shared/report-freshness";
|
|
21
22
|
import {
|
|
23
|
+
holderShareBasisMarker,
|
|
24
|
+
holderShareBasisNote,
|
|
22
25
|
holderValueBasis,
|
|
23
26
|
holdersHeaderLine,
|
|
24
27
|
moneyColumnHeader,
|
|
@@ -28,6 +31,8 @@ import {
|
|
|
28
31
|
|
|
29
32
|
/** Columns a source fills only when it reports the quarter's change. */
|
|
30
33
|
const CHANGE_COLUMN_KEYS = new Set(["changeShares", "changePercent"]);
|
|
34
|
+
/** Shown only when some row is held as depositary receipts, as on a home line abroad. */
|
|
35
|
+
const BASIS_COLUMN_KEY = "shareBasis";
|
|
31
36
|
import type { BeneficialOwnersPayload } from "../../../api-client/beneficial-owners";
|
|
32
37
|
import { fetchBeneficialOwners, type BeneficialOwnersRequest } from "./beneficial-client";
|
|
33
38
|
import type { BeneficialColumnId } from "./beneficial-model";
|
|
@@ -60,6 +65,11 @@ const HOLDER_COLUMNS: HeadlessPaneColumn[] = [
|
|
|
60
65
|
align: "right",
|
|
61
66
|
format: (value) => value == null ? "-" : formatCompact(Number(value)),
|
|
62
67
|
},
|
|
68
|
+
{
|
|
69
|
+
key: BASIS_COLUMN_KEY,
|
|
70
|
+
header: "Basis",
|
|
71
|
+
format: (_value, row) => holderShareBasisMarker(row),
|
|
72
|
+
},
|
|
63
73
|
{
|
|
64
74
|
key: "changeShares",
|
|
65
75
|
header: "Chg",
|
|
@@ -238,6 +248,8 @@ export function createHoldersHeadless(
|
|
|
238
248
|
}
|
|
239
249
|
const { data, marketCap } = await dependencies.loadSnapshot(symbol, args, ctx);
|
|
240
250
|
const currency = data.currency ?? "USD";
|
|
251
|
+
// A London line's values can be dollars from the 13F filings; they are labelled and formatted as such.
|
|
252
|
+
const valueCurrency = holderValueCurrency(data, currency)!;
|
|
241
253
|
const sorted = sortRows(buildRows(data), {
|
|
242
254
|
columnId: SORT_COLUMNS[String(args.options.sort)] ?? "value",
|
|
243
255
|
direction: args.options.order === "asc" ? "asc" : "desc",
|
|
@@ -253,19 +265,23 @@ export function createHoldersHeadless(
|
|
|
253
265
|
changePercent: row.changePercent ?? null,
|
|
254
266
|
percentHeld: resolveHolderOwnershipPercent(row, marketCap) ?? null,
|
|
255
267
|
reportDate: row.reportDate ?? null,
|
|
256
|
-
currency,
|
|
268
|
+
currency: valueCurrency,
|
|
269
|
+
shareBasis: row.shareBasis ?? null,
|
|
257
270
|
}));
|
|
258
271
|
const total = data.summary?.institutionsCount ?? null;
|
|
259
272
|
const changeReported = rows.some((row) => row.changeShares != null || row.changePercent != null);
|
|
260
273
|
const reportDate = sharedReportDate(rows);
|
|
261
|
-
const valueBasis = holderValueBasis(reportDate);
|
|
274
|
+
const valueBasis = holderValueBasis(reportDate, data.valueBasis, valueCurrency);
|
|
262
275
|
const positionsBasis = rows.length > 0 ? nonUsHolderCaveat(data.exchange, currency) : null;
|
|
276
|
+
const shareBasisNote = holderShareBasisNote(data, rows);
|
|
277
|
+
const markedRows = rows.some((row) => holderShareBasisMarker(row));
|
|
263
278
|
// The title names the ticker and the closing line carries the as-of date.
|
|
264
279
|
const header = holdersHeaderLine({
|
|
265
280
|
name: data.name,
|
|
266
281
|
symbol: data.symbol && data.symbol !== symbol ? data.symbol : null,
|
|
267
282
|
exchange: data.exchange,
|
|
268
|
-
currency,
|
|
283
|
+
currency: valueCurrency,
|
|
284
|
+
listingCurrency: data.currency,
|
|
269
285
|
shown: rows.length,
|
|
270
286
|
reported: sorted.length,
|
|
271
287
|
total,
|
|
@@ -278,13 +294,18 @@ export function createHoldersHeadless(
|
|
|
278
294
|
// A change column that is empty on every row says nothing; the line above explains it once.
|
|
279
295
|
columns: HOLDER_COLUMNS
|
|
280
296
|
.filter((column) => changeReported || !CHANGE_COLUMN_KEYS.has(column.key))
|
|
281
|
-
.
|
|
297
|
+
.filter((column) => markedRows || column.key !== BASIS_COLUMN_KEY)
|
|
298
|
+
.map((column) => column.key === "value" ? { ...column, header: moneyColumnHeader(column.header, valueCurrency) } : column),
|
|
282
299
|
rows,
|
|
283
300
|
metadata: {
|
|
284
301
|
symbol: data.symbol || symbol,
|
|
285
302
|
name: data.name ?? null,
|
|
286
303
|
exchange: data.exchange ?? null,
|
|
287
304
|
currency,
|
|
305
|
+
valueCurrency,
|
|
306
|
+
...(data.isDepositaryReceipt != null ? { isDepositaryReceipt: data.isDepositaryReceipt } : {}),
|
|
307
|
+
...(data.shareBasis ? { shareBasis: data.shareBasis } : {}),
|
|
308
|
+
...(data.adrRatio != null ? { adrRatio: data.adrRatio } : {}),
|
|
288
309
|
asOf: data.asOf ?? null,
|
|
289
310
|
summary: data.summary ?? null,
|
|
290
311
|
shown: rows.length,
|
|
@@ -294,7 +315,7 @@ export function createHoldersHeadless(
|
|
|
294
315
|
valueBasis,
|
|
295
316
|
changeReported,
|
|
296
317
|
positionsBasis,
|
|
297
|
-
notices: [header, basis, positionsBasis].filter((line): line is string => !!line),
|
|
318
|
+
notices: [header, basis, positionsBasis, shareBasisNote].filter((line): line is string => !!line),
|
|
298
319
|
},
|
|
299
320
|
};
|
|
300
321
|
},
|