gloomberb 0.6.1 → 0.7.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/package.json +1 -1
- package/patches/@opentui%2Fcore@0.1.90.patch +48 -24
- package/src/app.tsx +9 -6
- package/src/cli/registry.test.ts +0 -1
- package/src/components/chart/chart-controller.ts +0 -1
- package/src/components/chart/chart-data.ts +1 -31
- package/src/components/chart/chart-renderer.ts +70 -0
- package/src/components/chart/chart-resolution.test.ts +0 -36
- package/src/components/chart/chart-resolution.ts +0 -27
- package/src/components/chart/chart.test.ts +17 -45
- package/src/components/chart/comparison-chart-data.test.ts +1 -5
- package/src/components/chart/comparison-chart-data.ts +1 -4
- package/src/components/chart/comparison-stock-chart.tsx +48 -27
- package/src/components/chart/static-chart-surface.tsx +28 -4
- package/src/components/chart/stock-chart.test.ts +0 -16
- package/src/components/chart/stock-chart.tsx +38 -21
- package/src/components/chart/time-axis-label.tsx +43 -0
- package/src/components/command-bar/command-bar.test.tsx +71 -8
- package/src/components/command-bar/command-bar.tsx +388 -713
- package/src/components/command-bar/confirm-body.tsx +51 -0
- package/src/components/command-bar/list-model.ts +77 -0
- package/src/components/command-bar/view-model.ts +0 -19
- package/src/components/command-bar/workflow-ops.ts +4 -0
- package/src/components/data-table-stack-view.test.tsx +39 -4
- package/src/components/data-table-stack-view.tsx +11 -104
- package/src/components/data-table-view.tsx +43 -79
- package/src/components/feed-data-table-stack-view.tsx +10 -17
- package/src/components/feed-stack-controller.ts +39 -0
- package/src/components/index.ts +5 -1
- package/src/components/layout/floating-pane.tsx +30 -2
- package/src/components/layout/header.tsx +10 -7
- package/src/components/layout/pane-cycle-order.test.ts +30 -0
- package/src/components/layout/pane-cycle-order.ts +32 -0
- package/src/components/layout/pane-footer.tsx +11 -4
- package/src/components/layout/pane-header.tsx +5 -5
- package/src/components/layout/shell.test.tsx +66 -22
- package/src/components/layout/shell.tsx +13 -6
- package/src/components/onboarding/onboarding-wizard.tsx +1 -2
- package/src/components/pane-settings-dialog.tsx +12 -4
- package/src/components/pane-template-wizard.test.tsx +3 -4
- package/src/components/speedometer-gauge.test.tsx +45 -0
- package/src/components/speedometer-gauge.tsx +558 -0
- package/src/components/table-view-shared.tsx +158 -0
- package/src/components/ticker-list-table-view.tsx +42 -72
- package/src/components/ui/confirm-dialog.tsx +66 -0
- package/src/components/ui/data-table.tsx +18 -10
- package/src/components/ui/index.ts +0 -1
- package/src/components/ui/page-stack-view.tsx +39 -10
- package/src/components/ui/table-layout.test.ts +16 -0
- package/src/components/ui/table-layout.ts +10 -3
- package/src/components/ui/ui.test.tsx +100 -20
- package/src/core/state/app-state.ts +6 -3
- package/src/market-data/coordinator.test.ts +1 -1
- package/src/market-data/coordinator.ts +0 -1
- package/src/news/aggregator.ts +0 -3
- package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +46 -16
- package/src/plugins/builtin/ai/providers.ts +0 -17
- package/src/plugins/builtin/ai/screener-pane.test.tsx +3 -4
- package/src/plugins/builtin/ai/screener-pane.tsx +0 -3
- package/src/plugins/builtin/ask-ai.test.tsx +89 -2
- package/src/plugins/builtin/broker-manager/index.test.tsx +2 -2
- package/src/plugins/builtin/broker-manager/index.tsx +19 -36
- package/src/plugins/builtin/chat-controller.test.ts +105 -0
- package/src/plugins/builtin/chat-controller.ts +75 -6
- package/src/plugins/builtin/chat.test.tsx +6 -6
- package/src/plugins/builtin/chat.tsx +29 -20
- package/src/plugins/builtin/cloud-tweets.tsx +0 -1
- package/src/plugins/builtin/comparison-chart.test.tsx +53 -2
- package/src/plugins/builtin/comparison-chart.tsx +11 -5
- package/src/plugins/builtin/correlation/index.tsx +1 -6
- package/src/plugins/builtin/debug.tsx +1 -3
- package/src/plugins/builtin/econ/index.tsx +1 -2
- package/src/plugins/builtin/fear-greed/fear-greed-data.test.ts +72 -0
- package/src/plugins/builtin/fear-greed/fear-greed-data.ts +366 -0
- package/src/plugins/builtin/fear-greed/index.tsx +478 -0
- package/src/plugins/builtin/fx-matrix/index.tsx +2 -2
- package/src/plugins/builtin/help.test.tsx +2 -2
- package/src/plugins/builtin/help.tsx +2 -2
- package/src/plugins/builtin/holders/index.tsx +10 -15
- package/src/plugins/builtin/insider/index.tsx +22 -89
- package/src/plugins/builtin/market-movers/index.tsx +1 -1
- package/src/plugins/builtin/news-wire/index.tsx +41 -2
- package/src/plugins/builtin/news-wire/news-table.tsx +11 -11
- package/src/plugins/builtin/news.tsx +4 -16
- package/src/plugins/builtin/notes.tsx +18 -55
- package/src/plugins/builtin/options.test.tsx +58 -2
- package/src/plugins/builtin/options.tsx +108 -49
- package/src/plugins/builtin/plugin-groups.ts +2 -0
- package/src/plugins/builtin/portfolio-list/cli/commands.ts +1 -1
- package/src/plugins/builtin/research/index.tsx +5 -21
- package/src/plugins/builtin/sec-filing-display.tsx +84 -0
- package/src/plugins/builtin/sec.tsx +13 -66
- package/src/plugins/builtin/sectors/index.tsx +1 -1
- package/src/plugins/builtin/ticker-detail/chart-tab.tsx +0 -1
- package/src/plugins/builtin/ticker-detail/index.tsx +0 -1
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +0 -1
- package/src/plugins/builtin/ticker-detail-chart-tab-switch.test.tsx +3 -4
- package/src/plugins/builtin/world-indices/index.tsx +1 -1
- package/src/plugins/builtin/yield-curve/index.tsx +1 -1
- package/src/plugins/ibkr/broker-adapter.ts +1 -3
- package/src/plugins/ibkr/gateway-service-native.ts +1 -5
- package/src/plugins/ibkr/gateway-service.ts +1 -1
- package/src/plugins/ibkr/index.test.tsx +0 -2
- package/src/plugins/ibkr/trade-tab.test.tsx +3 -4
- package/src/plugins/pane-manager.ts +0 -13
- package/src/plugins/plugin-runtime.test.tsx +6 -6
- package/src/plugins/plugin-runtime.tsx +13 -8
- package/src/plugins/prediction-markets/cli.ts +0 -1
- package/src/plugins/prediction-markets/controller-data.ts +0 -2
- package/src/plugins/prediction-markets/pane.test.tsx +3 -11
- package/src/plugins/prediction-markets/test-helpers.tsx +1 -1
- package/src/plugins/registry.ts +10 -39
- package/src/react/input.ts +1 -0
- package/src/renderers/electrobun/view/data-table.tsx +28 -6
- package/src/renderers/electrobun/view/desktop-controls.tsx +34 -20
- package/src/renderers/electrobun/view/dialog-host.tsx +11 -1
- package/src/renderers/electrobun/view/input-host.tsx +74 -10
- package/src/renderers/electrobun/view/key-event.ts +1 -1
- package/src/renderers/electrobun/view/styles.css +331 -0
- package/src/renderers/opentui/chart-surface.tsx +179 -0
- package/src/renderers/opentui/start.tsx +48 -30
- package/src/renderers/opentui/test-utils.tsx +17 -0
- package/src/renderers/opentui/ui-host.tsx +5 -2
- package/src/sources/provider-router.ts +0 -4
- package/src/state/app-context.test.ts +45 -0
- package/src/state/use-inline-tickers.test.tsx +73 -0
- package/src/state/use-inline-tickers.ts +7 -1
- package/src/test-support/plugin-runtime.ts +3 -2
- package/src/theme/colors.ts +12 -3
- package/src/theme/themes.test.ts +23 -1
- package/src/theme/themes.ts +115 -0
- package/src/types/broker.ts +1 -1
- package/src/types/data-provider.ts +0 -1
- package/src/types/plugin.ts +0 -2
- package/src/ui/host.tsx +1 -0
- package/src/utils/back-navigation.ts +24 -1
- package/src/version.ts +1 -1
- package/src/plugins/builtin/news-wire/feed-pane.tsx +0 -21
- package/src/plugins/builtin/news-wire/top-pane.tsx +0 -21
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { Box, Text } from "../../ui";
|
|
2
2
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
3
3
|
import { TextAttributes } from "../../ui";
|
|
4
|
-
import type {
|
|
4
|
+
import type { GloomPlugin } from "../../types/plugin";
|
|
5
5
|
import type { OptionContract, OptionsChain } from "../../types/financials";
|
|
6
6
|
import { usePaneTicker } from "../../state/app-context";
|
|
7
|
-
import { blendHex, colors } from "../../theme/colors";
|
|
7
|
+
import { blendHex, colors, hoverBg } from "../../theme/colors";
|
|
8
|
+
import { blendForContrast, contrastRatio, higherContrast } from "../../theme/color-utils";
|
|
8
9
|
import { formatCompact, formatNumber } from "../../utils/format";
|
|
9
10
|
import { formatMarketPrice } from "../../utils/market-format";
|
|
10
11
|
import { formatExpDate, resolveOptionsTarget } from "../../utils/options";
|
|
@@ -12,7 +13,7 @@ import { useOptionsQuery, useResolvedEntryValue } from "../../market-data/hooks"
|
|
|
12
13
|
import { DataTableView, Spinner, Tabs, usePaneFooter, type DataTableCell, type DataTableColumn, type DataTableKeyEvent } from "../../components";
|
|
13
14
|
import { createTickerSurfacePaneTemplate } from "./ticker-surface";
|
|
14
15
|
|
|
15
|
-
type OptionColumnId =
|
|
16
|
+
export type OptionColumnId =
|
|
16
17
|
| "callLast"
|
|
17
18
|
| "callBid"
|
|
18
19
|
| "callAsk"
|
|
@@ -36,31 +37,40 @@ interface OptionTableRow {
|
|
|
36
37
|
isPositionStrike: boolean;
|
|
37
38
|
}
|
|
38
39
|
|
|
39
|
-
type OptionsViewProps =
|
|
40
|
-
|
|
40
|
+
type OptionsViewProps = {
|
|
41
|
+
width: number;
|
|
42
|
+
height: number;
|
|
43
|
+
focused: boolean;
|
|
44
|
+
onCapture?: (capturing: boolean) => void;
|
|
41
45
|
};
|
|
42
46
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
{ id: "
|
|
58
|
-
{ id: "
|
|
59
|
-
{ id: "
|
|
60
|
-
{ id: "
|
|
61
|
-
{ id: "
|
|
62
|
-
{ id: "
|
|
63
|
-
{ id: "
|
|
47
|
+
type OptionColorRole = "call" | "put" | "price" | "activity" | "iv" | "strike";
|
|
48
|
+
|
|
49
|
+
const OPTION_TEXT_MIN_CONTRAST = 4.5;
|
|
50
|
+
|
|
51
|
+
const OPTION_BASE_COLORS: Record<OptionColorRole, string> = {
|
|
52
|
+
call: "#5ed69a",
|
|
53
|
+
put: "#ff9c7a",
|
|
54
|
+
price: "#dfc05b",
|
|
55
|
+
activity: "#35a7d6",
|
|
56
|
+
iv: "#8bd878",
|
|
57
|
+
strike: "#8fb7ff",
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const OPTION_COLUMNS: Array<Omit<OptionColumn, "headerColor">> = [
|
|
61
|
+
{ id: "callOpenInterest", label: "C OI", width: 6, align: "right" },
|
|
62
|
+
{ id: "callVolume", label: "C VOL", width: 6, align: "right" },
|
|
63
|
+
{ id: "callLast", label: "C LAST", width: 7, align: "right" },
|
|
64
|
+
{ id: "callIv", label: "C IV", width: 6, align: "right" },
|
|
65
|
+
{ id: "callBid", label: "C BID", width: 7, align: "right" },
|
|
66
|
+
{ id: "callAsk", label: "C ASK", width: 7, align: "right" },
|
|
67
|
+
{ id: "strike", label: "STRIKE", width: 9, align: "right" },
|
|
68
|
+
{ id: "putBid", label: "P BID", width: 7, align: "right" },
|
|
69
|
+
{ id: "putAsk", label: "P ASK", width: 7, align: "right" },
|
|
70
|
+
{ id: "putIv", label: "P IV", width: 6, align: "right" },
|
|
71
|
+
{ id: "putLast", label: "P LAST", width: 7, align: "right" },
|
|
72
|
+
{ id: "putVolume", label: "P VOL", width: 6, align: "right" },
|
|
73
|
+
{ id: "putOpenInterest", label: "P OI", width: 6, align: "right" },
|
|
64
74
|
];
|
|
65
75
|
|
|
66
76
|
export function OptionsView({ width, height, focused, onCapture = () => {} }: OptionsViewProps) {
|
|
@@ -161,6 +171,10 @@ export function OptionsView({ width, height, focused, onCapture = () => {} }: Op
|
|
|
161
171
|
const selectedStrike = strikes[strikeIdx];
|
|
162
172
|
const selectedCall = selectedStrike != null ? callsByStrike.get(selectedStrike) : undefined;
|
|
163
173
|
const selectedPut = selectedStrike != null ? putsByStrike.get(selectedStrike) : undefined;
|
|
174
|
+
const optionColumns = OPTION_COLUMNS.map((column) => ({
|
|
175
|
+
...column,
|
|
176
|
+
headerColor: optionColumnColor(column.id, colors.panel),
|
|
177
|
+
}));
|
|
164
178
|
|
|
165
179
|
usePaneFooter("options", () => {
|
|
166
180
|
const info = [
|
|
@@ -303,7 +317,7 @@ export function OptionsView({ width, height, focused, onCapture = () => {} }: Op
|
|
|
303
317
|
focused={focused}
|
|
304
318
|
selectedIndex={strikeIdx}
|
|
305
319
|
onRootKeyDown={handleTableKeyDown}
|
|
306
|
-
columns={
|
|
320
|
+
columns={optionColumns}
|
|
307
321
|
items={rows}
|
|
308
322
|
sortColumnId={null}
|
|
309
323
|
sortDirection="asc"
|
|
@@ -327,14 +341,6 @@ export function OptionsView({ width, height, focused, onCapture = () => {} }: Op
|
|
|
327
341
|
);
|
|
328
342
|
}
|
|
329
343
|
|
|
330
|
-
export function OptionsTab(props: DetailTabProps) {
|
|
331
|
-
return <OptionsView {...props} />;
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
export function OptionsPane({ focused, width, height }: PaneProps) {
|
|
335
|
-
return <OptionsView focused={focused} width={width} height={height} />;
|
|
336
|
-
}
|
|
337
|
-
|
|
338
344
|
function buildStrikeList(chain: OptionsChain): number[] {
|
|
339
345
|
const set = new Set<number>();
|
|
340
346
|
for (const c of chain.calls) set.add(c.strike);
|
|
@@ -378,12 +384,60 @@ function optionContractForColumn(row: OptionTableRow, columnId: OptionColumnId):
|
|
|
378
384
|
return columnId.startsWith("call") ? row.call : row.put;
|
|
379
385
|
}
|
|
380
386
|
|
|
381
|
-
function optionColumnColor(columnId: OptionColumnId): string {
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
+
export function optionColumnColor(columnId: OptionColumnId, surface = colors.bg): string {
|
|
388
|
+
return optionRoleColor(optionColumnRole(columnId), surface);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function optionColumnRole(columnId: OptionColumnId): OptionColorRole {
|
|
392
|
+
if (columnId === "strike") return "strike";
|
|
393
|
+
if (columnId.endsWith("Iv")) return "iv";
|
|
394
|
+
if (columnId.endsWith("Volume") || columnId.endsWith("OpenInterest")) return "activity";
|
|
395
|
+
if (columnId.endsWith("Bid") || columnId.endsWith("Ask")) return "price";
|
|
396
|
+
return columnId.startsWith("call") ? "call" : "put";
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function optionRoleThemeColor(role: OptionColorRole): string {
|
|
400
|
+
switch (role) {
|
|
401
|
+
case "call":
|
|
402
|
+
case "iv":
|
|
403
|
+
return colors.positive;
|
|
404
|
+
case "put":
|
|
405
|
+
return colors.negative;
|
|
406
|
+
case "price":
|
|
407
|
+
return colors.warning;
|
|
408
|
+
case "activity":
|
|
409
|
+
case "strike":
|
|
410
|
+
return colors.borderFocused;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function mostReadableColor(surface: string, candidates: readonly string[]): string {
|
|
415
|
+
return candidates.reduce((best, candidate) =>
|
|
416
|
+
contrastRatio(candidate, surface) > contrastRatio(best, surface) ? candidate : best,
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
export function optionRoleColor(role: OptionColorRole, surface: string): string {
|
|
421
|
+
const preferred = OPTION_BASE_COLORS[role];
|
|
422
|
+
const themeColor = optionRoleThemeColor(role);
|
|
423
|
+
const fallback = mostReadableColor(surface, [
|
|
424
|
+
preferred,
|
|
425
|
+
themeColor,
|
|
426
|
+
colors.text,
|
|
427
|
+
colors.textBright,
|
|
428
|
+
higherContrast("#ffffff", "#000000", surface),
|
|
429
|
+
]);
|
|
430
|
+
return blendForContrast(preferred, surface, fallback, OPTION_TEXT_MIN_CONTRAST);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
export function optionMutedColor(surface: string): string {
|
|
434
|
+
const fallback = mostReadableColor(surface, [
|
|
435
|
+
colors.textDim,
|
|
436
|
+
colors.text,
|
|
437
|
+
colors.textBright,
|
|
438
|
+
higherContrast("#ffffff", "#000000", surface),
|
|
439
|
+
]);
|
|
440
|
+
return blendForContrast(colors.textDim, surface, fallback, OPTION_TEXT_MIN_CONTRAST);
|
|
387
441
|
}
|
|
388
442
|
|
|
389
443
|
function optionMoneynessBackground(
|
|
@@ -443,23 +497,28 @@ function renderOptionCell(
|
|
|
443
497
|
rowState: { selected: boolean; hovered: boolean },
|
|
444
498
|
): DataTableCell {
|
|
445
499
|
const selectedColor = rowState.selected ? colors.selectedText : undefined;
|
|
500
|
+
const rowSurface = rowState.selected ? colors.selected : rowState.hovered ? hoverBg() : colors.bg;
|
|
446
501
|
|
|
447
502
|
if (column.id === "strike") {
|
|
503
|
+
const backgroundColor = rowState.selected || rowState.hovered
|
|
504
|
+
? undefined
|
|
505
|
+
: blendHex(colors.bg, row.isPositionStrike ? colors.borderFocused : colors.header, row.isPositionStrike ? 0.18 : 0.1);
|
|
506
|
+
const surface = backgroundColor ?? rowSurface;
|
|
448
507
|
return {
|
|
449
508
|
text: formatStrikeLabel(row.strike),
|
|
450
|
-
color: selectedColor ??
|
|
451
|
-
backgroundColor
|
|
452
|
-
? undefined
|
|
453
|
-
: blendHex(colors.bg, row.isPositionStrike ? colors.borderFocused : colors.header, row.isPositionStrike ? 0.18 : 0.1),
|
|
509
|
+
color: selectedColor ?? optionRoleColor("strike", surface),
|
|
510
|
+
backgroundColor,
|
|
454
511
|
attributes: rowState.selected || row.isPositionStrike ? TextAttributes.BOLD : TextAttributes.NONE,
|
|
455
512
|
};
|
|
456
513
|
}
|
|
457
514
|
|
|
458
515
|
const contract = optionContractForColumn(row, column.id);
|
|
516
|
+
const backgroundColor = optionMoneynessBackground(row, contract, column.id, rowState);
|
|
517
|
+
const surface = backgroundColor ?? rowSurface;
|
|
459
518
|
return {
|
|
460
519
|
text: formatOptionContractCell(contract, column),
|
|
461
|
-
color: selectedColor ?? (contract ?
|
|
462
|
-
backgroundColor
|
|
520
|
+
color: selectedColor ?? (contract ? optionRoleColor(optionColumnRole(column.id), surface) : optionMutedColor(surface)),
|
|
521
|
+
backgroundColor,
|
|
463
522
|
};
|
|
464
523
|
}
|
|
465
524
|
|
|
@@ -475,7 +534,7 @@ export const optionsPlugin: GloomPlugin = {
|
|
|
475
534
|
id: "options",
|
|
476
535
|
name: "Options",
|
|
477
536
|
icon: "O",
|
|
478
|
-
component:
|
|
537
|
+
component: OptionsView,
|
|
479
538
|
defaultPosition: "right",
|
|
480
539
|
defaultMode: "floating",
|
|
481
540
|
defaultFloatingSize: { width: 112, height: 28 },
|
|
@@ -498,7 +557,7 @@ export const optionsPlugin: GloomPlugin = {
|
|
|
498
557
|
id: "options",
|
|
499
558
|
name: "Options",
|
|
500
559
|
order: 35, // after Chart (30), before News (40)
|
|
501
|
-
component:
|
|
560
|
+
component: OptionsView,
|
|
502
561
|
isVisible: ({ hasOptionsChain }) => hasOptionsChain,
|
|
503
562
|
});
|
|
504
563
|
},
|
|
@@ -4,6 +4,7 @@ import { correlationPlugin } from "./correlation";
|
|
|
4
4
|
import { earningsPlugin } from "./earnings";
|
|
5
5
|
import { registerEconCalendarFeature } from "./econ";
|
|
6
6
|
import { fxMatrixPlugin } from "./fx-matrix";
|
|
7
|
+
import { fearGreedPlugin } from "./fear-greed";
|
|
7
8
|
import { holdersPlugin } from "./holders";
|
|
8
9
|
import { insiderPlugin } from "./insider";
|
|
9
10
|
import { marketMoversPlugin } from "./market-movers";
|
|
@@ -74,6 +75,7 @@ export const marketOverviewPlugin = createPluginGroup({
|
|
|
74
75
|
correlationPlugin,
|
|
75
76
|
worldIndicesPlugin,
|
|
76
77
|
marketMoversPlugin,
|
|
78
|
+
fearGreedPlugin,
|
|
77
79
|
sectorsPlugin,
|
|
78
80
|
fxMatrixPlugin,
|
|
79
81
|
],
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
renderTable,
|
|
17
17
|
} from "../../../../utils/cli-output";
|
|
18
18
|
import { exchangeShortName } from "../../../../utils/market-status";
|
|
19
|
-
import { formatCompact
|
|
19
|
+
import { formatCompact } from "../../../../utils/format";
|
|
20
20
|
import { formatMarketCostWithCurrency, formatMarketPriceWithCurrency, formatMarketQuantity } from "../../../../utils/market-format";
|
|
21
21
|
import type { AppConfig } from "../../../../types/config";
|
|
22
22
|
import type { CliCommandContext, CliCommandDef } from "../../../../types/plugin";
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
type DataTableKeyEvent,
|
|
9
9
|
} from "../../../components";
|
|
10
10
|
import type { AnalystResearchData, CorporateActionsData, TickerFinancials } from "../../../types/financials";
|
|
11
|
-
import type {
|
|
11
|
+
import type { GloomPlugin, PaneProps } from "../../../types/plugin";
|
|
12
12
|
import { usePaneInstance, usePaneTicker } from "../../../state/app-context";
|
|
13
13
|
import { blendHex, colors, priceColor } from "../../../theme/colors";
|
|
14
14
|
import { formatCompact, formatCurrency, formatNumber, formatPercent, formatPercentRaw } from "../../../utils/format";
|
|
@@ -609,22 +609,6 @@ function RelativeValuationPane({ focused, width, height }: PaneProps) {
|
|
|
609
609
|
);
|
|
610
610
|
}
|
|
611
611
|
|
|
612
|
-
function ResearchPane({ focused, width, height }: PaneProps) {
|
|
613
|
-
return <AnalystResearchView focused={focused} width={width} height={height} />;
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
function EventsPane({ focused, width, height }: PaneProps) {
|
|
617
|
-
return <CorporateActionsView focused={focused} width={width} height={height} />;
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
function AnalystTab({ focused, width, height }: DetailTabProps) {
|
|
621
|
-
return <AnalystResearchView focused={focused} width={width} height={height} />;
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
function EventsTab({ focused, width, height }: DetailTabProps) {
|
|
625
|
-
return <CorporateActionsView focused={focused} width={width} height={height} />;
|
|
626
|
-
}
|
|
627
|
-
|
|
628
612
|
export const researchPlugin: GloomPlugin = {
|
|
629
613
|
id: "research",
|
|
630
614
|
name: "Research",
|
|
@@ -637,14 +621,14 @@ export const researchPlugin: GloomPlugin = {
|
|
|
637
621
|
id: "analyst-research",
|
|
638
622
|
name: "Analyst",
|
|
639
623
|
order: 32,
|
|
640
|
-
component:
|
|
624
|
+
component: AnalystResearchView,
|
|
641
625
|
isVisible: ({ ticker }) => !!ticker,
|
|
642
626
|
});
|
|
643
627
|
ctx.registerDetailTab({
|
|
644
628
|
id: "corporate-actions",
|
|
645
629
|
name: "Events",
|
|
646
630
|
order: 34,
|
|
647
|
-
component:
|
|
631
|
+
component: CorporateActionsView,
|
|
648
632
|
isVisible: ({ ticker }) => !!ticker,
|
|
649
633
|
});
|
|
650
634
|
},
|
|
@@ -654,7 +638,7 @@ export const researchPlugin: GloomPlugin = {
|
|
|
654
638
|
id: "analyst-research",
|
|
655
639
|
name: "Analyst Research",
|
|
656
640
|
icon: "A",
|
|
657
|
-
component:
|
|
641
|
+
component: AnalystResearchView,
|
|
658
642
|
defaultPosition: "right",
|
|
659
643
|
defaultMode: "floating",
|
|
660
644
|
defaultFloatingSize: { width: 90, height: 28 },
|
|
@@ -663,7 +647,7 @@ export const researchPlugin: GloomPlugin = {
|
|
|
663
647
|
id: "corporate-actions",
|
|
664
648
|
name: "Corporate Actions",
|
|
665
649
|
icon: "E",
|
|
666
|
-
component:
|
|
650
|
+
component: CorporateActionsView,
|
|
667
651
|
defaultPosition: "right",
|
|
668
652
|
defaultMode: "floating",
|
|
669
653
|
defaultFloatingSize: { width: 82, height: 24 },
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { Box, Text } from "../../ui";
|
|
2
|
+
import { colors } from "../../theme/colors";
|
|
3
|
+
import { formatCompact, formatCurrency } from "../../utils/format";
|
|
4
|
+
import { transactionTypeLabel, type InsiderTransaction } from "./insider/insider-data";
|
|
5
|
+
|
|
6
|
+
const MONTH_NAMES = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] as const;
|
|
7
|
+
|
|
8
|
+
export function formatFilingShortDate(value: Date | string | number): string {
|
|
9
|
+
const date = value instanceof Date ? value : new Date(value);
|
|
10
|
+
if (isNaN(date.getTime())) return "—";
|
|
11
|
+
return `${MONTH_NAMES[date.getMonth()]} ${String(date.getDate()).padStart(2, " ")} ${date.getFullYear()}`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function formatFilingMetaDate(value: Date): string {
|
|
15
|
+
return value.toLocaleString("en-US", {
|
|
16
|
+
month: "short",
|
|
17
|
+
day: "numeric",
|
|
18
|
+
year: "numeric",
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function formatFilingFormLabel(form: string, fallback = "FORM 4"): string {
|
|
23
|
+
const value = form.trim();
|
|
24
|
+
return value ? `FORM ${value}` : fallback;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function renderFilingNotice(message: string, width: number) {
|
|
28
|
+
const lines = wrapNoticeLines(message, width - 4);
|
|
29
|
+
return (
|
|
30
|
+
<Box flexDirection="column" paddingX={1} paddingY={1}>
|
|
31
|
+
{lines.map((line, index) => (
|
|
32
|
+
<Box key={index} height={1}>
|
|
33
|
+
<Text fg={colors.textDim}>{line}</Text>
|
|
34
|
+
</Box>
|
|
35
|
+
))}
|
|
36
|
+
</Box>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function buildInsiderTransactionTitle(transaction: InsiderTransaction): string {
|
|
41
|
+
const type = transactionTypeLabel(transaction.transactionType);
|
|
42
|
+
const price = transaction.pricePerShare != null
|
|
43
|
+
? ` @ ${formatCurrency(transaction.pricePerShare)}`
|
|
44
|
+
: "";
|
|
45
|
+
const value = transaction.totalValue != null
|
|
46
|
+
? ` | ${formatCurrency(transaction.totalValue)}`
|
|
47
|
+
: "";
|
|
48
|
+
return `${type} ${formatCompact(transaction.shares)} shares${price}${value}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function buildInsiderTransactionDetailBody(transaction: InsiderTransaction): string {
|
|
52
|
+
const lines = [
|
|
53
|
+
`Transaction: ${transactionTypeLabel(transaction.transactionType)}`,
|
|
54
|
+
`Date: ${formatFilingShortDate(transaction.filingDate)}`,
|
|
55
|
+
`Shares: ${formatCompact(transaction.shares)}`,
|
|
56
|
+
`Price/Share: ${transaction.pricePerShare != null ? formatCurrency(transaction.pricePerShare) : "—"}`,
|
|
57
|
+
`Total Value: ${transaction.totalValue != null ? formatCurrency(transaction.totalValue) : "—"}`,
|
|
58
|
+
`Shares Owned After: ${transaction.sharesOwned != null ? formatCompact(transaction.sharesOwned) : "—"}`,
|
|
59
|
+
];
|
|
60
|
+
return lines.join("\n");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function wrapNoticeLines(text: string, width: number): string[] {
|
|
64
|
+
const maxWidth = Math.max(width, 12);
|
|
65
|
+
const words = text.trim().split(/\s+/);
|
|
66
|
+
const lines: string[] = [];
|
|
67
|
+
let current = "";
|
|
68
|
+
|
|
69
|
+
for (const word of words) {
|
|
70
|
+
if (!current) {
|
|
71
|
+
current = word;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (current.length + 1 + word.length <= maxWidth) {
|
|
75
|
+
current = `${current} ${word}`;
|
|
76
|
+
} else {
|
|
77
|
+
lines.push(current);
|
|
78
|
+
current = word;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (current) lines.push(current);
|
|
83
|
+
return lines;
|
|
84
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Text } from "../../ui";
|
|
2
2
|
import { useEffect, useMemo, useRef, useState } from "react";
|
|
3
|
-
import type {
|
|
3
|
+
import type { GloomPlugin } from "../../types/plugin";
|
|
4
4
|
import type { SecFilingItem } from "../../types/data-provider";
|
|
5
5
|
import { useResolvedEntryValue, useSecFilingContent, useSecFilingsQuery } from "../../market-data/hooks";
|
|
6
6
|
import { instrumentFromTicker } from "../../market-data/request-types";
|
|
@@ -13,6 +13,10 @@ import { getSharedMarketDataCoordinator } from "../../market-data/coordinator";
|
|
|
13
13
|
import { parseForm4Xml, transactionTypeLabel } from "./insider/insider-data";
|
|
14
14
|
import { formatCompact, formatCurrency } from "../../utils/format";
|
|
15
15
|
import { createTickerSurfacePaneTemplate } from "./ticker-surface";
|
|
16
|
+
import {
|
|
17
|
+
formatFilingMetaDate,
|
|
18
|
+
renderFilingNotice,
|
|
19
|
+
} from "./sec-filing-display";
|
|
16
20
|
|
|
17
21
|
const SEC_FILING_LIMIT = 50;
|
|
18
22
|
const OWNERSHIP_FORMS = new Set(["3", "4", "5"]);
|
|
@@ -60,48 +64,8 @@ function getFilingDisplayTitle(filing: SecFilingItem): string {
|
|
|
60
64
|
return description ? `${formLabel} | ${description}` : formLabel;
|
|
61
65
|
}
|
|
62
66
|
|
|
63
|
-
function wrapMessageLines(text: string, width: number): string[] {
|
|
64
|
-
const maxWidth = Math.max(width, 12);
|
|
65
|
-
const words = text.trim().split(/\s+/);
|
|
66
|
-
const lines: string[] = [];
|
|
67
|
-
let current = "";
|
|
68
|
-
|
|
69
|
-
for (const word of words) {
|
|
70
|
-
if (!current) {
|
|
71
|
-
current = word;
|
|
72
|
-
continue;
|
|
73
|
-
}
|
|
74
|
-
if ((current.length + 1 + word.length) <= maxWidth) {
|
|
75
|
-
current = `${current} ${word}`;
|
|
76
|
-
} else {
|
|
77
|
-
lines.push(current);
|
|
78
|
-
current = word;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
if (current) lines.push(current);
|
|
83
|
-
return lines;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function renderNotice(message: string, width: number) {
|
|
87
|
-
const lines = wrapMessageLines(message, width - 4);
|
|
88
|
-
return (
|
|
89
|
-
<Box flexDirection="column" paddingX={1} paddingY={1}>
|
|
90
|
-
{lines.map((line, index) => (
|
|
91
|
-
<Box key={index} height={1}>
|
|
92
|
-
<Text fg={colors.textDim}>{line}</Text>
|
|
93
|
-
</Box>
|
|
94
|
-
))}
|
|
95
|
-
</Box>
|
|
96
|
-
);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
67
|
function formatFiledAt(filing: SecFilingItem): string {
|
|
100
|
-
return filing.filingDate
|
|
101
|
-
month: "short",
|
|
102
|
-
day: "numeric",
|
|
103
|
-
year: "numeric",
|
|
104
|
-
});
|
|
68
|
+
return formatFilingMetaDate(filing.filingDate);
|
|
105
69
|
}
|
|
106
70
|
|
|
107
71
|
function buildDetailBody(filing: SecFilingItem): string {
|
|
@@ -142,15 +106,6 @@ function buildForm4Detail(content: string | null, filing: SecFilingItem): string
|
|
|
142
106
|
return lines.join("\n");
|
|
143
107
|
}
|
|
144
108
|
|
|
145
|
-
function getFormCategory(form: string): "annual" | "quarterly" | "current" | "ownership" | "other" {
|
|
146
|
-
const f = form.trim().toUpperCase();
|
|
147
|
-
if (f === "10-K" || f === "10-K/A" || f === "20-F") return "annual";
|
|
148
|
-
if (f === "10-Q" || f === "10-Q/A") return "quarterly";
|
|
149
|
-
if (f.startsWith("8-K")) return "current";
|
|
150
|
-
if (OWNERSHIP_FORMS.has(f) || f === "SC 13G" || f === "SC 13D" || f === "SC 13G/A" || f === "SC 13D/A") return "ownership";
|
|
151
|
-
return "other";
|
|
152
|
-
}
|
|
153
|
-
|
|
154
109
|
function getFormDescription(form: string): string {
|
|
155
110
|
const f = form.trim().toUpperCase();
|
|
156
111
|
switch (f) {
|
|
@@ -226,7 +181,7 @@ function toFeedItems(
|
|
|
226
181
|
});
|
|
227
182
|
}
|
|
228
183
|
|
|
229
|
-
|
|
184
|
+
function SecView({ width, height, focused }: { width: number; height: number; focused: boolean }) {
|
|
230
185
|
const { ticker } = usePaneTicker();
|
|
231
186
|
const selectionKey = `selectedIdx:${ticker?.metadata.ticker ?? "none"}`;
|
|
232
187
|
const [selectedIdx, setSelectedIdx] = usePluginPaneState<number>(selectionKey, 0);
|
|
@@ -323,10 +278,10 @@ export function SecView({ width, height, focused }: Pick<DetailTabProps, "width"
|
|
|
323
278
|
});
|
|
324
279
|
|
|
325
280
|
if (!ticker) return <Text fg={colors.textDim}>Select a ticker to view SEC filings.</Text>;
|
|
326
|
-
if (!eligibleTicker) return
|
|
281
|
+
if (!eligibleTicker) return renderFilingNotice("SEC filings are only shown for US equities.", width);
|
|
327
282
|
if (loading && filings.length === 0) return <Spinner label="Loading SEC filings..." />;
|
|
328
|
-
if (error) return
|
|
329
|
-
if (filings.length === 0) return
|
|
283
|
+
if (error) return renderFilingNotice(`Error: ${error}`, width);
|
|
284
|
+
if (filings.length === 0) return renderFilingNotice(`No recent SEC filings for ${ticker.metadata.ticker}.`, width);
|
|
330
285
|
|
|
331
286
|
return (
|
|
332
287
|
<FeedDataTableStackView
|
|
@@ -344,14 +299,6 @@ export function SecView({ width, height, focused }: Pick<DetailTabProps, "width"
|
|
|
344
299
|
);
|
|
345
300
|
}
|
|
346
301
|
|
|
347
|
-
function SecTab(props: DetailTabProps) {
|
|
348
|
-
return <SecView {...props} />;
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
function SecPane({ focused, width, height }: PaneProps) {
|
|
352
|
-
return <SecView focused={focused} width={width} height={height} />;
|
|
353
|
-
}
|
|
354
|
-
|
|
355
302
|
export const secPlugin: GloomPlugin = {
|
|
356
303
|
id: "sec",
|
|
357
304
|
name: "SEC",
|
|
@@ -364,7 +311,7 @@ export const secPlugin: GloomPlugin = {
|
|
|
364
311
|
id: "sec",
|
|
365
312
|
name: "SEC",
|
|
366
313
|
icon: "S",
|
|
367
|
-
component:
|
|
314
|
+
component: SecView,
|
|
368
315
|
defaultPosition: "right",
|
|
369
316
|
defaultMode: "floating",
|
|
370
317
|
defaultFloatingSize: { width: 100, height: 32 },
|
|
@@ -388,7 +335,7 @@ export const secPlugin: GloomPlugin = {
|
|
|
388
335
|
id: "sec",
|
|
389
336
|
name: "SEC",
|
|
390
337
|
order: 45,
|
|
391
|
-
component:
|
|
338
|
+
component: SecView,
|
|
392
339
|
isVisible: ({ ticker }) => isUsEquityTicker(ticker),
|
|
393
340
|
});
|
|
394
341
|
},
|
|
@@ -109,7 +109,7 @@ function nextSortPreference(current: SectorSortPreference, columnId: string): Se
|
|
|
109
109
|
return DEFAULT_SORT_PREFERENCE;
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
-
|
|
112
|
+
function SectorPerformancePane({ focused, width, height }: PaneProps) {
|
|
113
113
|
const dataProvider = useAssetData();
|
|
114
114
|
const { navigateTicker } = usePluginTickerActions();
|
|
115
115
|
const [rows, setRows] = useState<SectorRow[]>(
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { GloomPlugin } from "../../../types/plugin";
|
|
2
2
|
import { normalizeTickerInput } from "../../../utils/ticker-search";
|
|
3
3
|
import { TickerDetailPane } from "./pane";
|
|
4
|
-
import { FinancialsTab } from "./financials-tab";
|
|
5
4
|
import { QuoteMonitorPane } from "./quote-monitor";
|
|
6
5
|
import {
|
|
7
6
|
buildQuoteMonitorSettingsDef,
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
2
|
import { createTestRenderer } from "@opentui/core/testing";
|
|
3
|
-
import { createOpenTuiTestRoot as createRoot } from "../../renderers/opentui/test-utils";
|
|
4
|
-
import { DialogProvider } from "@opentui-ui/dialog/react";
|
|
3
|
+
import { TestDialogProvider, createOpenTuiTestRoot as createRoot } from "../../renderers/opentui/test-utils";
|
|
5
4
|
import { act, useReducer, type ReactElement } from "react";
|
|
6
5
|
import {
|
|
7
6
|
AppContext,
|
|
@@ -111,7 +110,7 @@ function DetailHarness({
|
|
|
111
110
|
harnessDispatch = dispatch;
|
|
112
111
|
|
|
113
112
|
return (
|
|
114
|
-
<
|
|
113
|
+
<TestDialogProvider>
|
|
115
114
|
<AppContext value={{ state, dispatch }}>
|
|
116
115
|
<PaneInstanceProvider paneId={TEST_PANE_ID}>
|
|
117
116
|
<PluginRenderProvider pluginId={tickerDetailPlugin.id} runtime={runtime}>
|
|
@@ -125,7 +124,7 @@ function DetailHarness({
|
|
|
125
124
|
</PluginRenderProvider>
|
|
126
125
|
</PaneInstanceProvider>
|
|
127
126
|
</AppContext>
|
|
128
|
-
</
|
|
127
|
+
</TestDialogProvider>
|
|
129
128
|
);
|
|
130
129
|
}
|
|
131
130
|
|
|
@@ -148,7 +148,7 @@ function nextSortPreference(
|
|
|
148
148
|
return DEFAULT_SORT_PREFERENCE;
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
-
|
|
151
|
+
function WorldIndicesPane({ focused, width, height }: PaneProps) {
|
|
152
152
|
const dataProvider = useAssetData();
|
|
153
153
|
const { pinTicker } = usePluginTickerActions();
|
|
154
154
|
const [quotes, setQuotes] = useState<QuoteMap>(new Map());
|
|
@@ -30,7 +30,7 @@ function spreadBp(points: YieldPoint[]): number | null {
|
|
|
30
30
|
return Math.round((y10 - y2) * 100);
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
function YieldCurvePane({ focused, width, height }: PaneProps) {
|
|
34
34
|
const [points, setPoints] = useState<YieldPoint[]>([]);
|
|
35
35
|
const [loading, setLoading] = useState(false);
|
|
36
36
|
const [error, setError] = useState<string | null>(null);
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import type { BrokerAdapter, BrokerPosition } from "../../types/broker";
|
|
2
|
-
import type { BrokerInstanceConfig } from "../../types/config";
|
|
3
2
|
import {
|
|
4
3
|
buildIbkrConfigFromValues,
|
|
5
|
-
getGatewayConfig,
|
|
6
4
|
IBKR_CONFIG_FIELDS,
|
|
7
5
|
isFlexConfigured,
|
|
8
6
|
isGatewayConfigured,
|
|
@@ -82,7 +80,7 @@ export const ibkrBroker: BrokerAdapter = {
|
|
|
82
80
|
return [{
|
|
83
81
|
id: "ibkr-console",
|
|
84
82
|
label: "IBKR Console",
|
|
85
|
-
|
|
83
|
+
paneId: "ibkr-trading",
|
|
86
84
|
disabled: normalized.connectionMode !== "gateway",
|
|
87
85
|
disabledReason: "IBKR Console is available for Gateway / TWS profiles.",
|
|
88
86
|
}];
|