gloomberb 0.3.3 → 0.3.7
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 +8 -8
- package/package.json +1 -1
- package/src/components/chart/chart-renderer.ts +7 -2
- package/src/components/chart/chart.test.ts +26 -0
- package/src/components/chart/stock-chart.tsx +36 -13
- package/src/plugins/ibkr/gateway-service.test.ts +30 -0
- package/src/plugins/ibkr/gateway-service.ts +38 -2
- package/src/plugins/prediction-markets/chart.test.tsx +139 -0
- package/src/plugins/prediction-markets/chart.tsx +65 -436
- package/src/plugins/prediction-markets/controller-data.ts +6 -2
- package/src/plugins/prediction-markets/detail/outcomes.tsx +2 -6
- package/src/plugins/prediction-markets/detail/pane.tsx +4 -25
- package/src/plugins/prediction-markets/detail/shared.tsx +0 -1
- package/src/plugins/prediction-markets/detail.test.tsx +4 -0
- package/src/plugins/prediction-markets/pane.test.tsx +41 -1
- package/src/plugins/prediction-markets/pane.tsx +1 -31
- package/src/sources/provider-router.test.ts +43 -0
- package/src/sources/provider-router.ts +8 -6
- package/src/utils/price-history.test.ts +25 -0
- package/src/utils/price-history.ts +29 -5
- package/src/version.ts +1 -1
|
@@ -90,7 +90,8 @@ describe("prediction markets pane interactions", () => {
|
|
|
90
90
|
await flushFrames(testSetup);
|
|
91
91
|
|
|
92
92
|
let frame = testSetup.captureCharFrame();
|
|
93
|
-
expect(frame).toContain("
|
|
93
|
+
expect(frame).toContain("All venues");
|
|
94
|
+
expect(frame).not.toContain("VOL = native venue units");
|
|
94
95
|
expect(frame).toContain("Will inflation fall?");
|
|
95
96
|
expect(frame).toContain("Kalshi");
|
|
96
97
|
|
|
@@ -117,6 +118,45 @@ describe("prediction markets pane interactions", () => {
|
|
|
117
118
|
expect(frame).toContain("Kalshi primary rule");
|
|
118
119
|
});
|
|
119
120
|
|
|
121
|
+
test("loads selected detail once instead of refetching in a render loop", async () => {
|
|
122
|
+
const { fetchUrls } = installPredictionMarketMocks();
|
|
123
|
+
|
|
124
|
+
testSetup = await testRender(<Harness />, { width: 120, height: 34 });
|
|
125
|
+
await flushFrames(testSetup);
|
|
126
|
+
|
|
127
|
+
const frame = testSetup.captureCharFrame();
|
|
128
|
+
const lines = frame.split("\n");
|
|
129
|
+
const kalshiRow = lines.findIndex((line) =>
|
|
130
|
+
line.includes("Will the Fed cut rates?"),
|
|
131
|
+
);
|
|
132
|
+
const kalshiCol = lines[kalshiRow]?.indexOf("Will the Fed cut rates?") ?? -1;
|
|
133
|
+
|
|
134
|
+
await act(async () => {
|
|
135
|
+
await testSetup!.mockMouse.click(kalshiCol + 1, kalshiRow);
|
|
136
|
+
await testSetup!.renderOnce();
|
|
137
|
+
});
|
|
138
|
+
await flushFrames(testSetup, 8);
|
|
139
|
+
|
|
140
|
+
const eventFetches = fetchUrls.filter((url) =>
|
|
141
|
+
url.includes("/trade-api/v2/events/FED-1"),
|
|
142
|
+
);
|
|
143
|
+
const orderbookFetches = fetchUrls.filter((url) =>
|
|
144
|
+
url.includes("/trade-api/v2/markets/KAL-1/orderbook"),
|
|
145
|
+
);
|
|
146
|
+
const tradeFetches = fetchUrls.filter((url) =>
|
|
147
|
+
url.includes("/trade-api/v2/markets/trades?ticker=KAL-1"),
|
|
148
|
+
);
|
|
149
|
+
const historyFetches = fetchUrls.filter((url) =>
|
|
150
|
+
url.includes("/trade-api/v2/series/FED/markets/KAL-1/candlesticks"),
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
expect(eventFetches).toHaveLength(2);
|
|
154
|
+
expect(orderbookFetches).toHaveLength(1);
|
|
155
|
+
expect(tradeFetches).toHaveLength(1);
|
|
156
|
+
expect(historyFetches).toHaveLength(1);
|
|
157
|
+
expect(testSetup.captureCharFrame()).not.toContain("Loading market detail...");
|
|
158
|
+
});
|
|
159
|
+
|
|
120
160
|
test("moves selection through the list with keyboard navigation", async () => {
|
|
121
161
|
installPredictionMarketMocks();
|
|
122
162
|
|
|
@@ -1,16 +1,14 @@
|
|
|
1
1
|
import { useRef } from "react";
|
|
2
2
|
import { TextAttributes, type BoxRenderable } from "@opentui/core";
|
|
3
|
-
import {
|
|
3
|
+
import { TabBar } from "../../components";
|
|
4
4
|
import { usePredictionMarketsController } from "./controller";
|
|
5
5
|
import { PredictionMarketDetailPane } from "./detail/pane";
|
|
6
6
|
import { BROWSE_TABS, VENUE_TABS } from "./navigation";
|
|
7
7
|
import { PredictionMarketsTable } from "./table";
|
|
8
|
-
import { formatPredictionTransportBadge } from "./status";
|
|
9
8
|
import { PREDICTION_CATEGORY_OPTIONS } from "./categories";
|
|
10
9
|
import type { PredictionBrowseTab, PredictionCategoryId } from "./types";
|
|
11
10
|
import type { PaneProps } from "../../types/plugin";
|
|
12
11
|
import { colors } from "../../theme/colors";
|
|
13
|
-
import { formatTimeAgo } from "../../utils/format";
|
|
14
12
|
|
|
15
13
|
function clamp(value: number, min: number, max: number): number {
|
|
16
14
|
return Math.min(Math.max(value, min), max);
|
|
@@ -22,12 +20,6 @@ export function PredictionMarketsPane({ focused, width, height }: PaneProps) {
|
|
|
22
20
|
const splitDragRef = useRef<{ startX: number; startRatio: number } | null>(
|
|
23
21
|
null,
|
|
24
22
|
);
|
|
25
|
-
const statusBadge = formatPredictionTransportBadge(
|
|
26
|
-
controller.catalogLoadCount > 0 || controller.detailLoadCount > 0
|
|
27
|
-
? "loading"
|
|
28
|
-
: controller.transportState,
|
|
29
|
-
);
|
|
30
|
-
const showSummaryUnitsHint = controller.effectiveVenueScope === "all";
|
|
31
23
|
const minTableWidth = 44;
|
|
32
24
|
const minDetailWidth = 36;
|
|
33
25
|
const effectiveMinTableWidth = controller.selectedSummary
|
|
@@ -108,28 +100,6 @@ export function PredictionMarketsPane({ focused, width, height }: PaneProps) {
|
|
|
108
100
|
backgroundColor={colors.panel}
|
|
109
101
|
onMouse={handlePaneMouse}
|
|
110
102
|
>
|
|
111
|
-
{!controller.paneSettings.hideHeader && (
|
|
112
|
-
<box flexDirection="row" height={1} paddingX={1}>
|
|
113
|
-
<text fg={colors.textBright} attributes={TextAttributes.BOLD}>
|
|
114
|
-
Prediction Markets
|
|
115
|
-
</text>
|
|
116
|
-
<box flexGrow={1} />
|
|
117
|
-
{showSummaryUnitsHint && (
|
|
118
|
-
<text fg={colors.textDim}>VOL = native venue units</text>
|
|
119
|
-
)}
|
|
120
|
-
<box width={1} />
|
|
121
|
-
{controller.searchLoading && <Spinner />}
|
|
122
|
-
{controller.searchLoading && <box width={1} />}
|
|
123
|
-
<text fg={statusBadge.color}>{statusBadge.label}</text>
|
|
124
|
-
<box width={1} />
|
|
125
|
-
<text fg={colors.textDim}>
|
|
126
|
-
{controller.lastRefreshAt
|
|
127
|
-
? `refresh ${formatTimeAgo(new Date(controller.lastRefreshAt))}`
|
|
128
|
-
: "waiting"}
|
|
129
|
-
</text>
|
|
130
|
-
</box>
|
|
131
|
-
)}
|
|
132
|
-
|
|
133
103
|
{!controller.paneSettings.hideTabs && (
|
|
134
104
|
<TabBar
|
|
135
105
|
tabs={VENUE_TABS.map((tab) => ({
|
|
@@ -1355,6 +1355,49 @@ describe("ProviderRouter", () => {
|
|
|
1355
1355
|
expect(history.map((point) => point.close)).toEqual([101, 102, 103]);
|
|
1356
1356
|
});
|
|
1357
1357
|
|
|
1358
|
+
test("ignores poisoned cached chart history and refetches clean data", async () => {
|
|
1359
|
+
const dbPath = createTempDbPath("poisoned-chart-cache");
|
|
1360
|
+
const persistence = new AppPersistence(dbPath);
|
|
1361
|
+
|
|
1362
|
+
persistence.resources.set(
|
|
1363
|
+
{
|
|
1364
|
+
namespace: "market",
|
|
1365
|
+
kind: "price-history",
|
|
1366
|
+
entityKey: "AAPL",
|
|
1367
|
+
variantKey: "exchange=NASDAQ;range=1Y",
|
|
1368
|
+
sourceKey: "provider:yahoo",
|
|
1369
|
+
},
|
|
1370
|
+
[
|
|
1371
|
+
{ date: null as any, close: 101 },
|
|
1372
|
+
{ date: null as any, close: 102 },
|
|
1373
|
+
],
|
|
1374
|
+
{
|
|
1375
|
+
cachePolicy: { staleMs: 60_000, expireMs: 60_000 },
|
|
1376
|
+
},
|
|
1377
|
+
);
|
|
1378
|
+
|
|
1379
|
+
let providerCalls = 0;
|
|
1380
|
+
const router = new ProviderRouter({
|
|
1381
|
+
...fallbackProvider,
|
|
1382
|
+
id: "yahoo",
|
|
1383
|
+
name: "Yahoo",
|
|
1384
|
+
async getPriceHistory() {
|
|
1385
|
+
providerCalls += 1;
|
|
1386
|
+
return [
|
|
1387
|
+
{ date: new Date("2026-03-27T00:00:00Z"), close: 201 },
|
|
1388
|
+
{ date: new Date("2026-03-28T00:00:00Z"), close: 202 },
|
|
1389
|
+
];
|
|
1390
|
+
},
|
|
1391
|
+
}, [], persistence.resources);
|
|
1392
|
+
|
|
1393
|
+
const history = await router.getPriceHistory("AAPL", "NASDAQ", "1Y");
|
|
1394
|
+
|
|
1395
|
+
expect(providerCalls).toBe(1);
|
|
1396
|
+
expect(history.map((point) => point.close)).toEqual([201, 202]);
|
|
1397
|
+
|
|
1398
|
+
persistence.close();
|
|
1399
|
+
});
|
|
1400
|
+
|
|
1358
1401
|
test("bypasses cached financials on explicit refresh requests", async () => {
|
|
1359
1402
|
const dbPath = createTempDbPath("forced-financial-refresh");
|
|
1360
1403
|
const persistence = new AppPersistence(dbPath);
|
|
@@ -483,9 +483,10 @@ export class ProviderRouter implements DataProvider {
|
|
|
483
483
|
...this.getProviderSourceKeys(),
|
|
484
484
|
];
|
|
485
485
|
const cached = this.selectCachedArrayResource<PricePoint>("price-history", entityKey, variantKeys, sourceKeys, false);
|
|
486
|
+
const cachedValue = cached ? normalizePriceHistory(cached.value) : [];
|
|
486
487
|
const forceRefresh = context?.cacheMode === "refresh";
|
|
487
|
-
if (
|
|
488
|
-
return
|
|
488
|
+
if (cachedValue.length > 0 && !forceRefresh && cached && !cached.stale) {
|
|
489
|
+
return cachedValue;
|
|
489
490
|
}
|
|
490
491
|
|
|
491
492
|
const brokerHistory = await withBrokerTimeout(this.fetchBrokerPriceHistory(ticker, exchange, range, context));
|
|
@@ -495,7 +496,7 @@ export class ProviderRouter implements DataProvider {
|
|
|
495
496
|
if (providerHistory && providerHistory.value.length > 0) {
|
|
496
497
|
return providerHistory.value;
|
|
497
498
|
}
|
|
498
|
-
if (
|
|
499
|
+
if (cachedValue.length > 0) return cachedValue;
|
|
499
500
|
if (!providerHistory) {
|
|
500
501
|
throw new Error(`No history provider available for ${ticker}`);
|
|
501
502
|
}
|
|
@@ -520,9 +521,10 @@ export class ProviderRouter implements DataProvider {
|
|
|
520
521
|
...this.getProviderSourceKeys(),
|
|
521
522
|
];
|
|
522
523
|
const cached = this.selectCachedArrayResource<PricePoint>("detailed-price-history", entityKey, variantKeys, sourceKeys, false);
|
|
524
|
+
const cachedValue = cached ? normalizePriceHistory(cached.value) : [];
|
|
523
525
|
const forceRefresh = context?.cacheMode === "refresh";
|
|
524
|
-
if (
|
|
525
|
-
return
|
|
526
|
+
if (cachedValue.length > 0 && !forceRefresh && cached && !cached.stale) {
|
|
527
|
+
return cachedValue;
|
|
526
528
|
}
|
|
527
529
|
|
|
528
530
|
const brokerResult = await withBrokerTimeout(this.fetchBrokerDetailedPriceHistory(ticker, exchange, startDate, endDate, barSize, context));
|
|
@@ -532,7 +534,7 @@ export class ProviderRouter implements DataProvider {
|
|
|
532
534
|
if (providerResult && providerResult.value.length > 0) {
|
|
533
535
|
return providerResult.value;
|
|
534
536
|
}
|
|
535
|
-
return
|
|
537
|
+
return cachedValue.length > 0 ? cachedValue : (providerResult?.value ?? []);
|
|
536
538
|
}
|
|
537
539
|
|
|
538
540
|
async getOptionsChain(ticker: string, exchange?: string, expirationDate?: number, context?: MarketDataRequestContext): Promise<OptionsChain> {
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { normalizePriceHistory } from "./price-history";
|
|
3
|
+
|
|
4
|
+
describe("normalizePriceHistory", () => {
|
|
5
|
+
test("drops poisoned cached history when every timestamp collapses to the same value", () => {
|
|
6
|
+
const history = normalizePriceHistory([
|
|
7
|
+
{ date: null as any, close: 101 },
|
|
8
|
+
{ date: null as any, close: 102 },
|
|
9
|
+
{ date: null as any, close: 103 },
|
|
10
|
+
]);
|
|
11
|
+
|
|
12
|
+
expect(history).toEqual([]);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test("filters invalid dates and keeps the remaining points in chronological order", () => {
|
|
16
|
+
const history = normalizePriceHistory([
|
|
17
|
+
{ date: new Date("2026-03-29T00:00:00Z"), close: 103 },
|
|
18
|
+
{ date: null as any, close: 999 },
|
|
19
|
+
{ date: new Date("2026-03-27T00:00:00Z"), close: 101 },
|
|
20
|
+
{ date: new Date("2026-03-28T00:00:00Z"), close: 102 },
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
expect(history.map((point) => point.close)).toEqual([101, 102, 103]);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import type { PricePoint, TickerFinancials } from "../types/financials";
|
|
2
2
|
|
|
3
3
|
function getPointTimestamp(point: PricePoint): number {
|
|
4
|
-
|
|
4
|
+
const value = point.date as Date | string | number | null | undefined;
|
|
5
|
+
if (value instanceof Date) return value.getTime();
|
|
6
|
+
if (value == null) return Number.NaN;
|
|
7
|
+
return new Date(value).getTime();
|
|
5
8
|
}
|
|
6
9
|
|
|
7
10
|
function comparePricePointsByDate(left: PricePoint, right: PricePoint): number {
|
|
@@ -17,18 +20,39 @@ function comparePricePointsByDate(left: PricePoint, right: PricePoint): number {
|
|
|
17
20
|
}
|
|
18
21
|
|
|
19
22
|
export function normalizePriceHistory(points: PricePoint[]): PricePoint[] {
|
|
20
|
-
if (points.length
|
|
23
|
+
if (points.length === 0) return points;
|
|
21
24
|
|
|
25
|
+
const validPoints: PricePoint[] = [];
|
|
26
|
+
let sawDistinctTimestamp = false;
|
|
27
|
+
let firstTimestamp: number | null = null;
|
|
22
28
|
let previousTime = Number.NEGATIVE_INFINITY;
|
|
29
|
+
let requiresSort = false;
|
|
30
|
+
|
|
23
31
|
for (const point of points) {
|
|
24
32
|
const time = getPointTimestamp(point);
|
|
25
|
-
if (!Number.isFinite(time)
|
|
26
|
-
|
|
33
|
+
if (!Number.isFinite(time)) continue;
|
|
34
|
+
|
|
35
|
+
if (firstTimestamp === null) {
|
|
36
|
+
firstTimestamp = time;
|
|
37
|
+
} else if (time !== firstTimestamp) {
|
|
38
|
+
sawDistinctTimestamp = true;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (time < previousTime) {
|
|
42
|
+
requiresSort = true;
|
|
27
43
|
}
|
|
28
44
|
previousTime = time;
|
|
45
|
+
validPoints.push(point);
|
|
29
46
|
}
|
|
30
47
|
|
|
31
|
-
return
|
|
48
|
+
if (validPoints.length === 0) return [];
|
|
49
|
+
if (validPoints.length === 1) return validPoints;
|
|
50
|
+
if (!sawDistinctTimestamp) return [];
|
|
51
|
+
|
|
52
|
+
if (requiresSort) {
|
|
53
|
+
return [...validPoints].sort(comparePricePointsByDate);
|
|
54
|
+
}
|
|
55
|
+
return validPoints.length === points.length ? points : validPoints;
|
|
32
56
|
}
|
|
33
57
|
|
|
34
58
|
export function normalizeTickerFinancialsPriceHistory(financials: TickerFinancials): TickerFinancials {
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = "0.3.
|
|
1
|
+
export const VERSION = "0.3.4";
|