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
|
@@ -1,27 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { TextAttributes
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
buildChartScene,
|
|
6
|
-
renderChart,
|
|
7
|
-
resolveChartPalette,
|
|
8
|
-
type StyledContent,
|
|
9
|
-
} from "../../components/chart/chart-renderer";
|
|
10
|
-
import {
|
|
11
|
-
computeBitmapSize,
|
|
12
|
-
renderNativeChartBase,
|
|
13
|
-
type CellRect,
|
|
14
|
-
type NativeChartBitmap,
|
|
15
|
-
} from "../../components/chart/native/chart-rasterizer";
|
|
16
|
-
import {
|
|
17
|
-
ensureKittySupport,
|
|
18
|
-
getCachedKittySupport,
|
|
19
|
-
} from "../../components/chart/native/kitty-support";
|
|
20
|
-
import { resolveChartRendererState } from "../../components/chart/native/renderer-selection";
|
|
21
|
-
import { getNativeSurfaceManager } from "../../components/chart/native/surface-manager";
|
|
22
|
-
import { syncCachedNativeSurface } from "../../components/chart/native/surface-sync";
|
|
23
|
-
import { projectChartData } from "../../components/chart/chart-data";
|
|
24
|
-
import { useAppState, usePaneInstanceId } from "../../state/app-context";
|
|
1
|
+
import { useMemo } from "react";
|
|
2
|
+
import { TextAttributes } from "@opentui/core";
|
|
3
|
+
import { StockChart } from "../../components/chart/stock-chart";
|
|
25
4
|
import { EmptyState } from "../../components/ui/status";
|
|
26
5
|
import { colors } from "../../theme/colors";
|
|
27
6
|
import { formatNumber, formatPercentRaw } from "../../utils/format";
|
|
@@ -29,15 +8,6 @@ import type { PricePoint } from "../../types/financials";
|
|
|
29
8
|
import type { PredictionHistoryPoint, PredictionHistoryRange } from "./types";
|
|
30
9
|
|
|
31
10
|
const RANGES: PredictionHistoryRange[] = ["1D", "1W", "1M", "ALL"];
|
|
32
|
-
const AXIS_WIDTH = 8;
|
|
33
|
-
|
|
34
|
-
interface RenderableNode {
|
|
35
|
-
x: number;
|
|
36
|
-
y: number;
|
|
37
|
-
width: number;
|
|
38
|
-
height: number;
|
|
39
|
-
parent: RenderableNode | null;
|
|
40
|
-
}
|
|
41
11
|
|
|
42
12
|
function coercePredictionPointDate(value: unknown): Date | null {
|
|
43
13
|
if (value instanceof Date) {
|
|
@@ -54,79 +24,47 @@ function toPricePoints(points: PredictionHistoryPoint[]): PricePoint[] {
|
|
|
54
24
|
return points.flatMap((point) => {
|
|
55
25
|
const date = coercePredictionPointDate(point.date);
|
|
56
26
|
if (!date) return [];
|
|
57
|
-
return [
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
},
|
|
66
|
-
];
|
|
27
|
+
return [{
|
|
28
|
+
date,
|
|
29
|
+
close: point.close,
|
|
30
|
+
open: point.open,
|
|
31
|
+
high: point.high,
|
|
32
|
+
low: point.low,
|
|
33
|
+
volume: point.volume,
|
|
34
|
+
}];
|
|
67
35
|
});
|
|
68
36
|
}
|
|
69
37
|
|
|
70
|
-
function
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
};
|
|
101
|
-
let current: RenderableNode | null = renderable;
|
|
102
|
-
|
|
103
|
-
while (current) {
|
|
104
|
-
const nextVisible = intersectCellRects(visible, extractCellRect(current));
|
|
105
|
-
if (!nextVisible) return null;
|
|
106
|
-
visible = nextVisible;
|
|
107
|
-
current = current.parent;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
return visible;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function buildBlankPlotLines(width: number, height: number): string[] {
|
|
114
|
-
return Array.from({ length: height }, () => " ".repeat(width));
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function buildNativeBitmapKey(
|
|
118
|
-
points: PricePoint[],
|
|
119
|
-
pixelWidth: number,
|
|
120
|
-
pixelHeight: number,
|
|
121
|
-
paletteId: string,
|
|
122
|
-
): string {
|
|
123
|
-
const fingerprint = points
|
|
124
|
-
.map(
|
|
125
|
-
(point) =>
|
|
126
|
-
`${point.date.getTime()}:${point.open}:${point.high}:${point.low}:${point.close}:${point.volume ?? 0}`,
|
|
127
|
-
)
|
|
128
|
-
.join("|");
|
|
129
|
-
return [pixelWidth, pixelHeight, paletteId, fingerprint].join("::");
|
|
38
|
+
function PredictionRangeTabs({
|
|
39
|
+
activeRange,
|
|
40
|
+
onRangeSelect,
|
|
41
|
+
}: {
|
|
42
|
+
activeRange: PredictionHistoryRange;
|
|
43
|
+
onRangeSelect: (range: PredictionHistoryRange) => void;
|
|
44
|
+
}) {
|
|
45
|
+
return (
|
|
46
|
+
<box flexDirection="row" gap={1}>
|
|
47
|
+
{RANGES.map((entry) => {
|
|
48
|
+
const active = entry === activeRange;
|
|
49
|
+
return (
|
|
50
|
+
<box
|
|
51
|
+
key={entry}
|
|
52
|
+
onMouseDown={(event) => {
|
|
53
|
+
event.preventDefault();
|
|
54
|
+
onRangeSelect(entry);
|
|
55
|
+
}}
|
|
56
|
+
>
|
|
57
|
+
<text
|
|
58
|
+
fg={active ? colors.textBright : colors.textDim}
|
|
59
|
+
attributes={active ? TextAttributes.BOLD : 0}
|
|
60
|
+
>
|
|
61
|
+
{entry}
|
|
62
|
+
</text>
|
|
63
|
+
</box>
|
|
64
|
+
);
|
|
65
|
+
})}
|
|
66
|
+
</box>
|
|
67
|
+
);
|
|
130
68
|
}
|
|
131
69
|
|
|
132
70
|
export function PredictionMarketChart({
|
|
@@ -142,299 +80,16 @@ export function PredictionMarketChart({
|
|
|
142
80
|
range: PredictionHistoryRange;
|
|
143
81
|
onRangeSelect: (range: PredictionHistoryRange) => void;
|
|
144
82
|
}) {
|
|
145
|
-
const { state } = useAppState();
|
|
146
|
-
const renderer = useRenderer();
|
|
147
|
-
const paneId = usePaneInstanceId();
|
|
148
|
-
const nativeSurfaceManager = useMemo(
|
|
149
|
-
() => getNativeSurfaceManager(renderer),
|
|
150
|
-
[renderer],
|
|
151
|
-
);
|
|
152
|
-
const [kittySupport, setKittySupport] = useState<boolean | null>(() =>
|
|
153
|
-
getCachedKittySupport(renderer),
|
|
154
|
-
);
|
|
155
|
-
const plotRef = useRef<BoxRenderable>(null);
|
|
156
|
-
const nativeSurfaceIdRef = useRef(`prediction-chart-surface:${paneId}`);
|
|
157
|
-
const lastNativeBitmapRef = useRef<{
|
|
158
|
-
key: string;
|
|
159
|
-
bitmap: NativeChartBitmap;
|
|
160
|
-
} | null>(null);
|
|
161
|
-
const lastNativeGeometryRef = useRef<{
|
|
162
|
-
rect: CellRect;
|
|
163
|
-
visibleRect: CellRect | null;
|
|
164
|
-
} | null>(null);
|
|
165
|
-
|
|
166
|
-
const chartWidth = Math.max(width - AXIS_WIDTH - 2, 18);
|
|
167
|
-
const chartHeight = Math.max(height - 2, 6);
|
|
168
|
-
const hasHistory = history.length > 0;
|
|
169
|
-
|
|
170
|
-
useEffect(() => {
|
|
171
|
-
if (kittySupport !== null) return;
|
|
172
|
-
let disposed = false;
|
|
173
|
-
ensureKittySupport(renderer)
|
|
174
|
-
.then((supported) => {
|
|
175
|
-
if (!disposed) setKittySupport(supported);
|
|
176
|
-
})
|
|
177
|
-
.catch(() => {
|
|
178
|
-
if (!disposed) setKittySupport(false);
|
|
179
|
-
});
|
|
180
|
-
return () => {
|
|
181
|
-
disposed = true;
|
|
182
|
-
};
|
|
183
|
-
}, [kittySupport, renderer]);
|
|
184
|
-
|
|
185
83
|
const pricePoints = useMemo(() => toPricePoints(history), [history]);
|
|
186
|
-
const projection = useMemo(
|
|
187
|
-
() => projectChartData(pricePoints, chartWidth, "area"),
|
|
188
|
-
[chartWidth, pricePoints],
|
|
189
|
-
);
|
|
190
|
-
const first = projection.points[0] ?? null;
|
|
191
|
-
const last = projection.points[projection.points.length - 1] ?? null;
|
|
192
|
-
const delta = first && last ? last.close - first.close : 0;
|
|
193
|
-
const deltaPct = first?.close ? (delta / first.close) * 100 : 0;
|
|
194
|
-
const palette = useMemo(
|
|
195
|
-
() =>
|
|
196
|
-
resolveChartPalette(
|
|
197
|
-
colors,
|
|
198
|
-
delta > 0 ? "positive" : delta < 0 ? "negative" : "neutral",
|
|
199
|
-
),
|
|
200
|
-
[delta],
|
|
201
|
-
);
|
|
202
|
-
const rendererState = resolveChartRendererState(
|
|
203
|
-
state.config.chartPreferences.renderer,
|
|
204
|
-
kittySupport,
|
|
205
|
-
renderer.resolution,
|
|
206
|
-
);
|
|
207
|
-
const effectiveRenderer = rendererState.renderer;
|
|
208
84
|
|
|
209
|
-
|
|
210
|
-
() =>
|
|
211
|
-
renderChart(projection.points, {
|
|
212
|
-
width: chartWidth,
|
|
213
|
-
height: chartHeight,
|
|
214
|
-
mode: "area",
|
|
215
|
-
showVolume: false,
|
|
216
|
-
volumeHeight: 0,
|
|
217
|
-
cursorX: null,
|
|
218
|
-
cursorY: null,
|
|
219
|
-
colors: palette,
|
|
220
|
-
axisMode: "price",
|
|
221
|
-
currency: "USD",
|
|
222
|
-
}),
|
|
223
|
-
[chartHeight, chartWidth, palette, projection.points],
|
|
224
|
-
);
|
|
225
|
-
|
|
226
|
-
const nativeScene = useMemo(
|
|
227
|
-
() =>
|
|
228
|
-
buildChartScene(projection.points, {
|
|
229
|
-
width: chartWidth,
|
|
230
|
-
height: chartHeight,
|
|
231
|
-
showVolume: false,
|
|
232
|
-
volumeHeight: 0,
|
|
233
|
-
cursorX: null,
|
|
234
|
-
cursorY: null,
|
|
235
|
-
mode: "area",
|
|
236
|
-
axisMode: "price",
|
|
237
|
-
colors: palette,
|
|
238
|
-
}),
|
|
239
|
-
[chartHeight, chartWidth, palette, projection.points],
|
|
240
|
-
);
|
|
241
|
-
|
|
242
|
-
const axisByRow = useMemo(
|
|
243
|
-
() => new Map(rendered.axisLabels.map((entry) => [entry.row, entry.label])),
|
|
244
|
-
[rendered.axisLabels],
|
|
245
|
-
);
|
|
246
|
-
const blankPlotLines = useMemo(
|
|
247
|
-
() => buildBlankPlotLines(chartWidth, chartHeight),
|
|
248
|
-
[chartHeight, chartWidth],
|
|
249
|
-
);
|
|
250
|
-
|
|
251
|
-
useEffect(
|
|
252
|
-
() => () => {
|
|
253
|
-
lastNativeBitmapRef.current = null;
|
|
254
|
-
lastNativeGeometryRef.current = null;
|
|
255
|
-
nativeSurfaceManager.removeSurface(nativeSurfaceIdRef.current);
|
|
256
|
-
renderer.requestRender();
|
|
257
|
-
},
|
|
258
|
-
[nativeSurfaceManager, renderer],
|
|
259
|
-
);
|
|
260
|
-
|
|
261
|
-
useEffect(() => {
|
|
262
|
-
if (effectiveRenderer === "kitty" && rendererState.nativeReady) return;
|
|
263
|
-
lastNativeBitmapRef.current = null;
|
|
264
|
-
lastNativeGeometryRef.current = null;
|
|
265
|
-
nativeSurfaceManager.removeSurface(nativeSurfaceIdRef.current);
|
|
266
|
-
}, [effectiveRenderer, nativeSurfaceManager, rendererState.nativeReady]);
|
|
267
|
-
|
|
268
|
-
useEffect(() => {
|
|
269
|
-
if (
|
|
270
|
-
effectiveRenderer !== "kitty" ||
|
|
271
|
-
!rendererState.nativeReady ||
|
|
272
|
-
!plotRef.current
|
|
273
|
-
)
|
|
274
|
-
return;
|
|
275
|
-
const plot = plotRef.current;
|
|
276
|
-
let mountTimer: ReturnType<typeof setTimeout> | null = null;
|
|
277
|
-
|
|
278
|
-
const syncPlacement = () => {
|
|
279
|
-
if (
|
|
280
|
-
effectiveRenderer !== "kitty" ||
|
|
281
|
-
!rendererState.nativeReady ||
|
|
282
|
-
!plotRef.current
|
|
283
|
-
)
|
|
284
|
-
return;
|
|
285
|
-
const rect = extractCellRect(plotRef.current);
|
|
286
|
-
const visibleRect = resolveVisibleRect(
|
|
287
|
-
plotRef.current,
|
|
288
|
-
renderer.terminalWidth,
|
|
289
|
-
renderer.terminalHeight,
|
|
290
|
-
);
|
|
291
|
-
const previous = lastNativeGeometryRef.current;
|
|
292
|
-
if (
|
|
293
|
-
previous &&
|
|
294
|
-
previous.rect.x === rect.x &&
|
|
295
|
-
previous.rect.y === rect.y &&
|
|
296
|
-
previous.rect.width === rect.width &&
|
|
297
|
-
previous.rect.height === rect.height &&
|
|
298
|
-
previous.visibleRect?.x === visibleRect?.x &&
|
|
299
|
-
previous.visibleRect?.y === visibleRect?.y &&
|
|
300
|
-
previous.visibleRect?.width === visibleRect?.width &&
|
|
301
|
-
previous.visibleRect?.height === visibleRect?.height
|
|
302
|
-
) {
|
|
303
|
-
return;
|
|
304
|
-
}
|
|
305
|
-
lastNativeGeometryRef.current = { rect, visibleRect };
|
|
306
|
-
syncCachedNativeSurface(
|
|
307
|
-
nativeSurfaceManager,
|
|
308
|
-
nativeSurfaceIdRef.current,
|
|
309
|
-
{ paneId, rect, visibleRect },
|
|
310
|
-
lastNativeBitmapRef.current,
|
|
311
|
-
);
|
|
312
|
-
};
|
|
313
|
-
|
|
314
|
-
plot.onLifecyclePass = syncPlacement;
|
|
315
|
-
renderer.registerLifecyclePass(plot);
|
|
316
|
-
syncPlacement();
|
|
317
|
-
mountTimer = setTimeout(() => {
|
|
318
|
-
syncPlacement();
|
|
319
|
-
renderer.requestRender();
|
|
320
|
-
}, 0);
|
|
321
|
-
|
|
322
|
-
return () => {
|
|
323
|
-
if (mountTimer) clearTimeout(mountTimer);
|
|
324
|
-
plot.onLifecyclePass = null;
|
|
325
|
-
renderer.unregisterLifecyclePass(plot);
|
|
326
|
-
lastNativeGeometryRef.current = null;
|
|
327
|
-
};
|
|
328
|
-
}, [
|
|
329
|
-
effectiveRenderer,
|
|
330
|
-
nativeSurfaceManager,
|
|
331
|
-
paneId,
|
|
332
|
-
renderer,
|
|
333
|
-
rendererState.nativeReady,
|
|
334
|
-
]);
|
|
335
|
-
|
|
336
|
-
useEffect(() => {
|
|
337
|
-
if (
|
|
338
|
-
effectiveRenderer !== "kitty" ||
|
|
339
|
-
!rendererState.nativeReady ||
|
|
340
|
-
!renderer.resolution ||
|
|
341
|
-
!plotRef.current ||
|
|
342
|
-
!nativeScene
|
|
343
|
-
) {
|
|
344
|
-
lastNativeBitmapRef.current = null;
|
|
345
|
-
nativeSurfaceManager.removeSurface(nativeSurfaceIdRef.current);
|
|
346
|
-
return;
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
const plotRect = extractCellRect(plotRef.current);
|
|
350
|
-
const visibleRect = resolveVisibleRect(
|
|
351
|
-
plotRef.current,
|
|
352
|
-
renderer.terminalWidth,
|
|
353
|
-
renderer.terminalHeight,
|
|
354
|
-
);
|
|
355
|
-
if (!visibleRect) {
|
|
356
|
-
nativeSurfaceManager.removeSurface(nativeSurfaceIdRef.current);
|
|
357
|
-
return;
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
const bitmapSize = computeBitmapSize(
|
|
361
|
-
plotRect,
|
|
362
|
-
renderer.resolution,
|
|
363
|
-
renderer.terminalWidth,
|
|
364
|
-
renderer.terminalHeight,
|
|
365
|
-
);
|
|
366
|
-
const bitmapKey = buildNativeBitmapKey(
|
|
367
|
-
pricePoints,
|
|
368
|
-
bitmapSize.pixelWidth,
|
|
369
|
-
bitmapSize.pixelHeight,
|
|
370
|
-
[palette.lineColor, palette.fillColor, palette.gridColor].join(","),
|
|
371
|
-
);
|
|
372
|
-
const cachedBitmap =
|
|
373
|
-
lastNativeBitmapRef.current?.key === bitmapKey
|
|
374
|
-
? lastNativeBitmapRef.current.bitmap
|
|
375
|
-
: null;
|
|
376
|
-
const bitmap =
|
|
377
|
-
cachedBitmap ??
|
|
378
|
-
renderNativeChartBase(
|
|
379
|
-
nativeScene,
|
|
380
|
-
bitmapSize.pixelWidth,
|
|
381
|
-
bitmapSize.pixelHeight,
|
|
382
|
-
);
|
|
383
|
-
if (!cachedBitmap) {
|
|
384
|
-
lastNativeBitmapRef.current = { key: bitmapKey, bitmap };
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
nativeSurfaceManager.upsertSurface({
|
|
388
|
-
id: nativeSurfaceIdRef.current,
|
|
389
|
-
paneId,
|
|
390
|
-
rect: plotRect,
|
|
391
|
-
visibleRect,
|
|
392
|
-
bitmap,
|
|
393
|
-
bitmapKey,
|
|
394
|
-
});
|
|
395
|
-
renderer.requestRender();
|
|
396
|
-
}, [
|
|
397
|
-
effectiveRenderer,
|
|
398
|
-
nativeScene,
|
|
399
|
-
nativeSurfaceManager,
|
|
400
|
-
paneId,
|
|
401
|
-
palette.fillColor,
|
|
402
|
-
palette.gridColor,
|
|
403
|
-
palette.lineColor,
|
|
404
|
-
pricePoints,
|
|
405
|
-
renderer,
|
|
406
|
-
rendererState.nativeReady,
|
|
407
|
-
]);
|
|
408
|
-
|
|
409
|
-
const plotLines: Array<string | StyledContent> =
|
|
410
|
-
effectiveRenderer === "kitty" ? blankPlotLines : rendered.lines;
|
|
411
|
-
const plotContent = plotLines.map((line, index) => (
|
|
412
|
-
<text key={index} content={line as any} />
|
|
413
|
-
));
|
|
414
|
-
|
|
415
|
-
if (!hasHistory) {
|
|
85
|
+
if (pricePoints.length === 0) {
|
|
416
86
|
return (
|
|
417
87
|
<box flexDirection="column" height={height}>
|
|
418
|
-
<box flexDirection="row"
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
key={entry}
|
|
424
|
-
onMouseDown={(event) => {
|
|
425
|
-
event.preventDefault();
|
|
426
|
-
onRangeSelect(entry);
|
|
427
|
-
}}
|
|
428
|
-
>
|
|
429
|
-
<text
|
|
430
|
-
fg={active ? colors.textBright : colors.textDim}
|
|
431
|
-
attributes={active ? TextAttributes.BOLD : 0}
|
|
432
|
-
>
|
|
433
|
-
{entry}
|
|
434
|
-
</text>
|
|
435
|
-
</box>
|
|
436
|
-
);
|
|
437
|
-
})}
|
|
88
|
+
<box flexDirection="row" height={1}>
|
|
89
|
+
<PredictionRangeTabs
|
|
90
|
+
activeRange={range}
|
|
91
|
+
onRangeSelect={onRangeSelect}
|
|
92
|
+
/>
|
|
438
93
|
</box>
|
|
439
94
|
<box flexGrow={1} justifyContent="center">
|
|
440
95
|
<EmptyState
|
|
@@ -446,30 +101,19 @@ export function PredictionMarketChart({
|
|
|
446
101
|
);
|
|
447
102
|
}
|
|
448
103
|
|
|
104
|
+
const first = pricePoints[0] ?? null;
|
|
105
|
+
const last = pricePoints[pricePoints.length - 1] ?? null;
|
|
106
|
+
const delta = first && last ? last.close - first.close : 0;
|
|
107
|
+
const deltaPct = first?.close ? (delta / first.close) * 100 : 0;
|
|
108
|
+
const chartHeight = Math.max(height - 1, 2);
|
|
109
|
+
|
|
449
110
|
return (
|
|
450
111
|
<box flexDirection="column" height={height}>
|
|
451
112
|
<box flexDirection="row" height={1}>
|
|
452
|
-
<
|
|
453
|
-
{
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
<box
|
|
457
|
-
key={entry}
|
|
458
|
-
onMouseDown={(event) => {
|
|
459
|
-
event.preventDefault();
|
|
460
|
-
onRangeSelect(entry);
|
|
461
|
-
}}
|
|
462
|
-
>
|
|
463
|
-
<text
|
|
464
|
-
fg={active ? colors.textBright : colors.textDim}
|
|
465
|
-
attributes={active ? TextAttributes.BOLD : 0}
|
|
466
|
-
>
|
|
467
|
-
{entry}
|
|
468
|
-
</text>
|
|
469
|
-
</box>
|
|
470
|
-
);
|
|
471
|
-
})}
|
|
472
|
-
</box>
|
|
113
|
+
<PredictionRangeTabs
|
|
114
|
+
activeRange={range}
|
|
115
|
+
onRangeSelect={onRangeSelect}
|
|
116
|
+
/>
|
|
473
117
|
<box flexGrow={1} />
|
|
474
118
|
<text
|
|
475
119
|
fg={
|
|
@@ -484,29 +128,14 @@ export function PredictionMarketChart({
|
|
|
484
128
|
</text>
|
|
485
129
|
</box>
|
|
486
130
|
|
|
487
|
-
<
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
{plotContent}
|
|
496
|
-
</box>
|
|
497
|
-
<box width={1} />
|
|
498
|
-
<box width={AXIS_WIDTH} flexDirection="column" height={chartHeight}>
|
|
499
|
-
{Array.from({ length: chartHeight }, (_, index) => (
|
|
500
|
-
<text key={index} fg={colors.textDim}>
|
|
501
|
-
{axisByRow.get(index) ?? ""}
|
|
502
|
-
</text>
|
|
503
|
-
))}
|
|
504
|
-
</box>
|
|
505
|
-
</box>
|
|
506
|
-
|
|
507
|
-
<box height={1}>
|
|
508
|
-
<text fg={colors.textDim}>{rendered.timeLabels}</text>
|
|
509
|
-
</box>
|
|
131
|
+
<StockChart
|
|
132
|
+
width={width}
|
|
133
|
+
height={chartHeight}
|
|
134
|
+
focused={false}
|
|
135
|
+
compact
|
|
136
|
+
historyOverride={pricePoints}
|
|
137
|
+
currencyOverride="USD"
|
|
138
|
+
/>
|
|
510
139
|
</box>
|
|
511
140
|
);
|
|
512
141
|
}
|
|
@@ -431,14 +431,18 @@ export function usePredictionMarketsDataState({
|
|
|
431
431
|
setTransportState("idle");
|
|
432
432
|
return;
|
|
433
433
|
}
|
|
434
|
-
const detailSummary = selectedSummaryRef.current
|
|
434
|
+
const detailSummary = selectedSummaryRef.current;
|
|
435
|
+
if (!detailSummary) {
|
|
436
|
+
setTransportState("idle");
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
435
439
|
const delayMs = detailLoadDelayRef.current;
|
|
436
440
|
detailLoadDelayRef.current = 0;
|
|
437
441
|
const timeoutId = setTimeout(() => {
|
|
438
442
|
void loadSelectedDetail(detailSummary);
|
|
439
443
|
}, delayMs);
|
|
440
444
|
return () => clearTimeout(timeoutId);
|
|
441
|
-
}, [loadSelectedDetail, selectedSummaryKey
|
|
445
|
+
}, [loadSelectedDetail, selectedSummaryKey]);
|
|
442
446
|
|
|
443
447
|
useEffect(() => {
|
|
444
448
|
if (!focused || selectedSummaryVenue !== "kalshi" || !selectedSummaryKey) {
|
|
@@ -27,11 +27,10 @@ export function PredictionMarketOutcomesView({
|
|
|
27
27
|
|
|
28
28
|
return (
|
|
29
29
|
<box flexDirection="column">
|
|
30
|
-
<box
|
|
30
|
+
<box height={1}>
|
|
31
31
|
<text fg={colors.textBright} attributes={TextAttributes.BOLD}>
|
|
32
32
|
Outcomes
|
|
33
33
|
</text>
|
|
34
|
-
<text fg={colors.textDim}>Ranked by implied YES probability.</text>
|
|
35
34
|
</box>
|
|
36
35
|
|
|
37
36
|
<box flexDirection="row" height={1}>
|
|
@@ -64,10 +63,7 @@ export function PredictionMarketOutcomesView({
|
|
|
64
63
|
fg={selected ? colors.selectedText : colors.text}
|
|
65
64
|
attributes={selected ? TextAttributes.BOLD : 0}
|
|
66
65
|
>
|
|
67
|
-
{padTo(
|
|
68
|
-
`${index === 0 ? "TOP " : ""}${market.marketLabel}`,
|
|
69
|
-
labelWidth,
|
|
70
|
-
)}
|
|
66
|
+
{padTo(market.marketLabel, labelWidth)}
|
|
71
67
|
</text>
|
|
72
68
|
</box>
|
|
73
69
|
<box width={8}>
|
|
@@ -8,7 +8,6 @@ import { DETAIL_TABS } from "../navigation";
|
|
|
8
8
|
import {
|
|
9
9
|
formatPredictionEndsAt,
|
|
10
10
|
formatPredictionMetric,
|
|
11
|
-
formatPredictionPercent,
|
|
12
11
|
formatPredictionProbability,
|
|
13
12
|
formatPredictionSpread,
|
|
14
13
|
getPredictionProbabilityColor,
|
|
@@ -34,10 +33,6 @@ interface MetricCell {
|
|
|
34
33
|
color?: string;
|
|
35
34
|
}
|
|
36
35
|
|
|
37
|
-
function formatVenueLabel(venue: PredictionMarketSummary["venue"]): string {
|
|
38
|
-
return venue === "polymarket" ? "Polymarket" : "Kalshi";
|
|
39
|
-
}
|
|
40
|
-
|
|
41
36
|
function MetricLabelRow({ metrics }: { metrics: MetricCell[] }) {
|
|
42
37
|
return (
|
|
43
38
|
<box flexDirection="row" height={1}>
|
|
@@ -114,14 +109,9 @@ export function PredictionMarketDetailPane({
|
|
|
114
109
|
const summaryMetrics = detail?.summary ?? selectedSummary;
|
|
115
110
|
const detailTitle =
|
|
116
111
|
selectedRow?.kind === "group" ? selectedRow.title : summaryMetrics.title;
|
|
117
|
-
const
|
|
118
|
-
selectedSummary.marketLabel,
|
|
119
|
-
Math.max(detailWidth - 18, 12),
|
|
120
|
-
);
|
|
121
|
-
const detailSubtitleParts = [formatVenueLabel(summaryMetrics.venue)];
|
|
112
|
+
const detailSubtitleParts: string[] = [];
|
|
122
113
|
if (selectedRow?.kind === "group") {
|
|
123
114
|
if (summaryMetrics.category) detailSubtitleParts.push(summaryMetrics.category);
|
|
124
|
-
detailSubtitleParts.push(`${selectedRow.marketCount} targets`);
|
|
125
115
|
} else if (
|
|
126
116
|
summaryMetrics.eventLabel &&
|
|
127
117
|
summaryMetrics.eventLabel !== summaryMetrics.title
|
|
@@ -195,7 +185,7 @@ export function PredictionMarketDetailPane({
|
|
|
195
185
|
0,
|
|
196
186
|
Math.max(Math.min(Math.floor((detailWidth - 10) / 18), 3), 0),
|
|
197
187
|
) ?? []);
|
|
198
|
-
const headerHeight =
|
|
188
|
+
const headerHeight = detailSubtitle.length > 0 ? 3 : 2;
|
|
199
189
|
const detailLoading = detailLoadCount > 0 && !detail;
|
|
200
190
|
const titleColor = focused ? colors.textBright : colors.text;
|
|
201
191
|
|
|
@@ -207,20 +197,9 @@ export function PredictionMarketDetailPane({
|
|
|
207
197
|
{detailTitle}
|
|
208
198
|
</text>
|
|
209
199
|
</box>
|
|
210
|
-
|
|
211
|
-
<text fg={colors.textDim}>{detailSubtitle}</text>
|
|
212
|
-
</box>
|
|
213
|
-
{selectedRow?.kind === "group" && (
|
|
200
|
+
{detailSubtitle.length > 0 && (
|
|
214
201
|
<box flexDirection="row" height={1}>
|
|
215
|
-
<text fg={colors.textDim}>
|
|
216
|
-
<box width={1} />
|
|
217
|
-
<text fg={colors.textBright} attributes={TextAttributes.BOLD}>
|
|
218
|
-
{selectedLabel}
|
|
219
|
-
</text>
|
|
220
|
-
<box width={1} />
|
|
221
|
-
<text fg={getPredictionProbabilityColor(selectedSummary.yesPrice)}>
|
|
222
|
-
{formatPredictionPercent(selectedSummary.yesPrice)}
|
|
223
|
-
</text>
|
|
202
|
+
<text fg={colors.textDim}>{detailSubtitle}</text>
|
|
224
203
|
</box>
|
|
225
204
|
)}
|
|
226
205
|
</box>
|
|
@@ -20,7 +20,6 @@ export function SummaryLink({
|
|
|
20
20
|
}) {
|
|
21
21
|
return (
|
|
22
22
|
<box height={1} onMouseDown={() => openUrl(url)}>
|
|
23
|
-
<text fg={colors.textDim}>{"Venue: "}</text>
|
|
24
23
|
<text fg={colors.textBright} attributes={TextAttributes.UNDERLINE}>
|
|
25
24
|
{truncatePredictionText(url, maxLength)}
|
|
26
25
|
</text>
|
|
@@ -43,5 +43,9 @@ describe("prediction markets detail views", () => {
|
|
|
43
43
|
expect(frame).toContain("Outcomes");
|
|
44
44
|
expect(frame).toContain("Above 4.25%");
|
|
45
45
|
expect(frame).toContain("Above 4.50%");
|
|
46
|
+
expect(frame).not.toContain("Ranked by implied YES probability.");
|
|
47
|
+
expect(frame).not.toContain("TOP Above 4.25%");
|
|
48
|
+
expect(frame).not.toContain("Kalshi");
|
|
49
|
+
expect(frame).not.toContain("targets");
|
|
46
50
|
});
|
|
47
51
|
});
|