gloomberb 0.15.7 → 0.15.8
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 +1 -0
- package/package.json +1 -1
- package/src/api-client/data.ts +5 -0
- package/src/api-client/doe.ts +75 -0
- package/src/api-client/index.ts +1 -0
- package/src/api-client/paths.ts +3 -0
- package/src/api-client/types.ts +2 -0
- package/src/cli/pane-functions/function-help.ts +15 -0
- package/src/components/chart/composite/composite-chart.tsx +15 -2
- package/src/components/chart/composite/format.ts +24 -11
- package/src/components/chart/composite/rasterizer.ts +67 -16
- package/src/components/chart/composite/scene.ts +53 -2
- package/src/components/chart/composite/session-shading.ts +3 -2
- package/src/components/chart/composite/text-renderer.ts +52 -1
- package/src/components/chart/composite/time-axis.ts +100 -30
- package/src/components/chart/composite/tools.ts +2 -1
- package/src/components/chart/composite/types.ts +4 -0
- package/src/components/command-bar/workflow/collection-actions.ts +9 -7
- package/src/components/layout/shell/index.tsx +38 -28
- package/src/components/onboarding/portfolio-step/positions-panel.tsx +14 -5
- package/src/components/onboarding/wizard-model.ts +13 -5
- package/src/components/onboarding/wizard-positions.ts +56 -7
- package/src/components/ui/display.tsx +6 -3
- package/src/components/ui/fields.tsx +5 -0
- package/src/components/ui/header-case.ts +25 -3
- package/src/components/ui/query-bar.tsx +8 -1
- package/src/i18n/es.ts +1 -0
- package/src/i18n/ja.ts +1 -0
- package/src/i18n/ko.ts +1 -0
- package/src/i18n/zh-cn.ts +1 -0
- package/src/i18n/zh-tw.ts +1 -0
- package/src/market-data/market/trading-sessions.ts +65 -1
- package/src/plugins/builtin/chart-composer/cli-options.ts +4 -0
- package/src/plugins/builtin/chart-composer/headless-schema.ts +6 -0
- package/src/plugins/builtin/chart-composer/pane.tsx +28 -1
- package/src/plugins/builtin/composite-plugins.ts +2 -0
- package/src/plugins/builtin/doe/client.ts +73 -0
- package/src/plugins/builtin/doe/headless.ts +93 -0
- package/src/plugins/builtin/doe/index.tsx +41 -0
- package/src/plugins/builtin/doe/model.ts +215 -0
- package/src/plugins/builtin/doe/pane.tsx +148 -0
- package/src/plugins/builtin/doe/range-bar.tsx +45 -0
- package/src/plugins/builtin/earnings-calls/transcript-view.tsx +7 -7
- package/src/plugins/builtin/executives/pane.tsx +4 -4
- package/src/plugins/builtin/fear-greed/charts.tsx +1 -1
- package/src/plugins/builtin/holders/pane.tsx +1 -1
- package/src/plugins/builtin/kelly-sizer/pane.tsx +1 -1
- package/src/plugins/builtin/options/view.tsx +1 -1
- package/src/plugins/builtin/portfolio-list/cli/render.ts +1 -1
- package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +6 -20
- package/src/plugins/builtin/portfolio-list/command-bar.ts +6 -2
- package/src/plugins/builtin/portfolio-list/mutations.ts +53 -1
- package/src/plugins/builtin/research/equity-diagnostic-pane.tsx +6 -6
- package/src/plugins/builtin/risk-factors/pane.tsx +4 -4
- package/src/plugins/builtin/shared/ticker-request.ts +1 -1
- package/src/plugins/builtin/ticker-detail/financials/ratios.ts +42 -10
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +1 -1
- package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +1 -1
- package/src/plugins/catalog-browser.ts +2 -0
- package/src/sources/gloomberb-cloud/index.ts +47 -13
- package/src/sources/history-result.ts +1 -0
- package/src/sources/history-retention.ts +16 -0
- package/src/sources/provider-router/history.ts +4 -2
- package/src/sync/core-contributors.ts +18 -2
- package/src/tickers/open-target.ts +1 -1
- package/src/tickers/search/index.ts +44 -11
- package/src/tickers/search/upsert.ts +4 -1
- package/src/time-series/live-bars.ts +8 -3
- package/src/time-series/resolve.ts +224 -17
- package/src/time-series/spec.ts +1 -0
- package/src/time-series/types.ts +9 -1
- package/src/types/data-provider.ts +2 -0
- package/src/types/price-history.ts +2 -0
- package/src/utils/exchanges.ts +1 -0
- package/src/utils/price-history.ts +35 -1
- package/src/utils/sec.ts +2 -1
- package/src/utils/zoned-date-time.ts +67 -0
package/README.md
CHANGED
|
@@ -76,6 +76,7 @@ Press `Ctrl+P` to open the command bar, or press `` ` `` to search for a ticker.
|
|
|
76
76
|
| `HVG AAPL` / `HVT AAPL` | Realized volatility and volatility cones |
|
|
77
77
|
| `SEAS AAPL` | Seasonality: monthly returns by year and year overlays |
|
|
78
78
|
| `COT [code or root]` | CFTC positioning and cross-market extremes |
|
|
79
|
+
| `DOE` / `NGS` | EIA weekly oil stocks and gas storage, with builds, draws and five-year ranges |
|
|
79
80
|
| `TOP` | Market stories |
|
|
80
81
|
| `WIRP` / `FFIP` | US rate path and conditional FOMC probabilities |
|
|
81
82
|
| `BTMM` | Funding rates, bill curves and Federal Reserve liquidity |
|
package/package.json
CHANGED
package/src/api-client/data.ts
CHANGED
|
@@ -14,6 +14,7 @@ import type { ShortVolumePayload, ShortVolumeScope } from "./short-volume";
|
|
|
14
14
|
import type { SocialMentionDayPosts, SocialMentionsPayload, SocialMentionsRange } from "./social-mentions";
|
|
15
15
|
import type { FuturesCurveAsOfPayload, FuturesCurvePayload } from "./futures-curve";
|
|
16
16
|
import type { CotBoardPayload, CotContractPayload, CotFamily, CotClass } from "./cot";
|
|
17
|
+
import type { DoeBoardPayload } from "./doe";
|
|
17
18
|
import type { TapeSnapshot } from "./tape";
|
|
18
19
|
import type { ExchangeRateSnapshot } from "../types/exchange-rate";
|
|
19
20
|
import type { RatePathPayload } from "./rates";
|
|
@@ -385,6 +386,10 @@ export class CloudDataApi {
|
|
|
385
386
|
return this.request<CotContractPayload>(`/cloud/cot/contracts/${encodeURIComponent(code)}?${new URLSearchParams({ report })}`);
|
|
386
387
|
}
|
|
387
388
|
|
|
389
|
+
async getCloudDoeBoard(): Promise<DoeBoardPayload> {
|
|
390
|
+
return this.request<DoeBoardPayload>("/cloud/doe/board");
|
|
391
|
+
}
|
|
392
|
+
|
|
388
393
|
async getCloudTape(symbol: string, exchange: string, signal?: AbortSignal): Promise<TapeSnapshot> {
|
|
389
394
|
return this.request<TapeSnapshot>(`/cloud/tape/${encodeURIComponent(symbol)}?exchange=${encodeURIComponent(exchange)}`, { signal: signal ?? AbortSignal.timeout(30_000) });
|
|
390
395
|
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/** EIA weekly petroleum and natural gas storage, as GET /cloud/doe/board serves it. */
|
|
2
|
+
export type DoeReport = "petroleum" | "gas";
|
|
3
|
+
export type DoeTab = "crude" | "products" | "gas";
|
|
4
|
+
/** Thousand barrels, thousand barrels a day, percent, billion cubic feet: as published. */
|
|
5
|
+
export type DoeUnit = "kb" | "kbd" | "pct" | "bcf";
|
|
6
|
+
|
|
7
|
+
interface DoeFiveYear {
|
|
8
|
+
/** Calendar years in the band, oldest first; fewer than five where history is short. */
|
|
9
|
+
years: number[];
|
|
10
|
+
min: number;
|
|
11
|
+
max: number;
|
|
12
|
+
average: number;
|
|
13
|
+
/** The average change into this week over the same years. */
|
|
14
|
+
averageChange: number | null;
|
|
15
|
+
vsAverage: number;
|
|
16
|
+
vsAveragePercent: number | null;
|
|
17
|
+
/** 0 at the five-year low, 100 at the high, beyond either outside the range. */
|
|
18
|
+
position: number | null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface DoeYearAgo {
|
|
22
|
+
weekEnding: string;
|
|
23
|
+
value: number;
|
|
24
|
+
change: number;
|
|
25
|
+
changePercent: number | null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface DoeSeasonal {
|
|
29
|
+
/** The calendar year of the latest week. */
|
|
30
|
+
year: number;
|
|
31
|
+
/** Fridays in that year: 52 or 53. */
|
|
32
|
+
weeks: number;
|
|
33
|
+
/** [week of year, value]. */
|
|
34
|
+
current: Array<[number, number]>;
|
|
35
|
+
previous: Array<[number, number]>;
|
|
36
|
+
/** [week, min, average, max] of the band years on that week's day. */
|
|
37
|
+
band: Array<[number, number | null, number | null, number | null]>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface DoeSeriesRow {
|
|
41
|
+
id: string;
|
|
42
|
+
key: string;
|
|
43
|
+
report: DoeReport;
|
|
44
|
+
tab: DoeTab;
|
|
45
|
+
label: string;
|
|
46
|
+
unit: DoeUnit;
|
|
47
|
+
weekEnding: string | null;
|
|
48
|
+
value: number | null;
|
|
49
|
+
previousWeekEnding: string | null;
|
|
50
|
+
weekChange: number | null;
|
|
51
|
+
yearAgo: DoeYearAgo | null;
|
|
52
|
+
fiveYear: DoeFiveYear | null;
|
|
53
|
+
/** One plain read worked out from the numbers. */
|
|
54
|
+
read: string | null;
|
|
55
|
+
seasonal: DoeSeasonal | null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface DoeReportStatus {
|
|
59
|
+
id: DoeReport;
|
|
60
|
+
weekEnding: string | null;
|
|
61
|
+
releasedAt: string | null;
|
|
62
|
+
releaseHoliday: string | null;
|
|
63
|
+
nextWeekEnding: string | null;
|
|
64
|
+
nextReleaseAt: string | null;
|
|
65
|
+
nextReleaseHoliday: string | null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface DoeBoardPayload {
|
|
69
|
+
source: "EIA";
|
|
70
|
+
generatedAt: string;
|
|
71
|
+
status: "available" | "partial" | "unavailable";
|
|
72
|
+
gaps: string[];
|
|
73
|
+
reports: DoeReportStatus[];
|
|
74
|
+
series: DoeSeriesRow[];
|
|
75
|
+
}
|
package/src/api-client/index.ts
CHANGED
|
@@ -721,6 +721,7 @@ class GloomApiClient {
|
|
|
721
721
|
getCloudShiller = this.data.getCloudShiller.bind(this.data);
|
|
722
722
|
getCloudCotBoard = this.data.getCloudCotBoard.bind(this.data);
|
|
723
723
|
getCloudCotContract = this.data.getCloudCotContract.bind(this.data);
|
|
724
|
+
getCloudDoeBoard = this.data.getCloudDoeBoard.bind(this.data);
|
|
724
725
|
getCloudTape = this.data.getCloudTape.bind(this.data);
|
|
725
726
|
getCloudYieldCurve = this.data.getCloudYieldCurve.bind(this.data);
|
|
726
727
|
getCloudCurve = this.data.getCloudCurve.bind(this.data);
|
package/src/api-client/paths.ts
CHANGED
|
@@ -13,6 +13,8 @@ export type CloudHistoryParams = {
|
|
|
13
13
|
endDate?: string;
|
|
14
14
|
rangeKey?: string;
|
|
15
15
|
historyRecovery?: HistoryRetention;
|
|
16
|
+
/** Ask for pre-market and after-hours bars too. */
|
|
17
|
+
session?: "extended";
|
|
16
18
|
};
|
|
17
19
|
|
|
18
20
|
export type CloudFredSeriesParams = {
|
|
@@ -172,6 +174,7 @@ export function cloudHistoryPath(
|
|
|
172
174
|
if (params.endDate) search.set("endDate", params.endDate);
|
|
173
175
|
if (params.rangeKey) search.set("rangeKey", params.rangeKey);
|
|
174
176
|
if (params.historyRecovery) search.set("historyRecovery", JSON.stringify(params.historyRecovery));
|
|
177
|
+
if (params.session) search.set("session", params.session);
|
|
175
178
|
return appendQuery("/market/history", search);
|
|
176
179
|
}
|
|
177
180
|
|
package/src/api-client/types.ts
CHANGED
|
@@ -1293,6 +1293,8 @@ export interface CloudMarketResponse<T> {
|
|
|
1293
1293
|
historyRetention?: unknown;
|
|
1294
1294
|
/** Source-declared history semantics; validated separately from quote metadata. */
|
|
1295
1295
|
historySession?: unknown;
|
|
1296
|
+
/** "extended" when the history includes the pre-market and after-hours bars asked for. */
|
|
1297
|
+
session?: unknown;
|
|
1296
1298
|
/** Untrusted until the history adapter validates its boundary dates. */
|
|
1297
1299
|
coverage?: CloudHistoryCoverage;
|
|
1298
1300
|
providerMeta?: {
|
|
@@ -582,6 +582,21 @@ export const FUNCTION_HELP: Readonly<Record<string, FunctionHelp>> = {
|
|
|
582
582
|
data: same("Weekly: Tuesday positions, out on Friday"),
|
|
583
583
|
bloomberg: ["COT"],
|
|
584
584
|
},
|
|
585
|
+
DOE: {
|
|
586
|
+
summary: "The EIA's weekly petroleum and natural gas storage numbers: crude, Cushing and the SPR, gasoline, distillates, jet fuel and propane, refinery runs, trade and demand, and gas storage by region. Each row shows the week's build or draw, the change on a year ago and where it sits in its five-year range; the selected one is charted over the year against the five-year band with a plain read of the numbers.",
|
|
587
|
+
usage: ["DOE", "NGS"],
|
|
588
|
+
keys: [],
|
|
589
|
+
data: same("Weekly, on release"),
|
|
590
|
+
bloomberg: ["DOE"],
|
|
591
|
+
},
|
|
592
|
+
NGS: {
|
|
593
|
+
summary: "Lower 48 working gas in storage and each region's, the week's injection or withdrawal against a year ago and the five-year average, charted over the year. The Gas Storage tab of DOE.",
|
|
594
|
+
usage: ["NGS"],
|
|
595
|
+
keys: [],
|
|
596
|
+
data: same("Weekly, on release"),
|
|
597
|
+
bloomberg: [],
|
|
598
|
+
docs: "DOE",
|
|
599
|
+
},
|
|
585
600
|
VIX: {
|
|
586
601
|
summary: "Daily VIX tenor closes, the term structure and whether it is in contango or inverted, with the cross-asset volatility board beside it.",
|
|
587
602
|
usage: ["VIX"],
|
|
@@ -1159,6 +1159,17 @@ function CompositePanelSurface({
|
|
|
1159
1159
|
color: marker.color,
|
|
1160
1160
|
};
|
|
1161
1161
|
}, [formatAxisValue, panel]);
|
|
1162
|
+
const priorCloseMarker = useMemo(() => {
|
|
1163
|
+
const marker = panel.priorClose;
|
|
1164
|
+
const domain = marker ? panel.axes[marker.axis] : undefined;
|
|
1165
|
+
if (!marker || !domain) return null;
|
|
1166
|
+
return {
|
|
1167
|
+
side: marker.axis,
|
|
1168
|
+
yRatio: marker.yRatio,
|
|
1169
|
+
label: formatAxisValue ? formatAxisValue(marker.value, domain) : formatCompositeCursorValue(marker.value, domain),
|
|
1170
|
+
color: colors.textDim,
|
|
1171
|
+
};
|
|
1172
|
+
}, [colors.textDim, formatAxisValue, panel]);
|
|
1162
1173
|
const levelMarkers = useMemo(() => levelDomain ? [...projectedLevels]
|
|
1163
1174
|
// The picked level's label first, then alerts', when they compete for room.
|
|
1164
1175
|
.sort((left, right) => Number(right.level.id === selectedLevelId) - Number(left.level.id === selectedLevelId)
|
|
@@ -1181,7 +1192,8 @@ function CompositePanelSurface({
|
|
|
1181
1192
|
!!marker && (marker.side === null || marker.side === side)
|
|
1182
1193
|
);
|
|
1183
1194
|
const fixed = [lastPriceMarker, axisMarkers].filter(onSide).map(toAxisMarker);
|
|
1184
|
-
|
|
1195
|
+
// The prior close's label yields to every level's.
|
|
1196
|
+
const levelLabels = [...levelMarkers, priorCloseMarker].filter(onSide).map(toAxisMarker);
|
|
1185
1197
|
// A label is a row tall, so close levels would stack into an unreadable
|
|
1186
1198
|
// pile and hide the last price: each keeps its line, and only the labels
|
|
1187
1199
|
// with a row to themselves show. Desktop badges sit between rows.
|
|
@@ -2885,7 +2897,7 @@ export function CompositeChart({
|
|
|
2885
2897
|
const timeAxisCursorLabel = xAxis?.formatCursor && scene.cursorXRatio !== null
|
|
2886
2898
|
? xAxis.formatCursor(scene.cursorXRatio)
|
|
2887
2899
|
: scene.cursorDate
|
|
2888
|
-
? formatCompositeTimeAxisDate(scene.cursorDate, scene.startTime, scene.endTime)
|
|
2900
|
+
? formatCompositeTimeAxisDate(scene.cursorDate, scene.startTime, scene.endTime, scene.timeZone)
|
|
2889
2901
|
: null;
|
|
2890
2902
|
// The crosshair labels the moving end, so the axis only adds the anchor.
|
|
2891
2903
|
const timeAxisMarkers = toolSpan
|
|
@@ -2895,6 +2907,7 @@ export function CompositeChart({
|
|
|
2895
2907
|
new Date(toolSpan.startTime),
|
|
2896
2908
|
scene.startTime,
|
|
2897
2909
|
scene.endTime,
|
|
2910
|
+
scene.timeZone,
|
|
2898
2911
|
),
|
|
2899
2912
|
color: toolSpan.color,
|
|
2900
2913
|
}]
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { formatMarketPrice, formatMarketPriceWithCurrency, stablePriceFractionDigits, type MarketFormatOptions } from "../../../market-data/market/format";
|
|
2
2
|
import type { ResolvedSeries, TimeSeriesPoint } from "../../../time-series/types";
|
|
3
3
|
import type { CompositeAxisDomain, CompositePanelScene } from "./types";
|
|
4
|
+
import { timeZoneLabel, zonedWallClockMs } from "../../../utils/zoned-date-time";
|
|
4
5
|
|
|
5
6
|
const CURRENCY_SYMBOLS: Record<string, string> = {
|
|
6
7
|
USD: "$",
|
|
@@ -411,17 +412,28 @@ function utcTime(date: Date): string {
|
|
|
411
412
|
return date.toISOString().slice(11, 16);
|
|
412
413
|
}
|
|
413
414
|
|
|
415
|
+
/** The date's wall-clock reading in `timeZone` (UTC when absent), and the zone's short name. */
|
|
416
|
+
function onClock(date: Date, timeZone: string | undefined): { wall: Date; zone: string } {
|
|
417
|
+
if (!timeZone || timeZone === "UTC") return { wall: date, zone: "UTC" };
|
|
418
|
+
try {
|
|
419
|
+
return { wall: new Date(zonedWallClockMs(date.getTime(), timeZone)), zone: timeZoneLabel(timeZone) };
|
|
420
|
+
} catch {
|
|
421
|
+
return { wall: date, zone: "UTC" };
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
414
425
|
function isIntradaySpan(startTime: number, endTime: number): boolean {
|
|
415
426
|
return Number.isFinite(startTime)
|
|
416
427
|
&& Number.isFinite(endTime)
|
|
417
428
|
&& Math.abs(endTime - startTime) <= INTRADAY_SPAN_MAX_MS;
|
|
418
429
|
}
|
|
419
430
|
|
|
420
|
-
/** Shared-cursor timestamp
|
|
421
|
-
export function formatCompositeCursorDate(date: Date, startTime: number, endTime: number): string {
|
|
431
|
+
/** Shared-cursor timestamp on the chart's clock: the venue's for intraday bars, else UTC. */
|
|
432
|
+
export function formatCompositeCursorDate(date: Date, startTime: number, endTime: number, timeZone?: string): string {
|
|
433
|
+
const { wall, zone } = onClock(date, timeZone);
|
|
422
434
|
return isIntradaySpan(startTime, endTime)
|
|
423
|
-
? `${utcDate(
|
|
424
|
-
: utcDate(
|
|
435
|
+
? `${utcDate(wall)} ${utcTime(wall)} ${zone}`
|
|
436
|
+
: utcDate(wall);
|
|
425
437
|
}
|
|
426
438
|
|
|
427
439
|
function validUtcTimestamp(date: Date | undefined): string | null {
|
|
@@ -462,12 +474,13 @@ export function formatCompositePointDetails(point: TimeSeriesPoint | null | unde
|
|
|
462
474
|
return details.join(" · ");
|
|
463
475
|
}
|
|
464
476
|
|
|
465
|
-
/** Compact
|
|
466
|
-
export function formatCompositeTimeAxisDate(date: Date, startTime: number, endTime: number): string {
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
const
|
|
477
|
+
/** Compact tick label on the chart's clock, selected from the full visible chart span. */
|
|
478
|
+
export function formatCompositeTimeAxisDate(date: Date, startTime: number, endTime: number, timeZone?: string): string {
|
|
479
|
+
const { wall, zone } = onClock(date, timeZone);
|
|
480
|
+
if (!isIntradaySpan(startTime, endTime)) return utcDate(wall);
|
|
481
|
+
const startDate = utcDate(onClock(new Date(startTime), timeZone).wall);
|
|
482
|
+
const endDate = utcDate(onClock(new Date(endTime), timeZone).wall);
|
|
470
483
|
return startDate === endDate
|
|
471
|
-
? `${utcTime(
|
|
472
|
-
: `${utcDate(
|
|
484
|
+
? `${utcTime(wall)} ${zone}`
|
|
485
|
+
: `${utcDate(wall).slice(5)} ${utcTime(wall)} ${zone}`;
|
|
473
486
|
}
|
|
@@ -39,6 +39,9 @@ interface RenderCompositePanelBitmapOptions {
|
|
|
39
39
|
const LAST_PRICE_DASH_PIXELS = 6;
|
|
40
40
|
const LAST_PRICE_GAP_PIXELS = 5;
|
|
41
41
|
const LAST_PRICE_OPACITY = 0.9;
|
|
42
|
+
const PRIOR_CLOSE_DOT_PIXELS = 2;
|
|
43
|
+
const PRIOR_CLOSE_GAP_PIXELS = 3;
|
|
44
|
+
const PRIOR_CLOSE_OPACITY = 0.7;
|
|
42
45
|
|
|
43
46
|
function pixelPoint(point: CompositeProjectedPoint, width: number, height: number): { x: number; y: number } {
|
|
44
47
|
return {
|
|
@@ -118,6 +121,53 @@ function fillAreaGradient(
|
|
|
118
121
|
}
|
|
119
122
|
}
|
|
120
123
|
|
|
124
|
+
const BAND_FILL_OPACITY = 0.2;
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* The range a band series spans, one flat fill per pixel column between its
|
|
128
|
+
* points' `low` and `high`, read straight across between neighbouring points.
|
|
129
|
+
*/
|
|
130
|
+
function fillBand(
|
|
131
|
+
data: Uint8Array,
|
|
132
|
+
width: number,
|
|
133
|
+
height: number,
|
|
134
|
+
series: CompositeProjectedSeries,
|
|
135
|
+
domain: CompositeAxisDomain,
|
|
136
|
+
color: RgbaColor,
|
|
137
|
+
): void {
|
|
138
|
+
const edges = series.points.map((projected) => {
|
|
139
|
+
const { high, low } = projected.point;
|
|
140
|
+
const x = pixelPoint(projected, width, height).x;
|
|
141
|
+
if (high == null || low == null || !Number.isFinite(high) || !Number.isFinite(low)) return null;
|
|
142
|
+
const top = pixelY(high, domain, height);
|
|
143
|
+
const bottom = pixelY(low, domain, height);
|
|
144
|
+
return top === null || bottom === null ? null : { x, top: Math.min(top, bottom), bottom: Math.max(top, bottom) };
|
|
145
|
+
});
|
|
146
|
+
const filled = new Uint8Array(width);
|
|
147
|
+
const fillColumn = (x: number, top: number, bottom: number) => {
|
|
148
|
+
if (x < 0 || x >= width || filled[x]) return;
|
|
149
|
+
filled[x] = 1;
|
|
150
|
+
for (let y = Math.max(0, Math.round(top)); y <= Math.min(height - 1, Math.round(bottom)); y += 1) {
|
|
151
|
+
blendPixel(data, width, height, x, y, color, BAND_FILL_OPACITY);
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
for (let index = 0; index < edges.length; index += 1) {
|
|
155
|
+
const current = edges[index];
|
|
156
|
+
if (!current) continue;
|
|
157
|
+
const previous = index > 0 && !series.points[index]?.breakBefore ? edges[index - 1] : null;
|
|
158
|
+
if (!previous) {
|
|
159
|
+
fillColumn(Math.round(current.x), current.top, current.bottom);
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
const start = Math.round(previous.x);
|
|
163
|
+
const span = Math.max(1, Math.round(current.x) - start);
|
|
164
|
+
for (let x = start; x <= start + span; x += 1) {
|
|
165
|
+
const t = (x - start) / span;
|
|
166
|
+
fillColumn(x, previous.top + (current.top - previous.top) * t, previous.bottom + (current.bottom - previous.bottom) * t);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
121
171
|
function drawConnectedSeries(
|
|
122
172
|
data: Uint8Array,
|
|
123
173
|
width: number,
|
|
@@ -343,27 +393,19 @@ function drawOhlc(
|
|
|
343
393
|
}
|
|
344
394
|
}
|
|
345
395
|
|
|
346
|
-
function
|
|
396
|
+
function drawDashedLevel(
|
|
347
397
|
data: Uint8Array,
|
|
348
398
|
width: number,
|
|
349
399
|
height: number,
|
|
350
400
|
yRatio: number,
|
|
351
401
|
color: RgbaColor,
|
|
402
|
+
dash = LAST_PRICE_DASH_PIXELS,
|
|
403
|
+
gap = LAST_PRICE_GAP_PIXELS,
|
|
404
|
+
opacity = LAST_PRICE_OPACITY,
|
|
352
405
|
): void {
|
|
353
406
|
const y = Math.round(clamp(yRatio * Math.max(height - 1, 0), 0, Math.max(height - 1, 0)));
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
fillRect(
|
|
357
|
-
data,
|
|
358
|
-
width,
|
|
359
|
-
height,
|
|
360
|
-
x,
|
|
361
|
-
y,
|
|
362
|
-
Math.min(x + LAST_PRICE_DASH_PIXELS - 1, width - 1),
|
|
363
|
-
y,
|
|
364
|
-
color,
|
|
365
|
-
LAST_PRICE_OPACITY,
|
|
366
|
-
);
|
|
407
|
+
for (let x = 0; x < width; x += dash + gap) {
|
|
408
|
+
fillRect(data, width, height, x, y, Math.min(x + dash - 1, width - 1), y, color, opacity);
|
|
367
409
|
}
|
|
368
410
|
}
|
|
369
411
|
|
|
@@ -388,9 +430,14 @@ export function renderCompositePanelBitmap(
|
|
|
388
430
|
fillRect(data, width, height, 0, y, width - 1, y + 0.6, grid, 0.42);
|
|
389
431
|
}
|
|
390
432
|
if (panel.volumeProfile) paintVolumeProfile(data, width, height, panel.volumeProfile);
|
|
433
|
+
// Under the bars, which read across it.
|
|
434
|
+
if (panel.priorClose) {
|
|
435
|
+
drawDashedLevel(data, width, height, panel.priorClose.yRatio, parseHex(options.colors.textDim),
|
|
436
|
+
PRIOR_CLOSE_DOT_PIXELS, PRIOR_CLOSE_GAP_PIXELS, PRIOR_CLOSE_OPACITY);
|
|
437
|
+
}
|
|
391
438
|
|
|
392
439
|
const ordered = [...panel.series].sort((left, right) => {
|
|
393
|
-
const rank = (style: string) => style === "area" || style === "columns" ? 0 : 1;
|
|
440
|
+
const rank = (style: string) => style === "area" || style === "columns" || style === "band" ? 0 : 1;
|
|
394
441
|
return rank(left.source.style) - rank(right.source.style);
|
|
395
442
|
});
|
|
396
443
|
const columnLayout = buildCompositeColumnLayout(panel);
|
|
@@ -430,6 +477,10 @@ export function renderCompositePanelBitmap(
|
|
|
430
477
|
case "area":
|
|
431
478
|
drawConnectedSeries(data, width, height, series, domain, color, true);
|
|
432
479
|
break;
|
|
480
|
+
case "band":
|
|
481
|
+
fillBand(data, width, height, series, domain, color);
|
|
482
|
+
drawConnectedSeries(data, width, height, series, domain, color, false);
|
|
483
|
+
break;
|
|
433
484
|
case "points":
|
|
434
485
|
for (const point of series.points) {
|
|
435
486
|
const projected = pixelPoint(point, width, height);
|
|
@@ -449,7 +500,7 @@ export function renderCompositePanelBitmap(
|
|
|
449
500
|
}
|
|
450
501
|
|
|
451
502
|
if (panel.lastPrice) {
|
|
452
|
-
|
|
503
|
+
drawDashedLevel(data, width, height, panel.lastPrice.yRatio, parseHex(panel.lastPrice.color));
|
|
453
504
|
}
|
|
454
505
|
|
|
455
506
|
return { width, height, pixels: data };
|
|
@@ -24,7 +24,8 @@ import { compositeAxisMaxTicks, seriesPriceReference } from "./format";
|
|
|
24
24
|
import type { CompositeTimeScale } from "./types";
|
|
25
25
|
import { isFiniteNumber } from "../../../utils/guards";
|
|
26
26
|
import { volumeProfile } from "../../../time-series/trader-studies";
|
|
27
|
-
import { extendedHoursSpans } from "./session-shading";
|
|
27
|
+
import { extendedHoursSpans, isIntradaySeries } from "./session-shading";
|
|
28
|
+
import { listingTimeZone } from "../../../market-data/market/trading-sessions";
|
|
28
29
|
|
|
29
30
|
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
30
31
|
|
|
@@ -247,6 +248,10 @@ function seriesDomainValues(series: ResolvedSeries): number[] {
|
|
|
247
248
|
for (const candidate of [point.open, point.high, point.low, point.close]) {
|
|
248
249
|
if (isFiniteNumber(candidate)) values.push(candidate);
|
|
249
250
|
}
|
|
251
|
+
} else if (series.style === "band") {
|
|
252
|
+
for (const candidate of [point.high, point.low]) {
|
|
253
|
+
if (isFiniteNumber(candidate)) values.push(candidate);
|
|
254
|
+
}
|
|
250
255
|
}
|
|
251
256
|
}
|
|
252
257
|
if (series.style === "columns") values.push(0);
|
|
@@ -300,6 +305,24 @@ function axisPriceReferences(axisSeries: ResolvedSeries[]): Record<string, numbe
|
|
|
300
305
|
return references;
|
|
301
306
|
}
|
|
302
307
|
|
|
308
|
+
/**
|
|
309
|
+
* A one-day chart's previous close joins the axis when it lies within one
|
|
310
|
+
* session range of the bars, so a quiet session still shows where it opened
|
|
311
|
+
* from; a far gap leaves the bars their full height.
|
|
312
|
+
*/
|
|
313
|
+
function priorCloseInReach(series: readonly ResolvedSeries[], values: readonly number[]): number[] {
|
|
314
|
+
const reference = series.find((entry) => isFiniteNumber(entry.priorClose) && entry.points.length > 0)?.priorClose;
|
|
315
|
+
if (reference === undefined || values.length === 0) return [];
|
|
316
|
+
let low = Number.POSITIVE_INFINITY;
|
|
317
|
+
let high = Number.NEGATIVE_INFINITY;
|
|
318
|
+
for (const value of values) {
|
|
319
|
+
if (value < low) low = value;
|
|
320
|
+
if (value > high) high = value;
|
|
321
|
+
}
|
|
322
|
+
const span = high - low;
|
|
323
|
+
return reference >= low - span && reference <= high + span ? [reference] : [];
|
|
324
|
+
}
|
|
325
|
+
|
|
303
326
|
function buildAxisDomain(
|
|
304
327
|
side: CompositeAxisSide,
|
|
305
328
|
series: ResolvedSeries[],
|
|
@@ -308,7 +331,8 @@ function buildAxisDomain(
|
|
|
308
331
|
): CompositeAxisDomain | undefined {
|
|
309
332
|
const axisSeries = series.filter((entry) => entry.axis === side);
|
|
310
333
|
if (axisSeries.length === 0) return undefined;
|
|
311
|
-
const
|
|
334
|
+
const values = axisSeries.flatMap(seriesDomainValues);
|
|
335
|
+
const { min, max } = paddedDomain([...values, ...priorCloseInReach(axisSeries, values)], scale);
|
|
312
336
|
const first = axisSeries[0]!;
|
|
313
337
|
return {
|
|
314
338
|
side,
|
|
@@ -496,6 +520,30 @@ function attachLastPriceMarker(
|
|
|
496
520
|
};
|
|
497
521
|
}
|
|
498
522
|
|
|
523
|
+
/** Marks the previous session's close a one-day chart carries on its price series. */
|
|
524
|
+
function attachPriorClose(panels: CompositePanelScene[], series: readonly ResolvedSeries[]): void {
|
|
525
|
+
const entry = series.find((candidate) => isFiniteNumber(candidate.priorClose));
|
|
526
|
+
const panel = entry ? panels.find((candidate) => candidate.id === entry.panelId) : undefined;
|
|
527
|
+
const domain = entry ? panel?.axes[entry.axis] : undefined;
|
|
528
|
+
if (!entry || !panel || !domain || !panel.series.some((candidate) => candidate.source.id === entry.id)) return;
|
|
529
|
+
const yRatio = projectCompositeValue(entry.priorClose!, domain);
|
|
530
|
+
// Off the axis it marks nothing; the axis follows the session's own bars.
|
|
531
|
+
if (yRatio === null || yRatio < 0 || yRatio > 1) return;
|
|
532
|
+
panel.priorClose = { axis: entry.axis, value: entry.priorClose!, yRatio };
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* The zone intraday market bars read in: the venue's own clock, as the tape
|
|
537
|
+
* does. Daily and longer bars carry dates, which stay in UTC.
|
|
538
|
+
*/
|
|
539
|
+
function sceneTimeZone(anchor: ResolvedSeries | undefined, series: readonly ResolvedSeries[]): string | undefined {
|
|
540
|
+
const market = anchor ?? series.find((entry) => entry.observationKind === "market");
|
|
541
|
+
if (!market || !isIntradaySeries(market)) return undefined;
|
|
542
|
+
const zone = market.timeBasis?.timeZone
|
|
543
|
+
?? (market.listing ? listingTimeZone(market.listing.symbol, market.listing.exchange) : null);
|
|
544
|
+
return zone && zone !== "UTC" ? zone : undefined;
|
|
545
|
+
}
|
|
546
|
+
|
|
499
547
|
/**
|
|
500
548
|
* Profiles a volume-profile study over its bars in view and takes its points
|
|
501
549
|
* off the panel, so it draws as a histogram rather than a line or cursor dots.
|
|
@@ -754,6 +802,8 @@ export function buildCompositeChartScene(
|
|
|
754
802
|
});
|
|
755
803
|
panelScenes.forEach(attachVolumeProfile);
|
|
756
804
|
attachLastPriceMarker(panelScenes, usableSeries);
|
|
805
|
+
attachPriorClose(panelScenes, usableSeries);
|
|
806
|
+
const timeZone = sceneTimeZone(anchor, timelineSeries);
|
|
757
807
|
|
|
758
808
|
return {
|
|
759
809
|
width: Math.max(1, Math.floor(options.width)),
|
|
@@ -767,6 +817,7 @@ export function buildCompositeChartScene(
|
|
|
767
817
|
cursorDate,
|
|
768
818
|
cursorXRatio,
|
|
769
819
|
cursorValues: buildCursorValues(panelScenes, cursorDate, { startTime, endTime, timeScale }),
|
|
820
|
+
...(timeZone ? { timeZone } : {}),
|
|
770
821
|
};
|
|
771
822
|
}
|
|
772
823
|
|
|
@@ -21,7 +21,8 @@ function isRegular(exchange: string, time: number): boolean | null {
|
|
|
21
21
|
return regular;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
/** Whether a series holds intraday bars, by its cadence or its acquired resolution. */
|
|
25
|
+
export function isIntradaySeries(series: ResolvedSeries): boolean {
|
|
25
26
|
const cadence = series.timeBasis?.cadenceMs;
|
|
26
27
|
if (cadence !== undefined) return cadence < DAY_MS;
|
|
27
28
|
return !!series.historyResolution && isIntradayResolution(series.historyResolution);
|
|
@@ -39,7 +40,7 @@ export function extendedHoursSpans(
|
|
|
39
40
|
ratios: readonly number[],
|
|
40
41
|
): CompositeExtendedHoursSpan[] {
|
|
41
42
|
const exchange = anchor?.timeBasis?.exchange;
|
|
42
|
-
if (!anchor || !exchange || !
|
|
43
|
+
if (!anchor || !exchange || !isIntradaySeries(anchor) || dates.length === 0) return [];
|
|
43
44
|
const edge = (index: number, side: -1 | 1) => {
|
|
44
45
|
const ratio = ratios[index]!;
|
|
45
46
|
// At either end of the plot, mirror the gap to the bar on the other side.
|
|
@@ -113,6 +113,46 @@ function renderArea(
|
|
|
113
113
|
renderLineLike(rows, series, width, height);
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
+
/** A band's range as shade between its points' low and high rows, under its line. */
|
|
117
|
+
function renderBand(
|
|
118
|
+
rows: string[][],
|
|
119
|
+
series: CompositeProjectedSeries,
|
|
120
|
+
domain: CompositeAxisDomain,
|
|
121
|
+
width: number,
|
|
122
|
+
height: number,
|
|
123
|
+
): void {
|
|
124
|
+
const edges = series.points.map((projected) => {
|
|
125
|
+
const { high, low } = projected.point;
|
|
126
|
+
if (high == null || low == null) return null;
|
|
127
|
+
const top = valueRow(high, domain, height);
|
|
128
|
+
const bottom = valueRow(low, domain, height);
|
|
129
|
+
return top === null || bottom === null
|
|
130
|
+
? null
|
|
131
|
+
: { x: cellPoint(projected, width, height).x, top: Math.min(top, bottom), bottom: Math.max(top, bottom) };
|
|
132
|
+
});
|
|
133
|
+
const shade = (x: number, top: number, bottom: number) => {
|
|
134
|
+
for (let row = Math.round(top); row <= Math.round(bottom); row += 1) {
|
|
135
|
+
const current = rows[row]?.[x];
|
|
136
|
+
if (current === " " || current === "·") setCell(rows, x, row, "░");
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
for (let index = 0; index < edges.length; index += 1) {
|
|
140
|
+
const current = edges[index];
|
|
141
|
+
if (!current) continue;
|
|
142
|
+
const previous = index > 0 && !series.points[index]?.breakBefore ? edges[index - 1] : null;
|
|
143
|
+
if (!previous) {
|
|
144
|
+
shade(current.x, current.top, current.bottom);
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
const span = Math.max(1, current.x - previous.x);
|
|
148
|
+
for (let x = previous.x; x <= current.x; x += 1) {
|
|
149
|
+
const t = (x - previous.x) / span;
|
|
150
|
+
shade(x, previous.top + (current.top - previous.top) * t, previous.bottom + (current.bottom - previous.bottom) * t);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
renderLineLike(rows, series, width, height);
|
|
154
|
+
}
|
|
155
|
+
|
|
116
156
|
function renderColumns(
|
|
117
157
|
rows: string[][],
|
|
118
158
|
series: CompositeProjectedSeries,
|
|
@@ -221,7 +261,7 @@ export function renderCompositePanelText(
|
|
|
221
261
|
|
|
222
262
|
const columnLayout = buildCompositeColumnLayout(panel);
|
|
223
263
|
const orderedSeries = [...panel.series].sort((left, right) => {
|
|
224
|
-
const layerRank = (style: string) => style === "area" || style === "columns" ? 0 : 1;
|
|
264
|
+
const layerRank = (style: string) => style === "area" || style === "columns" || style === "band" ? 0 : 1;
|
|
225
265
|
return layerRank(left.source.style) - layerRank(right.source.style);
|
|
226
266
|
});
|
|
227
267
|
for (const series of orderedSeries) {
|
|
@@ -234,6 +274,9 @@ export function renderCompositePanelText(
|
|
|
234
274
|
case "area":
|
|
235
275
|
renderArea(rows, series, domain, plotWidth, height);
|
|
236
276
|
break;
|
|
277
|
+
case "band":
|
|
278
|
+
renderBand(rows, series, domain, plotWidth, height);
|
|
279
|
+
break;
|
|
237
280
|
case "candles":
|
|
238
281
|
case "ohlc":
|
|
239
282
|
case "hlc":
|
|
@@ -264,6 +307,14 @@ export function renderCompositePanelText(
|
|
|
264
307
|
if (current === " " || current === "·") setCell(rows, x, row, "╌");
|
|
265
308
|
}
|
|
266
309
|
}
|
|
310
|
+
// The previous close, dotted, where the last price has not taken the row.
|
|
311
|
+
if (panel.priorClose) {
|
|
312
|
+
const row = clamp(Math.round(panel.priorClose.yRatio * Math.max(height - 1, 0)), 0, Math.max(height - 1, 0));
|
|
313
|
+
for (let x = 0; x < plotWidth; x += 1) {
|
|
314
|
+
const current = rows[row]?.[x];
|
|
315
|
+
if (current === " " || current === "·") setCell(rows, x, row, "┈");
|
|
316
|
+
}
|
|
317
|
+
}
|
|
267
318
|
|
|
268
319
|
if (cursorXRatio !== null) {
|
|
269
320
|
const cursorX = clamp(Math.round(cursorXRatio * Math.max(plotWidth - 1, 0)), 0, Math.max(plotWidth - 1, 0));
|