gloomberb 0.4.1 → 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 +168 -65
- 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 +292 -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/header.tsx +2 -1
- package/src/components/layout/shell.test.tsx +186 -27
- package/src/components/layout/shell.tsx +5 -4
- 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 +54 -1
- package/src/components/toggle-list.tsx +15 -2
- package/src/components/ui/data-table.tsx +60 -3
- 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 +62 -18
- package/src/plugins/builtin/chat-controller.ts +23 -12
- package/src/plugins/builtin/chat.test.tsx +715 -4
- package/src/plugins/builtin/chat.tsx +415 -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 +24 -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/index.test.tsx +1 -1
- package/src/plugins/ibkr/index.tsx +1 -1
- 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 +8 -0
- package/src/plugins/prediction-markets/detail/shared.tsx +7 -6
- package/src/plugins/prediction-markets/plugin.test.tsx +48 -1
- package/src/plugins/prediction-markets/services/fetch.ts +13 -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 +2 -0
- package/src/plugins/registry.ts +22 -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/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,212 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import type { PersistedResourceValue } from "../../../types/persistence";
|
|
3
|
+
import type { PluginPersistence } from "../../../types/plugin";
|
|
4
|
+
import {
|
|
5
|
+
attachYieldCurvePersistence,
|
|
6
|
+
isInverted,
|
|
7
|
+
loadYieldCurve,
|
|
8
|
+
parseYieldPoints,
|
|
9
|
+
resetYieldCurvePersistence,
|
|
10
|
+
TREASURY_MATURITIES,
|
|
11
|
+
YIELD_CURVE_CACHE_POLICY,
|
|
12
|
+
type YieldPoint,
|
|
13
|
+
} from "./treasury-data";
|
|
14
|
+
|
|
15
|
+
class MemoryPluginPersistence implements PluginPersistence {
|
|
16
|
+
private readonly resources = new Map<string, PersistedResourceValue<unknown>>();
|
|
17
|
+
private readonly state = new Map<string, { schemaVersion: number; value: unknown }>();
|
|
18
|
+
|
|
19
|
+
getState<T = unknown>(key: string, options?: { schemaVersion?: number }): T | null {
|
|
20
|
+
const record = this.state.get(key);
|
|
21
|
+
if (!record) return null;
|
|
22
|
+
if (options?.schemaVersion != null && record.schemaVersion !== options.schemaVersion) return null;
|
|
23
|
+
return record.value as T;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
setState(key: string, value: unknown, options?: { schemaVersion?: number }): void {
|
|
27
|
+
this.state.set(key, { schemaVersion: options?.schemaVersion ?? 1, value });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
deleteState(key: string): void {
|
|
31
|
+
this.state.delete(key);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
getResource<T = unknown>(
|
|
35
|
+
kind: string,
|
|
36
|
+
key: string,
|
|
37
|
+
options?: { sourceKey?: string; schemaVersion?: number; allowExpired?: boolean },
|
|
38
|
+
): PersistedResourceValue<T> | null {
|
|
39
|
+
const record = this.resources.get(this.resourceKey(kind, key, options?.sourceKey));
|
|
40
|
+
if (!record) return null;
|
|
41
|
+
if (options?.schemaVersion != null && record.schemaVersion !== options.schemaVersion) return null;
|
|
42
|
+
const next = this.withFreshness(record);
|
|
43
|
+
if (!options?.allowExpired && next.expired) return null;
|
|
44
|
+
return next as PersistedResourceValue<T>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
setResource<T = unknown>(
|
|
48
|
+
kind: string,
|
|
49
|
+
key: string,
|
|
50
|
+
value: T,
|
|
51
|
+
options: {
|
|
52
|
+
cachePolicy: { staleMs: number; expireMs: number };
|
|
53
|
+
sourceKey?: string;
|
|
54
|
+
schemaVersion?: number;
|
|
55
|
+
provenance?: unknown;
|
|
56
|
+
},
|
|
57
|
+
): PersistedResourceValue<T> {
|
|
58
|
+
const now = Date.now();
|
|
59
|
+
const record: PersistedResourceValue<T> = {
|
|
60
|
+
value,
|
|
61
|
+
fetchedAt: now,
|
|
62
|
+
staleAt: now + options.cachePolicy.staleMs,
|
|
63
|
+
expiresAt: now + options.cachePolicy.expireMs,
|
|
64
|
+
sourceKey: options.sourceKey ?? "",
|
|
65
|
+
schemaVersion: options.schemaVersion ?? 1,
|
|
66
|
+
provenance: options.provenance,
|
|
67
|
+
stale: false,
|
|
68
|
+
expired: false,
|
|
69
|
+
};
|
|
70
|
+
this.resources.set(this.resourceKey(kind, key, options.sourceKey), record);
|
|
71
|
+
return record;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
deleteResource(kind: string, key: string, options?: { sourceKey?: string }): void {
|
|
75
|
+
this.resources.delete(this.resourceKey(kind, key, options?.sourceKey));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
seedResource<T>(
|
|
79
|
+
kind: string,
|
|
80
|
+
key: string,
|
|
81
|
+
value: T,
|
|
82
|
+
options: { sourceKey?: string; stale?: boolean; expired?: boolean } = {},
|
|
83
|
+
): void {
|
|
84
|
+
const now = Date.now();
|
|
85
|
+
const record: PersistedResourceValue<T> = {
|
|
86
|
+
value,
|
|
87
|
+
fetchedAt: now - 60_000,
|
|
88
|
+
staleAt: options.stale ? now - 1 : now + YIELD_CURVE_CACHE_POLICY.staleMs,
|
|
89
|
+
expiresAt: options.expired ? now - 1 : now + YIELD_CURVE_CACHE_POLICY.expireMs,
|
|
90
|
+
sourceKey: options.sourceKey ?? "",
|
|
91
|
+
schemaVersion: 1,
|
|
92
|
+
stale: !!options.stale,
|
|
93
|
+
expired: !!options.expired,
|
|
94
|
+
};
|
|
95
|
+
this.resources.set(this.resourceKey(kind, key, options.sourceKey), record);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private resourceKey(kind: string, key: string, sourceKey = ""): string {
|
|
99
|
+
return `${kind}:${key}:${sourceKey}`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
private withFreshness<T>(record: PersistedResourceValue<T>): PersistedResourceValue<T> {
|
|
103
|
+
const now = Date.now();
|
|
104
|
+
return {
|
|
105
|
+
...record,
|
|
106
|
+
stale: now >= record.staleAt,
|
|
107
|
+
expired: now >= record.expiresAt,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const SAMPLE_POINTS: YieldPoint[] = [
|
|
113
|
+
{ maturity: "2Y", maturityYears: 2, yield: 4.5 },
|
|
114
|
+
{ maturity: "10Y", maturityYears: 10, yield: 4.3 },
|
|
115
|
+
];
|
|
116
|
+
|
|
117
|
+
afterEach(() => {
|
|
118
|
+
resetYieldCurvePersistence();
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
describe("parseYieldPoints", () => {
|
|
122
|
+
test("filters out null yields", () => {
|
|
123
|
+
const points: YieldPoint[] = [
|
|
124
|
+
{ maturity: "2Y", maturityYears: 2, yield: 4.5 },
|
|
125
|
+
{ maturity: "5Y", maturityYears: 5, yield: null },
|
|
126
|
+
{ maturity: "10Y", maturityYears: 10, yield: 4.3 },
|
|
127
|
+
];
|
|
128
|
+
const result = parseYieldPoints(points);
|
|
129
|
+
expect(result).toHaveLength(2);
|
|
130
|
+
expect(result[0]!.maturity).toBe("2Y");
|
|
131
|
+
expect(result[1]!.maturity).toBe("10Y");
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("returns empty for all nulls", () => {
|
|
135
|
+
expect(parseYieldPoints([{ maturity: "2Y", maturityYears: 2, yield: null }])).toEqual([]);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
describe("isInverted", () => {
|
|
140
|
+
test("detects inverted curve (2Y > 10Y)", () => {
|
|
141
|
+
const points: YieldPoint[] = [
|
|
142
|
+
{ maturity: "2Y", maturityYears: 2, yield: 5.0 },
|
|
143
|
+
{ maturity: "10Y", maturityYears: 10, yield: 4.3 },
|
|
144
|
+
];
|
|
145
|
+
expect(isInverted(points)).toBe(true);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("normal curve is not inverted", () => {
|
|
149
|
+
const points: YieldPoint[] = [
|
|
150
|
+
{ maturity: "2Y", maturityYears: 2, yield: 4.0 },
|
|
151
|
+
{ maturity: "10Y", maturityYears: 10, yield: 4.5 },
|
|
152
|
+
];
|
|
153
|
+
expect(isInverted(points)).toBe(false);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("returns false when data is missing", () => {
|
|
157
|
+
expect(isInverted([])).toBe(false);
|
|
158
|
+
expect(isInverted([{ maturity: "2Y", maturityYears: 2, yield: 4.0 }])).toBe(false);
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
describe("TREASURY_MATURITIES", () => {
|
|
163
|
+
test("has 10 maturities in ascending order", () => {
|
|
164
|
+
expect(TREASURY_MATURITIES).toHaveLength(10);
|
|
165
|
+
for (let i = 1; i < TREASURY_MATURITIES.length; i++) {
|
|
166
|
+
expect(TREASURY_MATURITIES[i]!.years).toBeGreaterThan(TREASURY_MATURITIES[i - 1]!.years);
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
describe("loadYieldCurve cache", () => {
|
|
172
|
+
test("uses a fresh plugin resource cache without fetching", async () => {
|
|
173
|
+
const persistence = new MemoryPluginPersistence();
|
|
174
|
+
attachYieldCurvePersistence(persistence);
|
|
175
|
+
persistence.setResource("treasury-yield-curve", "latest", SAMPLE_POINTS, {
|
|
176
|
+
sourceKey: "fred",
|
|
177
|
+
cachePolicy: YIELD_CURVE_CACHE_POLICY,
|
|
178
|
+
});
|
|
179
|
+
let fetchCalls = 0;
|
|
180
|
+
|
|
181
|
+
const points = await loadYieldCurve("key", {
|
|
182
|
+
fetcher: async () => {
|
|
183
|
+
fetchCalls += 1;
|
|
184
|
+
throw new Error("unexpected fetch");
|
|
185
|
+
},
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
expect(points).toEqual(SAMPLE_POINTS);
|
|
189
|
+
expect(fetchCalls).toBe(0);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test("falls back to stale plugin resource cache when refresh fails", async () => {
|
|
193
|
+
const persistence = new MemoryPluginPersistence();
|
|
194
|
+
attachYieldCurvePersistence(persistence);
|
|
195
|
+
persistence.seedResource("treasury-yield-curve", "latest", SAMPLE_POINTS, {
|
|
196
|
+
sourceKey: "fred",
|
|
197
|
+
stale: true,
|
|
198
|
+
});
|
|
199
|
+
let fetchCalls = 0;
|
|
200
|
+
|
|
201
|
+
const points = await loadYieldCurve("key", {
|
|
202
|
+
force: true,
|
|
203
|
+
fetcher: async () => {
|
|
204
|
+
fetchCalls += 1;
|
|
205
|
+
throw new Error("offline");
|
|
206
|
+
},
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
expect(points).toEqual(SAMPLE_POINTS);
|
|
210
|
+
expect(fetchCalls).toBe(1);
|
|
211
|
+
});
|
|
212
|
+
});
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { fetchFredObservations } from "../econ/fred-client";
|
|
2
|
+
import type { PersistedResourceValue } from "../../../types/persistence";
|
|
3
|
+
import type { PluginPersistence } from "../../../types/plugin";
|
|
4
|
+
|
|
5
|
+
export interface YieldPoint {
|
|
6
|
+
maturity: string; // "1M", "3M", "6M", "1Y", "2Y", "5Y", "7Y", "10Y", "20Y", "30Y"
|
|
7
|
+
maturityYears: number; // 0.083, 0.25, 0.5, 1, 2, 5, 7, 10, 20, 30
|
|
8
|
+
yield: number | null; // percent, e.g., 4.29
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const TREASURY_MATURITIES: Array<{ maturity: string; years: number; seriesId: string }> = [
|
|
12
|
+
{ maturity: "1M", years: 1/12, seriesId: "DGS1MO" },
|
|
13
|
+
{ maturity: "3M", years: 0.25, seriesId: "DGS3MO" },
|
|
14
|
+
{ maturity: "6M", years: 0.5, seriesId: "DGS6MO" },
|
|
15
|
+
{ maturity: "1Y", years: 1, seriesId: "DGS1" },
|
|
16
|
+
{ maturity: "2Y", years: 2, seriesId: "DGS2" },
|
|
17
|
+
{ maturity: "5Y", years: 5, seriesId: "DGS5" },
|
|
18
|
+
{ maturity: "7Y", years: 7, seriesId: "DGS7" },
|
|
19
|
+
{ maturity: "10Y", years: 10, seriesId: "DGS10" },
|
|
20
|
+
{ maturity: "20Y", years: 20, seriesId: "DGS20" },
|
|
21
|
+
{ maturity: "30Y", years: 30, seriesId: "DGS30" },
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
const CACHE_KIND = "treasury-yield-curve";
|
|
25
|
+
const CACHE_KEY = "latest";
|
|
26
|
+
const CACHE_SOURCE = "fred";
|
|
27
|
+
const CACHE_SCHEMA_VERSION = 1;
|
|
28
|
+
|
|
29
|
+
export const YIELD_CURVE_CACHE_POLICY = {
|
|
30
|
+
staleMs: 15 * 60 * 1000,
|
|
31
|
+
expireMs: 7 * 24 * 60 * 60 * 1000,
|
|
32
|
+
} as const;
|
|
33
|
+
|
|
34
|
+
let yieldCurvePersistence: PluginPersistence | null = null;
|
|
35
|
+
|
|
36
|
+
export function attachYieldCurvePersistence(persistence: PluginPersistence): void {
|
|
37
|
+
yieldCurvePersistence = persistence;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function resetYieldCurvePersistence(): void {
|
|
41
|
+
yieldCurvePersistence = null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function readYieldCurveCache(options?: {
|
|
45
|
+
allowExpired?: boolean;
|
|
46
|
+
}): PersistedResourceValue<YieldPoint[]> | null {
|
|
47
|
+
return yieldCurvePersistence?.getResource<YieldPoint[]>(CACHE_KIND, CACHE_KEY, {
|
|
48
|
+
sourceKey: CACHE_SOURCE,
|
|
49
|
+
schemaVersion: CACHE_SCHEMA_VERSION,
|
|
50
|
+
allowExpired: options?.allowExpired,
|
|
51
|
+
}) ?? null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function writeYieldCurveCache(points: YieldPoint[]): void {
|
|
55
|
+
yieldCurvePersistence?.setResource(CACHE_KIND, CACHE_KEY, points, {
|
|
56
|
+
sourceKey: CACHE_SOURCE,
|
|
57
|
+
schemaVersion: CACHE_SCHEMA_VERSION,
|
|
58
|
+
cachePolicy: YIELD_CURVE_CACHE_POLICY,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function fetchYieldCurve(apiKey: string): Promise<YieldPoint[]> {
|
|
63
|
+
const results = await Promise.allSettled(
|
|
64
|
+
TREASURY_MATURITIES.map(async ({ maturity, years, seriesId }) => {
|
|
65
|
+
const obs = await fetchFredObservations(apiKey, seriesId, { limit: 1, sortOrder: "desc" });
|
|
66
|
+
const value = obs[0]?.value ?? null;
|
|
67
|
+
return { maturity, maturityYears: years, yield: value };
|
|
68
|
+
}),
|
|
69
|
+
);
|
|
70
|
+
return results.map((r, i) => {
|
|
71
|
+
if (r.status === "fulfilled") return r.value;
|
|
72
|
+
return { maturity: TREASURY_MATURITIES[i]!.maturity, maturityYears: TREASURY_MATURITIES[i]!.years, yield: null };
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function loadYieldCurve(
|
|
77
|
+
apiKey: string,
|
|
78
|
+
options?: {
|
|
79
|
+
force?: boolean;
|
|
80
|
+
fetcher?: (apiKey: string) => Promise<YieldPoint[]>;
|
|
81
|
+
},
|
|
82
|
+
): Promise<YieldPoint[]> {
|
|
83
|
+
const freshCache = readYieldCurveCache();
|
|
84
|
+
if (!options?.force && freshCache && !freshCache.stale) {
|
|
85
|
+
return freshCache.value;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
const points = await (options?.fetcher ?? fetchYieldCurve)(apiKey);
|
|
90
|
+
writeYieldCurveCache(points);
|
|
91
|
+
return points;
|
|
92
|
+
} catch (error) {
|
|
93
|
+
const staleCache = freshCache ?? readYieldCurveCache({ allowExpired: true });
|
|
94
|
+
if (staleCache) return staleCache.value;
|
|
95
|
+
throw error;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function parseYieldPoints(points: YieldPoint[]): YieldPoint[] {
|
|
100
|
+
return points.filter((p) => p.yield !== null);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function isInverted(points: YieldPoint[]): boolean {
|
|
104
|
+
const y2 = points.find((p) => p.maturity === "2Y")?.yield;
|
|
105
|
+
const y10 = points.find((p) => p.maturity === "10Y")?.yield;
|
|
106
|
+
return y2 != null && y10 != null && y2 > y10;
|
|
107
|
+
}
|
package/src/plugins/catalog.ts
CHANGED
|
@@ -12,10 +12,21 @@ import { aiPlugin } from "./builtin/ai";
|
|
|
12
12
|
import { gloomberbCloudPlugin } from "./builtin/chat";
|
|
13
13
|
import { helpPlugin } from "./builtin/help";
|
|
14
14
|
import { comparisonChartPlugin } from "./builtin/comparison-chart";
|
|
15
|
+
import { econCalendarPlugin } from "./builtin/econ";
|
|
16
|
+
import { worldIndicesPlugin } from "./builtin/world-indices";
|
|
17
|
+
import { marketMoversPlugin } from "./builtin/market-movers";
|
|
15
18
|
import { debugPlugin } from "./builtin/debug";
|
|
16
19
|
import { layoutManagerPlugin } from "./builtin/layout-manager";
|
|
17
20
|
import { yahooPlugin } from "./builtin/yahoo";
|
|
18
21
|
import { predictionMarketsPlugin } from "./prediction-markets";
|
|
22
|
+
import { correlationPlugin } from "./builtin/correlation";
|
|
23
|
+
import { analyticsPlugin } from "./builtin/analytics";
|
|
24
|
+
import { insiderPlugin } from "./builtin/insider";
|
|
25
|
+
import { alertsPlugin } from "./builtin/alerts";
|
|
26
|
+
import { fxMatrixPlugin } from "./builtin/fx-matrix";
|
|
27
|
+
import { yieldCurvePlugin } from "./builtin/yield-curve";
|
|
28
|
+
import { sectorsPlugin } from "./builtin/sectors";
|
|
29
|
+
import { earningsPlugin } from "./builtin/earnings";
|
|
19
30
|
|
|
20
31
|
export interface PluginCatalogEntry {
|
|
21
32
|
plugin: GloomPlugin;
|
|
@@ -40,6 +51,17 @@ export const builtinPlugins: GloomPlugin[] = [
|
|
|
40
51
|
helpPlugin,
|
|
41
52
|
comparisonChartPlugin,
|
|
42
53
|
predictionMarketsPlugin,
|
|
54
|
+
correlationPlugin,
|
|
55
|
+
analyticsPlugin,
|
|
56
|
+
insiderPlugin,
|
|
57
|
+
econCalendarPlugin,
|
|
58
|
+
worldIndicesPlugin,
|
|
59
|
+
marketMoversPlugin,
|
|
60
|
+
alertsPlugin,
|
|
61
|
+
fxMatrixPlugin,
|
|
62
|
+
yieldCurvePlugin,
|
|
63
|
+
sectorsPlugin,
|
|
64
|
+
earningsPlugin,
|
|
43
65
|
debugPlugin,
|
|
44
66
|
];
|
|
45
67
|
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { AppNotificationRequest } from "../types/plugin";
|
|
2
|
+
|
|
3
|
+
export function notifyGridlockComplete(
|
|
4
|
+
notify: (notification: AppNotificationRequest) => void,
|
|
5
|
+
onRevert: () => void,
|
|
6
|
+
): void {
|
|
7
|
+
notify({
|
|
8
|
+
body: "Retiled all panes",
|
|
9
|
+
type: "success",
|
|
10
|
+
action: {
|
|
11
|
+
label: "Revert",
|
|
12
|
+
onClick: onRevert,
|
|
13
|
+
},
|
|
14
|
+
});
|
|
15
|
+
}
|
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
// Bun Snapshot v1, https://bun.sh/docs/test/snapshots
|
|
2
2
|
|
|
3
3
|
exports[`renders the compact trade tab layout 1`] = `
|
|
4
|
-
" Trade AMD · Advanced Micro Devices, Inc.
|
|
5
|
-
$199.
|
|
6
|
-
|
|
7
|
-
Broker Account Net Liq
|
|
8
|
-
Paper Gateway DU123456 —
|
|
4
|
+
" Trade AMD · Advanced Micro Devices, Inc. Broker Account Net Liq
|
|
5
|
+
$199.8 -3.97 · Bid 199.7 · Ask 199.9 · Spd 0.2 Paper Gateway DU123456 —
|
|
9
6
|
|
|
10
7
|
|
|
11
8
|
Next Submit order Ticket Standby Refresh r
|
|
@@ -31,5 +28,8 @@ exports[`renders the compact trade tab layout 1`] = `
|
|
|
31
28
|
│ Preview p Submit Order Enter │
|
|
32
29
|
╰────────────────────────────────────────────────────────────────────────────────────╯
|
|
33
30
|
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
|
|
34
34
|
"
|
|
35
35
|
`;
|
|
@@ -111,7 +111,7 @@ function setupIbkrPlugin(config: AppConfig) {
|
|
|
111
111
|
emit: () => {},
|
|
112
112
|
showWidget: () => {},
|
|
113
113
|
hideWidget: () => {},
|
|
114
|
-
|
|
114
|
+
notify: ({ body }: { body: string }) => { toasts.push(body); },
|
|
115
115
|
} satisfies GloomPluginContext;
|
|
116
116
|
|
|
117
117
|
void ibkrPlugin.setup?.(ctx);
|
|
@@ -20,7 +20,7 @@ let lastSelectedTickerSymbol: string | null = null;
|
|
|
20
20
|
|
|
21
21
|
function ensureIbkrTradingProfile(ctx: GloomPluginContext): boolean {
|
|
22
22
|
if (hasIbkrTradingProfiles(ctx.getConfig())) return true;
|
|
23
|
-
ctx.
|
|
23
|
+
ctx.notify({ body: "Connect a Gateway / TWS IBKR profile first.", type: "info" });
|
|
24
24
|
return false;
|
|
25
25
|
}
|
|
26
26
|
|
|
@@ -8,7 +8,8 @@ import { useAppState, usePaneCollection, usePaneInstanceId, usePaneTicker } from
|
|
|
8
8
|
import { colors, hoverBg } from "../../theme/colors";
|
|
9
9
|
import type { DetailTabProps } from "../../types/plugin";
|
|
10
10
|
import type { BrokerOrderRequest, BrokerOrderType } from "../../types/trading";
|
|
11
|
-
import { formatCurrency
|
|
11
|
+
import { formatCurrency } from "../../utils/format";
|
|
12
|
+
import { formatMarketPrice, formatMarketQuantity } from "../../utils/market-format";
|
|
12
13
|
import { getBrokerInstance } from "../../utils/broker-instances";
|
|
13
14
|
import { isGatewayConfigured } from "./config";
|
|
14
15
|
import { ChoiceDialog, InputDialog } from "./dialogs";
|
|
@@ -401,6 +402,7 @@ export function TradeTab({ focused, width, onCapture }: DetailTabProps) {
|
|
|
401
402
|
label={label}
|
|
402
403
|
currentValue={currentValue}
|
|
403
404
|
quote={financials?.quote}
|
|
405
|
+
assetCategory={ticker?.metadata.assetCategory}
|
|
404
406
|
/>
|
|
405
407
|
),
|
|
406
408
|
});
|
|
@@ -818,7 +820,7 @@ export function TradeTab({ focused, width, onCapture }: DetailTabProps) {
|
|
|
818
820
|
)}
|
|
819
821
|
</box>
|
|
820
822
|
<box height={1}>
|
|
821
|
-
<text fg={colors.textMuted}>{formatQuoteSummary(financials?.quote)}</text>
|
|
823
|
+
<text fg={colors.textMuted}>{formatQuoteSummary(financials?.quote, { assetCategory: ticker.metadata.assetCategory })}</text>
|
|
822
824
|
</box>
|
|
823
825
|
</box>
|
|
824
826
|
|
|
@@ -946,7 +948,11 @@ export function TradeTab({ focused, width, onCapture }: DetailTabProps) {
|
|
|
946
948
|
{renderFieldPill({
|
|
947
949
|
id: "quantity",
|
|
948
950
|
label: "Qty [q]",
|
|
949
|
-
value:
|
|
951
|
+
value: formatMarketQuantity(ticketState.draft.quantity, {
|
|
952
|
+
assetCategory: ticker.metadata.assetCategory,
|
|
953
|
+
contractSecType: activeContract.secType,
|
|
954
|
+
maxWidth: fieldTextWidth,
|
|
955
|
+
}),
|
|
950
956
|
widthOverride: orderFieldWidth,
|
|
951
957
|
onPress: () => {
|
|
952
958
|
editNumericField("Quantity", ticketState.draft.quantity, (value) => {
|
|
@@ -957,7 +963,13 @@ export function TradeTab({ focused, width, onCapture }: DetailTabProps) {
|
|
|
957
963
|
{showLimit && renderFieldPill({
|
|
958
964
|
id: "limitPrice",
|
|
959
965
|
label: "Limit [l]",
|
|
960
|
-
value: ticketState.draft.limitPrice != null
|
|
966
|
+
value: ticketState.draft.limitPrice != null
|
|
967
|
+
? formatMarketPrice(ticketState.draft.limitPrice, {
|
|
968
|
+
assetCategory: ticker.metadata.assetCategory,
|
|
969
|
+
contractSecType: activeContract.secType,
|
|
970
|
+
maxWidth: fieldTextWidth,
|
|
971
|
+
})
|
|
972
|
+
: "—",
|
|
961
973
|
widthOverride: orderFieldWidth,
|
|
962
974
|
onPress: () => {
|
|
963
975
|
editPriceField("Limit Price", ticketState.draft.limitPrice, (value) => {
|
|
@@ -968,7 +980,13 @@ export function TradeTab({ focused, width, onCapture }: DetailTabProps) {
|
|
|
968
980
|
{showStop && renderFieldPill({
|
|
969
981
|
id: "stopPrice",
|
|
970
982
|
label: "Stop [x]",
|
|
971
|
-
value: ticketState.draft.stopPrice != null
|
|
983
|
+
value: ticketState.draft.stopPrice != null
|
|
984
|
+
? formatMarketPrice(ticketState.draft.stopPrice, {
|
|
985
|
+
assetCategory: ticker.metadata.assetCategory,
|
|
986
|
+
contractSecType: activeContract.secType,
|
|
987
|
+
maxWidth: fieldTextWidth,
|
|
988
|
+
})
|
|
989
|
+
: "—",
|
|
972
990
|
widthOverride: orderFieldWidth,
|
|
973
991
|
onPress: () => {
|
|
974
992
|
editPriceField("Stop Price", ticketState.draft.stopPrice, (value) => {
|
|
@@ -125,7 +125,7 @@ describe("trade-utils", () => {
|
|
|
125
125
|
bid: 190.2,
|
|
126
126
|
ask: 190.3,
|
|
127
127
|
lastUpdated: Date.now(),
|
|
128
|
-
})).toContain("Spd 0.
|
|
128
|
+
})).toContain("Spd 0.1");
|
|
129
129
|
expect(formatPreviewSummary(null)).toContain("Preview required before submit");
|
|
130
130
|
expect(formatPreviewSummary({
|
|
131
131
|
initMarginBefore: 12000,
|
|
@@ -5,6 +5,7 @@ import type { TickerRecord } from "../../types/ticker";
|
|
|
5
5
|
import type { BrokerAccount, BrokerOrderPreview, BrokerOrderType } from "../../types/trading";
|
|
6
6
|
import { colors } from "../../theme/colors";
|
|
7
7
|
import { formatCompact, formatCurrency } from "../../utils/format";
|
|
8
|
+
import { formatMarketPrice, formatMarketPriceWithCurrency, formatSignedMarketPrice, type AssetDisplayContext } from "../../utils/market-format";
|
|
8
9
|
import { getConfiguredIbkrGatewayInstances } from "./instance-selection";
|
|
9
10
|
|
|
10
11
|
export type TradeTone = "neutral" | "accent" | "positive" | "negative";
|
|
@@ -71,13 +72,13 @@ export function formatContractLabel(contract: BrokerContractRef): string {
|
|
|
71
72
|
return `${base}${suffix}`;
|
|
72
73
|
}
|
|
73
74
|
|
|
74
|
-
export function formatQuoteSummary(quote?: Quote): string {
|
|
75
|
+
export function formatQuoteSummary(quote?: Quote, formatContext: AssetDisplayContext = {}): string {
|
|
75
76
|
if (!quote) return "No broker quote loaded";
|
|
76
|
-
const change =
|
|
77
|
-
const parts = [`${
|
|
78
|
-
if (quote.bid != null) parts.push(`Bid ${quote.bid
|
|
79
|
-
if (quote.ask != null) parts.push(`Ask ${quote.ask
|
|
80
|
-
if (quote.bid != null && quote.ask != null) parts.push(`Spd ${(quote.ask - quote.bid)
|
|
77
|
+
const change = formatSignedMarketPrice(quote.change, formatContext);
|
|
78
|
+
const parts = [`${formatMarketPriceWithCurrency(quote.price, quote.currency, formatContext)} ${change}`];
|
|
79
|
+
if (quote.bid != null) parts.push(`Bid ${formatMarketPrice(quote.bid, formatContext)}`);
|
|
80
|
+
if (quote.ask != null) parts.push(`Ask ${formatMarketPrice(quote.ask, formatContext)}`);
|
|
81
|
+
if (quote.bid != null && quote.ask != null) parts.push(`Spd ${formatMarketPrice(quote.ask - quote.bid, formatContext)}`);
|
|
81
82
|
return parts.join(" · ");
|
|
82
83
|
}
|
|
83
84
|
|
|
@@ -9,6 +9,7 @@ import { colors, priceColor } from "../../theme/colors";
|
|
|
9
9
|
import type { PaneProps } from "../../types/plugin";
|
|
10
10
|
import type { Quote } from "../../types/financials";
|
|
11
11
|
import { formatCurrency, padTo } from "../../utils/format";
|
|
12
|
+
import { formatMarketPrice, formatMarketQuantity } from "../../utils/market-format";
|
|
12
13
|
import { getBrokerInstance } from "../../utils/broker-instances";
|
|
13
14
|
import { getSharedRegistry } from "../registry";
|
|
14
15
|
import { isGatewayConfigured } from "./config";
|
|
@@ -354,12 +355,12 @@ export function TradingPane({ focused, width, height }: PaneProps) {
|
|
|
354
355
|
const selected = index === tradeState.selectedOpenOrderIndex;
|
|
355
356
|
const orderSymbol = order.contract.symbol;
|
|
356
357
|
const orderQuote = getOrderQuote(orderSymbol);
|
|
357
|
-
const bidStr = orderQuote?.bid != null ? orderQuote.bid.
|
|
358
|
-
const askStr = orderQuote?.ask != null ? orderQuote.ask.
|
|
358
|
+
const bidStr = orderQuote?.bid != null ? formatMarketPrice(orderQuote.bid, { contractSecType: order.contract.secType, maxWidth: 6 }) : "---";
|
|
359
|
+
const askStr = orderQuote?.ask != null ? formatMarketPrice(orderQuote.ask, { contractSecType: order.contract.secType, maxWidth: 6 }) : "---";
|
|
359
360
|
const orderPrice = order.limitPrice != null
|
|
360
|
-
? order.limitPrice.
|
|
361
|
+
? formatMarketPrice(order.limitPrice, { contractSecType: order.contract.secType, maxWidth: 9 })
|
|
361
362
|
: order.stopPrice != null
|
|
362
|
-
? order.stopPrice.
|
|
363
|
+
? formatMarketPrice(order.stopPrice, { contractSecType: order.contract.secType, maxWidth: 9 })
|
|
363
364
|
: "MKT";
|
|
364
365
|
return (
|
|
365
366
|
<box
|
|
@@ -379,7 +380,7 @@ export function TradingPane({ focused, width, height }: PaneProps) {
|
|
|
379
380
|
{padTo(order.action, 5)}
|
|
380
381
|
{padTo(order.contract.localSymbol || order.contract.symbol, 14)}
|
|
381
382
|
{padTo(order.status, 10)}
|
|
382
|
-
{padTo(
|
|
383
|
+
{padTo(formatMarketQuantity(order.remaining, { contractSecType: order.contract.secType, maxWidth: 5 }), 5, "right")}
|
|
383
384
|
{" "}
|
|
384
385
|
{padTo(orderPrice, 9)}
|
|
385
386
|
{padTo(`B:${bidStr}`, 10)}
|
|
@@ -415,9 +416,9 @@ export function TradingPane({ focused, width, height }: PaneProps) {
|
|
|
415
416
|
<text fg={priceColor(execution.side.toUpperCase() === "BOT" ? 1 : -1)}>
|
|
416
417
|
{padTo(execution.side, 5)}
|
|
417
418
|
{padTo(execution.contract.localSymbol || execution.contract.symbol, 18)}
|
|
418
|
-
{padTo(
|
|
419
|
+
{padTo(formatMarketQuantity(execution.shares, { contractSecType: execution.contract.secType, maxWidth: 6 }), 6, "right")}
|
|
419
420
|
{" "}
|
|
420
|
-
{execution.price.
|
|
421
|
+
{formatMarketPrice(execution.price, { contractSecType: execution.contract.secType })}
|
|
421
422
|
</text>
|
|
422
423
|
</box>
|
|
423
424
|
))
|
|
@@ -59,6 +59,8 @@ describe("plugin runtime hooks", () => {
|
|
|
59
59
|
const listeners = new Map<string, Set<() => void>>();
|
|
60
60
|
|
|
61
61
|
const runtime: PluginRuntimeAccess = {
|
|
62
|
+
pinTicker() {},
|
|
63
|
+
navigateTicker() {},
|
|
62
64
|
subscribeResumeState(pluginId, key, listener) {
|
|
63
65
|
const listenerKey = `${pluginId}:${key}`;
|
|
64
66
|
if (!listeners.has(listenerKey)) listeners.set(listenerKey, new Set());
|
|
@@ -10,6 +10,8 @@ import {
|
|
|
10
10
|
import { useAppState, usePaneInstanceId, type PaneRuntimeState } from "../state/app-context";
|
|
11
11
|
|
|
12
12
|
export interface PluginRuntimeAccess {
|
|
13
|
+
pinTicker(symbol: string, options?: { floating?: boolean; paneType?: string }): void;
|
|
14
|
+
navigateTicker(symbol: string): void;
|
|
13
15
|
subscribeResumeState(pluginId: string, key: string, listener: () => void): () => void;
|
|
14
16
|
getResumeState<T = unknown>(pluginId: string, key: string, schemaVersion?: number): T | null;
|
|
15
17
|
setResumeState(pluginId: string, key: string, value: unknown, schemaVersion?: number): void;
|
|
@@ -51,6 +53,14 @@ function usePluginRenderContext(): PluginRenderContextValue {
|
|
|
51
53
|
return context;
|
|
52
54
|
}
|
|
53
55
|
|
|
56
|
+
export function usePluginTickerActions() {
|
|
57
|
+
const { runtime } = usePluginRenderContext();
|
|
58
|
+
return {
|
|
59
|
+
pinTicker: runtime.pinTicker,
|
|
60
|
+
navigateTicker: runtime.navigateTicker,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
54
64
|
export function getPluginPaneStateValue<T>(
|
|
55
65
|
paneState: PaneRuntimeState | undefined,
|
|
56
66
|
pluginId: string,
|
|
@@ -76,6 +76,14 @@ function formatPredictionLoadError(
|
|
|
76
76
|
return `${venueLabel} is unavailable right now.`;
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
if (
|
|
80
|
+
/socket connection|socket hang up|ECONNRESET|ETIMEDOUT|ENOTFOUND|EAI_AGAIN|fetch failed|network connection|connection closed/i.test(
|
|
81
|
+
message,
|
|
82
|
+
)
|
|
83
|
+
) {
|
|
84
|
+
return `${venueLabel} is unavailable right now.`;
|
|
85
|
+
}
|
|
86
|
+
|
|
79
87
|
return `${fallback} ${message}`;
|
|
80
88
|
}
|
|
81
89
|
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { TextAttributes } from "@opentui/core";
|
|
2
1
|
import { colors } from "../../../theme/colors";
|
|
3
|
-
import {
|
|
2
|
+
import { ExternalLinkText } from "../../../components/ui/external-link";
|
|
4
3
|
|
|
5
4
|
export function truncatePredictionText(
|
|
6
5
|
value: string,
|
|
@@ -19,10 +18,12 @@ export function SummaryLink({
|
|
|
19
18
|
maxLength: number;
|
|
20
19
|
}) {
|
|
21
20
|
return (
|
|
22
|
-
<box height={1}
|
|
23
|
-
<
|
|
24
|
-
{
|
|
25
|
-
|
|
21
|
+
<box height={1}>
|
|
22
|
+
<ExternalLinkText
|
|
23
|
+
url={url}
|
|
24
|
+
label={truncatePredictionText(url, maxLength)}
|
|
25
|
+
color={colors.textBright}
|
|
26
|
+
/>
|
|
26
27
|
</box>
|
|
27
28
|
);
|
|
28
29
|
}
|