gloomberb 0.18.1 → 0.18.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/api-client/commodity-balances.ts +6 -0
- package/src/api-client/company-kpis.ts +9 -0
- package/src/api-client/crypto-markets.ts +9 -0
- package/src/api-client/data.ts +6 -5
- package/src/api-client/equity-screener.ts +2 -0
- package/src/api-client/types.ts +5 -0
- package/src/app/pane-runtime/plugin-bindings.ts +3 -1
- package/src/app/pane-runtime/ticker-open-runtime.ts +17 -5
- package/src/app/runtime/plugin-bindings.ts +6 -2
- package/src/brokers/logos.ts +66 -0
- package/src/brokers/profile-form.ts +3 -0
- package/src/brokers/remote-broker-adapter.ts +13 -3
- package/src/capabilities/index.ts +1 -1
- package/src/capabilities/service-capabilities.ts +7 -1
- package/src/cli/base-converter.ts +60 -18
- package/src/cli/commands/market.ts +281 -154
- package/src/cli/commands/news-search.ts +35 -8
- package/src/cli/commands/system.ts +35 -1
- package/src/cli/commands/ticker.ts +100 -50
- package/src/cli/currency-hints.ts +27 -0
- package/src/cli/fx-pair.ts +41 -4
- package/src/cli/glossary.ts +57 -4
- package/src/cli/helpers.ts +36 -3
- package/src/cli/index.ts +4 -1
- package/src/cli/known-gaps.ts +37 -1
- package/src/cli/listing-arg.ts +37 -2
- package/src/cli/pane-functions/catalog.ts +3 -1
- package/src/cli/pane-functions/discovery.ts +15 -1
- package/src/cli/pane-functions/dom.ts +9 -4
- package/src/cli/pane-functions/freshness.ts +4 -4
- package/src/cli/pane-functions/function-help.ts +27 -3
- package/src/cli/pane-functions/headless.ts +6 -5
- package/src/cli/pane-functions/index.ts +3 -2
- package/src/cli/pane-functions/lock-note.ts +7 -4
- package/src/cli/pane-functions/resolver.ts +2 -1
- package/src/cli/quote-answer.ts +179 -0
- package/src/cli/series-search.ts +2 -1
- package/src/cli/types.ts +4 -0
- package/src/components/chart/composite/composite-chart.tsx +11 -2
- package/src/components/chart/composite/format.ts +13 -1
- package/src/components/chart/composite/level-labels.ts +30 -0
- package/src/components/chart/composite/levels.ts +17 -6
- package/src/components/chart/composite/panel-surface.tsx +58 -8
- package/src/components/command-bar/routes/ticker-search/actions.ts +6 -0
- package/src/components/index.ts +2 -0
- package/src/components/layout/pane/content.tsx +16 -14
- package/src/components/layout/pane/footer/index.tsx +22 -3
- package/src/components/layout/pane/footer/model.ts +99 -4
- package/src/components/layout/pane/header-tabs.tsx +15 -1
- package/src/components/onboarding/onboarding-frame.tsx +9 -11
- package/src/components/onboarding/use-onboarding-broker-form.ts +5 -2
- package/src/components/onboarding/wizard-model.ts +10 -3
- package/src/components/ui/button.tsx +11 -4
- package/src/components/ui/choice-dialog.tsx +8 -5
- package/src/components/ui/display.tsx +11 -6
- package/src/components/ui/fields.tsx +100 -20
- package/src/components/ui/icon.tsx +6 -1
- package/src/components/ui/list-row-lines.tsx +87 -0
- package/src/components/ui/list-view.tsx +14 -0
- package/src/components/ui/logo-mark.tsx +64 -0
- package/src/components/ui/number-step.ts +54 -0
- package/src/components/ui/select-button.tsx +5 -4
- package/src/components/ui/select-field.tsx +3 -3
- package/src/components/ui/status.tsx +17 -3
- package/src/components/ui/tabs.tsx +9 -1
- package/src/components/ui/toggle.tsx +17 -3
- package/src/core/state/app/instrument.ts +8 -3
- package/src/data/config/store/normalize.ts +4 -1
- package/src/data/plugin-cache.ts +32 -6
- package/src/docs/snapshot.json +5 -5
- package/src/i18n/es.ts +9 -3
- package/src/i18n/ja.ts +9 -3
- package/src/i18n/ko.ts +9 -3
- package/src/i18n/zh-cn.ts +10 -4
- package/src/i18n/zh-tw.ts +10 -4
- package/src/market-data/commodity-names.ts +80 -0
- package/src/market-data/coordinator/auxiliary.ts +3 -1
- package/src/market-data/coordinator/chart.ts +3 -1
- package/src/market-data/futures-board.ts +38 -1
- package/src/market-data/futures-contract.ts +91 -0
- package/src/market-data/fx-closes.ts +78 -1
- package/src/market-data/market/change-basis.ts +219 -0
- package/src/market-data/market/format.ts +28 -12
- package/src/market-data/market/freshness.ts +38 -0
- package/src/market-data/market/status.ts +55 -9
- package/src/market-data/market/year-to-date.ts +46 -0
- package/src/market-data/quotes/contributions.ts +12 -0
- package/src/market-data/quotes/delisting.ts +34 -0
- package/src/market-data/quotes/live-valuation.ts +23 -0
- package/src/market-data/quotes/resolution.ts +8 -2
- package/src/market-data/tax-equivalent.ts +10 -2
- package/src/notifications/app-notifier.ts +2 -2
- package/src/notifications/notification-log.ts +8 -6
- package/src/plugins/builtin/alerts/cli.ts +26 -18
- package/src/plugins/builtin/alerts/headless.ts +1 -1
- package/src/plugins/builtin/alerts/index.tsx +13 -4
- package/src/plugins/builtin/attention/trending.tsx +8 -6
- package/src/plugins/builtin/backtest/client.ts +5 -2
- package/src/plugins/builtin/backtest/window.ts +16 -3
- package/src/plugins/builtin/broker-manager/add-flow.tsx +40 -8
- package/src/plugins/builtin/broker-manager/detail.tsx +26 -1
- package/src/plugins/builtin/broker-manager/index.tsx +20 -9
- package/src/plugins/builtin/broker-manager/model.ts +5 -0
- package/src/plugins/builtin/broker-manager/pane-actions.tsx +8 -1
- package/src/plugins/builtin/broker-manager/table.ts +5 -1
- package/src/plugins/builtin/broker-trading/account-picker.tsx +91 -0
- package/src/plugins/builtin/broker-trading/chart-levels.ts +54 -0
- package/src/plugins/builtin/broker-trading/command.ts +103 -0
- package/src/plugins/builtin/broker-trading/controller.ts +688 -0
- package/src/plugins/builtin/broker-trading/dock.tsx +182 -0
- package/src/plugins/builtin/broker-trading/headless.ts +76 -0
- package/src/plugins/builtin/broker-trading/index.tsx +77 -0
- package/src/plugins/builtin/broker-trading/order-feed.ts +383 -0
- package/src/plugins/builtin/broker-trading/orders-data.ts +133 -0
- package/src/plugins/builtin/broker-trading/orders.tsx +437 -0
- package/src/plugins/builtin/broker-trading/overview-orders.tsx +121 -0
- package/src/plugins/builtin/broker-trading/pane-model.ts +207 -0
- package/src/plugins/builtin/broker-trading/pane.tsx +505 -0
- package/src/plugins/builtin/broker-trading/strategy.ts +153 -0
- package/src/plugins/builtin/broker-trading/ticket-parts.tsx +117 -0
- package/src/plugins/builtin/broker-trading/ticket.tsx +573 -0
- package/src/plugins/builtin/buildout/data-runtime.ts +1 -1
- package/src/plugins/builtin/buildout/format.ts +1 -0
- package/src/plugins/builtin/buildout/model/index.ts +16 -7
- package/src/plugins/builtin/buildout/pane/index.tsx +3 -2
- package/src/plugins/builtin/buildout/pane/status.tsx +3 -3
- package/src/plugins/builtin/chart-composer/cli-options.ts +19 -0
- package/src/plugins/builtin/chart-composer/data-catalog-pane.tsx +2 -1
- package/src/plugins/builtin/chart-composer/headless-schema.ts +3 -3
- package/src/plugins/builtin/chart-composer/headless.ts +2 -1
- package/src/plugins/builtin/chart-composer/option-range.ts +39 -0
- package/src/plugins/builtin/chart-composer/pane.tsx +31 -11
- package/src/plugins/builtin/chart-composer/presets.ts +2 -2
- package/src/plugins/builtin/chart-composer/series-catalog.ts +4 -2
- package/src/plugins/builtin/chart-composer/shortcuts.ts +3 -3
- package/src/plugins/builtin/chart-composer/viewport-labels.ts +5 -2
- package/src/plugins/builtin/chat/index.tsx +1 -1
- package/src/plugins/builtin/chat/status-widget.tsx +1 -1
- package/src/plugins/builtin/chat/unread-inbox-pane.tsx +6 -1
- package/src/plugins/builtin/chat/unread-inbox.ts +1 -1
- package/src/plugins/builtin/commodity-balances/client.ts +6 -4
- package/src/plugins/builtin/commodity-balances/headless.ts +41 -7
- package/src/plugins/builtin/commodity-balances/index.tsx +19 -4
- package/src/plugins/builtin/commodity-balances/model.ts +22 -0
- package/src/plugins/builtin/commodity-balances/pane.tsx +36 -5
- package/src/plugins/builtin/company-kpis/client.ts +3 -2
- package/src/plugins/builtin/company-kpis/headless.ts +5 -7
- package/src/plugins/builtin/company-kpis/model.ts +85 -2
- package/src/plugins/builtin/company-kpis/pane.tsx +26 -12
- package/src/plugins/builtin/composite-plugins.ts +3 -2
- package/src/plugins/builtin/cot/headless.ts +1 -1
- package/src/plugins/builtin/cot/index.tsx +1 -1
- package/src/plugins/builtin/cot/model.ts +35 -12
- package/src/plugins/builtin/credit-boards/cdx-pane.tsx +2 -1
- package/src/plugins/builtin/credit-boards/sovr-pane.tsx +2 -1
- package/src/plugins/builtin/credit-conditions/index.tsx +2 -1
- package/src/plugins/builtin/crypto-board/client.ts +33 -2
- package/src/plugins/builtin/crypto-board/headless.ts +5 -2
- package/src/plugins/builtin/crypto-board/model.ts +28 -8
- package/src/plugins/builtin/dividend-yield/headless.ts +30 -8
- package/src/plugins/builtin/dividend-yield/provider-client.ts +3 -1
- package/src/plugins/builtin/earnings-calls/headless.ts +4 -4
- package/src/plugins/builtin/earnings-calls/lock-note.ts +12 -3
- package/src/plugins/builtin/econ/calendar-model.ts +35 -0
- package/src/plugins/builtin/econ/index.tsx +31 -4
- package/src/plugins/builtin/econ/print-watch.ts +279 -0
- package/src/plugins/builtin/equity-screener/headless.ts +20 -5
- package/src/plugins/builtin/equity-screener/model.ts +40 -2
- package/src/plugins/builtin/equity-screener/pane.tsx +24 -21
- package/src/plugins/builtin/estimate-revisions/headless.ts +3 -2
- package/src/plugins/builtin/estimate-revisions/lock-note.ts +29 -7
- package/src/plugins/builtin/exposure/client.ts +4 -1
- package/src/plugins/builtin/exposure/editor.tsx +3 -3
- package/src/plugins/builtin/exposure/evidence.ts +4 -1
- package/src/plugins/builtin/exposure/holdings.ts +3 -1
- package/src/plugins/builtin/exposure/pane.tsx +37 -16
- package/src/plugins/builtin/filing-events/index.tsx +9 -3
- package/src/plugins/builtin/futures/table.tsx +6 -3
- package/src/plugins/builtin/futures-curve/pane.tsx +2 -2
- package/src/plugins/builtin/insider/base-values.ts +72 -0
- package/src/plugins/builtin/insider/headless.ts +29 -4
- package/src/plugins/builtin/jobs/headless.ts +2 -2
- package/src/plugins/builtin/jobs/lock-note.ts +14 -3
- package/src/plugins/builtin/market-halts/model.ts +31 -6
- package/src/plugins/builtin/market-halts/pane.tsx +3 -0
- package/src/plugins/builtin/market-valuation/pane.tsx +2 -1
- package/src/plugins/builtin/members/pane.tsx +8 -3
- package/src/plugins/builtin/news/wire/feed-config.ts +29 -12
- package/src/plugins/builtin/news/wire/index.tsx +7 -2
- package/src/plugins/builtin/news/wire/news/preset-pane.tsx +11 -3
- package/src/plugins/builtin/news/wire/rss/cli.ts +3 -2
- package/src/plugins/builtin/news/wire/rss/source.ts +13 -4
- package/src/plugins/builtin/options/footer.ts +25 -20
- package/src/plugins/builtin/options/live-quotes.ts +33 -18
- package/src/plugins/builtin/options/view.tsx +77 -22
- package/src/plugins/builtin/options-calculator/pane.tsx +4 -2
- package/src/plugins/builtin/options-scenario/pane.tsx +41 -6
- package/src/plugins/builtin/portfolio-list/account-metrics.ts +3 -0
- package/src/plugins/builtin/portfolio-list/cli/render.ts +34 -10
- package/src/plugins/builtin/portfolio-list/headless.ts +1 -1
- package/src/plugins/builtin/portfolio-list/pane/data.ts +23 -1
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +33 -14
- package/src/plugins/builtin/portfolio-list/settings.ts +20 -2
- package/src/plugins/builtin/portfolio-list/summary/index.tsx +12 -0
- package/src/plugins/builtin/portfolio-list/summary/totals.ts +24 -3
- package/src/plugins/builtin/primary-dealers/client.ts +137 -62
- package/src/plugins/builtin/primary-dealers/headless.ts +30 -5
- package/src/plugins/builtin/primary-dealers/index.tsx +3 -2
- package/src/plugins/builtin/primary-dealers/model.ts +179 -129
- package/src/plugins/builtin/primary-dealers/pane.tsx +135 -15
- package/src/plugins/builtin/research/analyst-model.ts +5 -1
- package/src/plugins/builtin/risk-factors/index.tsx +12 -3
- package/src/plugins/builtin/scanner/feed.ts +6 -1
- package/src/plugins/builtin/scanner/headless.ts +1 -1
- package/src/plugins/builtin/scanner/lock-note.ts +3 -2
- package/src/plugins/builtin/series-inventory.ts +1 -0
- package/src/plugins/builtin/shared/cloud-denied.ts +7 -6
- package/src/plugins/builtin/shared/cloud-upgrade.ts +10 -11
- package/src/plugins/builtin/shared/free-routes.ts +24 -0
- package/src/plugins/builtin/shared/headless-options.ts +11 -0
- package/src/plugins/builtin/shared/pro-lock-note.ts +89 -7
- package/src/plugins/builtin/shared/research-trading.tsx +62 -0
- package/src/plugins/builtin/social-mentions/headless.ts +1 -1
- package/src/plugins/builtin/social-mentions/lock-note.ts +1 -1
- package/src/plugins/builtin/themes/headless.ts +8 -2
- package/src/plugins/builtin/themes/model.ts +43 -10
- package/src/plugins/builtin/themes/pane.tsx +8 -6
- package/src/plugins/builtin/ticker-detail/overview/model.ts +44 -10
- package/src/plugins/builtin/ticker-detail/overview/option-contract.ts +64 -0
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +82 -21
- package/src/plugins/builtin/ticker-detail/pane.tsx +21 -14
- package/src/plugins/builtin/ticker-detail/quote-footer.ts +5 -1
- package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +37 -8
- package/src/plugins/builtin/time-sales/pane.tsx +2 -2
- package/src/plugins/builtin/trace-bonds/client.ts +14 -3
- package/src/plugins/builtin/trace-bonds/headless.ts +1 -1
- package/src/plugins/builtin/vol-surface/pane.tsx +2 -2
- package/src/plugins/builtin-editorial.ts +1 -1
- package/src/plugins/compat.ts +3 -0
- package/src/plugins/external-runtime.ts +5 -0
- package/src/plugins/loader.ts +6 -0
- package/src/plugins/plugin-icon.ts +61 -0
- package/src/plugins/registry/host-actions.ts +2 -1
- package/src/plugins/registry/index.ts +9 -1
- package/src/plugins/runtime/context.tsx +5 -0
- package/src/plugins/runtime/index.tsx +3 -0
- package/src/portfolio/column-values.ts +13 -4
- package/src/portfolio/cost-fx.ts +5 -28
- package/src/public/__snapshots__/public-api.test.ts.snap +5 -0
- package/src/public/broker.ts +8 -0
- package/src/public/market-data.ts +5 -0
- package/src/renderers/dom/desktop/control-styles.ts +7 -1
- package/src/renderers/dom/desktop/controls.tsx +39 -7
- package/src/renderers/dom/desktop/icons.tsx +14 -0
- package/src/renderers/dom/desktop/list-view.tsx +51 -19
- package/src/renderers/dom/desktop/menu.tsx +10 -1
- package/src/renderers/dom/desktop/select-field.tsx +8 -6
- package/src/renderers/dom/host/style.ts +6 -1
- package/src/renderers/dom/host/tabs.tsx +6 -0
- package/src/renderers/dom/styles.css +12 -5
- package/src/renderers/electrobun/bun/core-capabilities.ts +12 -0
- package/src/renderers/electrobun/bun/external-plugins.ts +3 -0
- package/src/renderers/electrobun/shared/protocol.ts +2 -0
- package/src/renderers/electrobun/view/broker-remote-client.ts +16 -0
- package/src/renderers/electrobun/view/external-plugins.ts +1 -0
- package/src/series/validate.ts +2 -2
- package/src/sources/gloomberb-cloud/index.ts +50 -17
- package/src/sources/gloomberb-cloud/normalizers.ts +4 -0
- package/src/sources/history-result.ts +3 -0
- package/src/sources/provider-errors.ts +3 -1
- package/src/sources/provider-router/history.ts +49 -6
- package/src/sources/provider-router/index.ts +2 -1
- package/src/state/app/activity.ts +13 -0
- package/src/sync/controller.ts +4 -0
- package/src/sync/core-contributors.ts +62 -11
- package/src/sync/local-only.ts +121 -0
- package/src/sync/react.ts +6 -0
- package/src/sync/types.ts +3 -0
- package/src/tickers/listing-fate.ts +27 -8
- package/src/tickers/search/index.ts +24 -3
- package/src/time-series/price-comparison.ts +35 -0
- package/src/time-series/resolve.ts +52 -11
- package/src/time-series/spec.ts +1 -1
- package/src/time-series/types.ts +5 -0
- package/src/types/broker.ts +56 -1
- package/src/types/config.ts +6 -0
- package/src/types/data-provider.ts +5 -1
- package/src/types/financials.ts +23 -0
- package/src/types/headless.ts +12 -2
- package/src/types/instrument.ts +4 -0
- package/src/types/plugin.ts +24 -6
- package/src/types/price-history.ts +6 -1
- package/src/types/trading.ts +67 -0
- package/src/ui/host.tsx +4 -0
- package/src/utils/base-currency.ts +108 -0
- package/src/utils/currency-names.ts +128 -0
- package/src/utils/currency-pair.ts +2 -1
- package/src/utils/currency-units.ts +32 -1
- package/src/utils/dom-screenshot.ts +22 -9
- package/src/utils/exchange-notation.generated.ts +360 -0
- package/src/utils/exchanges.ts +15 -322
- package/src/utils/format.ts +31 -1
- package/src/utils/instrument-identity.ts +38 -1
- package/src/utils/options.ts +46 -1
- package/src/utils/price-history.ts +4 -0
- package/src/utils/reported-money.ts +8 -4
|
@@ -7,6 +7,7 @@ import { fetchCryptoMarkets, fetchStablecoinSupply } from "./client";
|
|
|
7
7
|
import {
|
|
8
8
|
buildCryptoRows,
|
|
9
9
|
CRYPTO_BOARD_FRESHNESS,
|
|
10
|
+
CRYPTO_CHANGE_BASIS_LABEL,
|
|
10
11
|
formatCryptoPercent,
|
|
11
12
|
formatDayRangeTime,
|
|
12
13
|
formatOffPeg,
|
|
@@ -30,7 +31,9 @@ const COIN_COLUMNS: HeadlessPaneColumn[] = [
|
|
|
30
31
|
{ key: "code", header: "Coin" },
|
|
31
32
|
{ key: "name", header: "Name" },
|
|
32
33
|
{ key: "price", header: "Price", align: "right", format: (_value, row) => String(row.priceText ?? "—") },
|
|
33
|
-
|
|
34
|
+
// Since 00:00 UTC, the day `quote` gives a coin (CRYPTO_CHANGE_BASIS_LABEL): the text says so, as the pane's
|
|
35
|
+
// CHG% UTC does, and an export keeps the header it has always had.
|
|
36
|
+
{ key: "changePercent", header: "Chg %", textHeader: "Chg % UTC", align: "right", format: percent },
|
|
34
37
|
{ key: "return7d", header: "7D %", align: "right", format: percent },
|
|
35
38
|
{ key: "return30d", header: "30D %", align: "right", format: percent },
|
|
36
39
|
{ key: "return1y", header: "1Y %", align: "right", format: percent },
|
|
@@ -167,7 +170,7 @@ export const cryptoBoardHeadless: HeadlessPaneDefinition<"rows"> = {
|
|
|
167
170
|
pegType: row.pegType,
|
|
168
171
|
} : {}),
|
|
169
172
|
}));
|
|
170
|
-
const metadata = { list: kind, status: data.status, asOf: data.asOf, generatedAt: data.generatedAt };
|
|
173
|
+
const metadata = { list: kind, status: data.status, asOf: data.asOf, generatedAt: data.generatedAt, changeBasis: CRYPTO_CHANGE_BASIS_LABEL };
|
|
171
174
|
if (kind !== "stablecoin") return { rows, errors: data.warnings, metadata };
|
|
172
175
|
const summary = supply ? supplySummaryLine(supply) : null;
|
|
173
176
|
const history = supply ? supplyHistoryTable(supply) : null;
|
|
@@ -57,6 +57,11 @@ export interface CryptoRow extends CryptoRowSupply {
|
|
|
57
57
|
price: number;
|
|
58
58
|
/** The price as the board prints it. */
|
|
59
59
|
priceText: string;
|
|
60
|
+
/**
|
|
61
|
+
* The move since 00:00 UTC, from the previous UTC day's close, as `quote`
|
|
62
|
+
* measures a coin (CRYPTO_CHANGE_BASIS_LABEL); null when the coin has no
|
|
63
|
+
* such close to measure from.
|
|
64
|
+
*/
|
|
60
65
|
changePercent: number | null;
|
|
61
66
|
return7d: number | null;
|
|
62
67
|
return30d: number | null;
|
|
@@ -100,9 +105,28 @@ function closeDaysAgo(asset: CryptoMarketAsset, days: number, now: number): numb
|
|
|
100
105
|
return isFiniteNumber(close) && close > 0 ? close : null;
|
|
101
106
|
}
|
|
102
107
|
|
|
103
|
-
const percentChange = (price: number, reference: number | null) =>
|
|
108
|
+
const percentChange = (price: number, reference: number | null | undefined) =>
|
|
104
109
|
reference != null && reference > 0 ? (price / reference - 1) * 100 : null;
|
|
105
110
|
|
|
111
|
+
/** What the board's day change is measured from, as `quote` labels a coin's: its header says it (CHG% UTC). */
|
|
112
|
+
export const CRYPTO_CHANGE_BASIS_LABEL = "since 00:00 UTC";
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* A coin's move since 00:00 UTC, the basis `quote` gives a coin: its live
|
|
116
|
+
* quote's while that quote is measured from the previous close, otherwise the
|
|
117
|
+
* board's, which Gloom Cloud measures the same way (with a live price, from
|
|
118
|
+
* the board's previous close). A coin either says moves over 24 hours, or not
|
|
119
|
+
* at all, has none here, so the column never mixes the two.
|
|
120
|
+
*/
|
|
121
|
+
function utcDayChangePercent(asset: CryptoMarketAsset, quote: Quote | null, price: number): number | null {
|
|
122
|
+
if (quote && quote.changeBasis == null) {
|
|
123
|
+
if (isFiniteNumber(quote.changePercent)) return quote.changePercent;
|
|
124
|
+
if (positive(quote.previousClose)) return percentChange(price, quote.previousClose);
|
|
125
|
+
}
|
|
126
|
+
if (asset.changeBasis != null) return null;
|
|
127
|
+
return quote ? percentChange(price, asset.previousClose) : asset.changePercent;
|
|
128
|
+
}
|
|
129
|
+
|
|
106
130
|
/** A live quote replaces the board's snapshot once it is at least as new. */
|
|
107
131
|
function liveQuote(
|
|
108
132
|
asset: CryptoMarketAsset,
|
|
@@ -134,11 +158,7 @@ function buildCryptoRow(
|
|
|
134
158
|
now = Date.now(),
|
|
135
159
|
): CryptoRow {
|
|
136
160
|
const price = quote?.price ?? asset.price;
|
|
137
|
-
const changePercent = quote
|
|
138
|
-
? quote.changePercent
|
|
139
|
-
: quote
|
|
140
|
-
? percentChange(price, asset.previousClose)
|
|
141
|
-
: asset.changePercent;
|
|
161
|
+
const changePercent = utcDayChangePercent(asset, quote, price);
|
|
142
162
|
// Supply moves slowly; the price is what makes market cap live. Scaling the
|
|
143
163
|
// reported cap keeps the source's supply basis and the board's ranking.
|
|
144
164
|
const marketCap = isFiniteNumber(asset.marketCap)
|
|
@@ -389,7 +409,7 @@ const COLUMN_SPECS: CryptoColumnSpec[] = [
|
|
|
389
409
|
{ id: "code", label: "COIN", width: 7, align: "left" },
|
|
390
410
|
{ id: "name", label: "NAME", width: 10, align: "left", optional: 1 },
|
|
391
411
|
{ id: "price", label: "PRICE", width: 13, align: "right" },
|
|
392
|
-
{ id: "changePercent", label: "CHG%", width: 8, align: "right" },
|
|
412
|
+
{ id: "changePercent", label: "CHG% UTC", title: "Change since 00:00 UTC", width: 8, align: "right" },
|
|
393
413
|
{ id: "return7d", label: "7D%", width: 8, align: "right" },
|
|
394
414
|
{ id: "return30d", label: "30D%", width: 8, align: "right", optional: 3 },
|
|
395
415
|
{ id: "return1y", label: "1Y%", width: 10, align: "right", optional: 5 },
|
|
@@ -415,7 +435,7 @@ const STABLECOIN_COLUMN_SPECS: CryptoColumnSpec[] = [
|
|
|
415
435
|
{ id: "low24hAt", label: "LOW UTC", width: 9, align: "left" },
|
|
416
436
|
{ id: "high24h", label: "24H HIGH", width: 8, align: "right", optional: 1 },
|
|
417
437
|
{ id: "high24hAt", label: "HIGH UTC", width: 9, align: "left", optional: 2 },
|
|
418
|
-
{ id: "changePercent", label: "CHG%", width: 8, align: "right", optional: 5 },
|
|
438
|
+
{ id: "changePercent", label: "CHG% UTC", title: "Change since 00:00 UTC", width: 8, align: "right", optional: 5 },
|
|
419
439
|
{ id: "return7d", label: "7D%", width: 8, align: "right", optional: 6 },
|
|
420
440
|
{ id: "return30d", label: "30D%", width: 8, align: "right", optional: 8 },
|
|
421
441
|
{ id: "return1y", label: "1Y%", width: 10, align: "right", optional: 9 },
|
|
@@ -15,6 +15,7 @@ import type { Quote } from "../../../types/financials";
|
|
|
15
15
|
import { regularSessionValuationPrice } from "../../../market-data/quotes/regular-session-valuation";
|
|
16
16
|
import { dividendPriceStatus, dividendQuotePriceMetadata } from "./reference-price";
|
|
17
17
|
import { mapWithConcurrency } from "../../../utils/concurrency";
|
|
18
|
+
import { logFailure } from "../../../utils/log-failure";
|
|
18
19
|
import { projectDividendRanking, roundedRatio, type DividendRankingEntry } from "./ranking";
|
|
19
20
|
import { REPORT_FILTER, REPORT_ROWS } from "../shared/headless-options";
|
|
20
21
|
import type { FundBondFacts } from "../../../api-client/fund-facts";
|
|
@@ -50,6 +51,9 @@ export interface DividendYieldHeadlessDependencies {
|
|
|
50
51
|
loadTreasury?(context: HeadlessPaneContext): Promise<TreasuryTenor[] | null>;
|
|
51
52
|
}
|
|
52
53
|
|
|
54
|
+
/** Already logged at the quote boundary; keep it distinct from absent bond facts. */
|
|
55
|
+
class BondClassificationError extends Error {}
|
|
56
|
+
|
|
53
57
|
const defaultDependencies: DividendYieldHeadlessDependencies = {
|
|
54
58
|
async loadData(symbol, context) {
|
|
55
59
|
let currentPrice: number | null = null;
|
|
@@ -63,7 +67,8 @@ const defaultDependencies: DividendYieldHeadlessDependencies = {
|
|
|
63
67
|
currentPrice = regularSessionValuationPrice(quote);
|
|
64
68
|
currentPriceCurrency = quote.currency;
|
|
65
69
|
referenceQuote = quote;
|
|
66
|
-
} catch {
|
|
70
|
+
} catch (error) {
|
|
71
|
+
logFailure("dividend-yield.headless", "Read dividend reference quote failed")(error);
|
|
67
72
|
currentPrice = null;
|
|
68
73
|
}
|
|
69
74
|
const data = await fetchDividendData(symbol, currentPrice, exchange, currentPriceCurrency);
|
|
@@ -75,7 +80,11 @@ const defaultDependencies: DividendYieldHeadlessDependencies = {
|
|
|
75
80
|
async loadBond(symbol, context) {
|
|
76
81
|
const instrument = await context.resolveInstrument?.(symbol);
|
|
77
82
|
const exchange = instrument?.exchange ?? "";
|
|
78
|
-
const quote = await context.marketData.getQuote(symbol, exchange)
|
|
83
|
+
const quote = await context.marketData.getQuote(symbol, exchange)
|
|
84
|
+
.catch((error) => {
|
|
85
|
+
logFailure("dividend-yield.headless", "Read bond fund classification quote failed")(error);
|
|
86
|
+
throw new BondClassificationError("Bond fund quote check failed");
|
|
87
|
+
});
|
|
79
88
|
const venue = exchange || quote?.exchangeName || "";
|
|
80
89
|
if (resolveTickerInstrumentKind(null, { quote }) !== "fund" || (venue && !isUsListingExchange(venue))) return null;
|
|
81
90
|
return fetchFundBondFacts(instrument?.symbol ?? symbol, context.apiClient);
|
|
@@ -125,7 +134,7 @@ export function projectDividendYieldHeadless(
|
|
|
125
134
|
const metrics = data.metrics;
|
|
126
135
|
const currency = data.currency ?? data.payments[0]?.currency ?? "";
|
|
127
136
|
const priceStatus = dividendPriceStatus(data.price, data.priceAsOf, data.priceStale);
|
|
128
|
-
const errors = [data.historyError, data.summaryError].filter((error): error is string => !!error);
|
|
137
|
+
const errors = [data.historyError, data.summaryError, tax?.checkError].filter((error): error is string => !!error);
|
|
129
138
|
|
|
130
139
|
return {
|
|
131
140
|
...(errors.length > 0 ? { complete: false, errors } : {}),
|
|
@@ -249,15 +258,26 @@ export function createDividendYieldHeadless(
|
|
|
249
258
|
// One symbol is its full report; several are one table ranked by yield.
|
|
250
259
|
if (args.symbols.length <= 1) {
|
|
251
260
|
const symbol = args.symbols[0] ?? String(args.argument);
|
|
261
|
+
let bondCheckError: string | undefined;
|
|
252
262
|
const [data, bond, treasury] = await Promise.all([
|
|
253
263
|
dependencies.loadData(symbol, context),
|
|
254
|
-
//
|
|
255
|
-
dependencies.loadBond?.(symbol, context)
|
|
256
|
-
|
|
264
|
+
// Optional enrichment without a tax rate; a requested tax check must distinguish failure from absence.
|
|
265
|
+
dependencies.loadBond?.(symbol, context)
|
|
266
|
+
.catch((error) => {
|
|
267
|
+
if (error instanceof BondClassificationError) {
|
|
268
|
+
bondCheckError = error.message;
|
|
269
|
+
} else {
|
|
270
|
+
logFailure("dividend-yield.headless", "Read bond fund facts for report failed")(error);
|
|
271
|
+
bondCheckError = "Bond fund facts check failed";
|
|
272
|
+
}
|
|
273
|
+
return null;
|
|
274
|
+
}) ?? null,
|
|
275
|
+
rates ? dependencies.loadTreasury?.(context)
|
|
276
|
+
.catch(logFailure("dividend-yield.headless", "Read Treasury curve for tax comparison failed", null)) ?? null : null,
|
|
257
277
|
]);
|
|
258
278
|
const forward = data.metrics.lapse ? null : distributionBasis(data.metrics.forwardYield, "forward", data.metrics.forwardRateAsOf ?? null);
|
|
259
279
|
const tax = rates ? analyzeTaxEquivalent({
|
|
260
|
-
symbol: symbol.toUpperCase(), bond, rates, treasury,
|
|
280
|
+
symbol: symbol.toUpperCase(), bond, rates, treasury, checkError: bondCheckError,
|
|
261
281
|
distribution: forward ?? distributionBasis(data.metrics.trailingYield, "trailing"),
|
|
262
282
|
}) : null;
|
|
263
283
|
return projectDividendYieldHeadless(data, args, { bond, tax });
|
|
@@ -275,7 +295,9 @@ export function createDividendYieldHeadless(
|
|
|
275
295
|
}
|
|
276
296
|
const entries = await mapWithConcurrency(args.symbols, RANKING_CONCURRENCY, async (symbol): Promise<DividendRankingEntry> => {
|
|
277
297
|
// A fund's bond facts never fail its row: without them it ranks as before.
|
|
278
|
-
const bond = dependencies.loadBond?.(symbol, context)
|
|
298
|
+
const bond = dependencies.loadBond?.(symbol, context)
|
|
299
|
+
.catch((error) => error instanceof BondClassificationError ? null
|
|
300
|
+
: logFailure("dividend-yield.headless", "Read bond fund facts for ranking failed", null)(error)) ?? null;
|
|
279
301
|
try {
|
|
280
302
|
return { symbol, data: await dependencies.loadData(symbol, context), bond: await bond };
|
|
281
303
|
} catch (error) {
|
|
@@ -3,6 +3,7 @@ import { regularSessionValuationPrice } from "../../../market-data/quotes/regula
|
|
|
3
3
|
import { resolveCurrencyUnit } from "../../../utils/currency-units";
|
|
4
4
|
import { cashInMajorUnits } from "../../../utils/dividend-cash";
|
|
5
5
|
import { dividendReferencePrice } from "../../../utils/trailing-dividend";
|
|
6
|
+
import { logFailure } from "../../../utils/log-failure";
|
|
6
7
|
import { buildDividendMetrics, INCOMPLETE_DIVIDEND_HISTORY, MISSING_DIVIDEND_CURRENCY, toDividendPayment, type DividendData } from "./client";
|
|
7
8
|
import { dividendQuotePriceMetadata } from "./reference-price";
|
|
8
9
|
|
|
@@ -17,7 +18,8 @@ export async function fetchProviderDividendData(
|
|
|
17
18
|
if (!provider.getCorporateActions) throw new Error("Dividend history source unavailable.");
|
|
18
19
|
const [sourceActions, quote] = await Promise.all([
|
|
19
20
|
provider.getCorporateActions(symbol, exchange),
|
|
20
|
-
provider.getQuote(symbol, exchange)
|
|
21
|
+
provider.getQuote(symbol, exchange)
|
|
22
|
+
.catch(logFailure("dividend-yield.provider-client", "Read dividend reference quote failed", null)),
|
|
21
23
|
]);
|
|
22
24
|
// In the major unit as the history arrives, as the provider router serves it.
|
|
23
25
|
const actions = cashInMajorUnits(sourceActions);
|
|
@@ -70,8 +70,8 @@ const defaultDependencies: EarningsCallsHeadlessDependencies = {
|
|
|
70
70
|
};
|
|
71
71
|
|
|
72
72
|
/** A refused read says what the lock note says (CALLS is Pro whole), so a free account meets the same words as a signed-out one. */
|
|
73
|
-
function explainDenied(ticker: string | null) {
|
|
74
|
-
return (error: unknown): never => { throw explainRefusal(error, callsLockNote, { symbol: ticker }); };
|
|
73
|
+
function explainDenied(ticker: string | null, context: HeadlessPaneContext) {
|
|
74
|
+
return async (error: unknown): Promise<never> => { throw await explainRefusal(error, callsLockNote, { symbol: ticker, context }); };
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
function callRow(call: CloudEarningsCallPayload) {
|
|
@@ -285,7 +285,7 @@ export function createEarningsCallsHeadless(
|
|
|
285
285
|
},
|
|
286
286
|
async load(args, context) {
|
|
287
287
|
const ticker = typeof args.argument === "string" ? args.argument : null;
|
|
288
|
-
const calls = await dependencies.loadCalls(ticker, 200, context).catch(explainDenied(ticker));
|
|
288
|
+
const calls = await dependencies.loadCalls(ticker, 200, context).catch(explainDenied(ticker, context));
|
|
289
289
|
if (!ticker || !readsTranscript(args)) {
|
|
290
290
|
return projectEarningsCallsHeadless(calls, { ...args, options: { ...args.options, limit: args.options.limit ?? 50 } });
|
|
291
291
|
}
|
|
@@ -299,7 +299,7 @@ export function createEarningsCallsHeadless(
|
|
|
299
299
|
}
|
|
300
300
|
throw new Error(`No ${quarter} earnings call found for ${ticker}.`);
|
|
301
301
|
}
|
|
302
|
-
const transcript = await dependencies.loadTranscript(selected.id, context).catch(explainDenied(ticker));
|
|
302
|
+
const transcript = await dependencies.loadTranscript(selected.id, context).catch(explainDenied(ticker, context));
|
|
303
303
|
if (isPendingTranscript(transcript)) {
|
|
304
304
|
throw new Error(`Transcript for ${ticker} ${quarter} is still being produced.`);
|
|
305
305
|
}
|
|
@@ -1,17 +1,26 @@
|
|
|
1
1
|
import type { PaneLock } from "../../../types/plugin";
|
|
2
|
-
import {
|
|
2
|
+
import { resolveHeadlessInstrument } from "../shared/headless-market-data";
|
|
3
|
+
import { proLockNote, type FreeRoute } from "../shared/pro-lock-note";
|
|
4
|
+
|
|
5
|
+
const EVENTS_ROUTE: FreeRoute = {
|
|
6
|
+
text: (ticker, company) => `gloomberb events ${ticker} lists ${company}'s earnings dates, with the EPS reported at each and the next estimate.`,
|
|
7
|
+
answers: async (symbol, context) => {
|
|
8
|
+
const { symbol: ticker, exchange } = await resolveHeadlessInstrument(context, symbol);
|
|
9
|
+
return ((await context.marketData.getCorporateActions?.(ticker, exchange))?.earnings.length ?? 0) > 0;
|
|
10
|
+
},
|
|
11
|
+
};
|
|
3
12
|
|
|
4
13
|
/**
|
|
5
14
|
* CALLS is Pro whole, the list of calls included. `events` needs no account
|
|
6
15
|
* and lists a company's earnings dates, each with the EPS it reported and the
|
|
7
16
|
* next one's estimate, without the call or its transcript.
|
|
8
17
|
*/
|
|
9
|
-
export function callsLockNote(lock: PaneLock): string {
|
|
18
|
+
export function callsLockNote(lock: PaneLock): Promise<string> {
|
|
10
19
|
return proLockNote(lock, {
|
|
11
20
|
token: "CALLS",
|
|
12
21
|
argument: "optional",
|
|
13
22
|
does: "it reads each earnings call into a transcript with speaker turns, analyst Q&A and extracted guidance",
|
|
14
23
|
shows: "it",
|
|
15
|
-
free:
|
|
24
|
+
free: [EVENTS_ROUTE],
|
|
16
25
|
});
|
|
17
26
|
}
|
|
@@ -191,6 +191,37 @@ export function matchesCountry(event: EconEvent, filter: CountryFilter): boolean
|
|
|
191
191
|
return event.country === filter;
|
|
192
192
|
}
|
|
193
193
|
|
|
194
|
+
/**
|
|
195
|
+
* How long after its release time a high-impact print is still news: the
|
|
196
|
+
* calendar is asked for it this long, and a print that turns up later is
|
|
197
|
+
* recorded without a toast.
|
|
198
|
+
*/
|
|
199
|
+
export const PRINT_PENDING_MS = 20 * 60 * 1000;
|
|
200
|
+
|
|
201
|
+
/** High-impact releases whose time has come in the last {@link PRINT_PENDING_MS} and that have no actual yet. */
|
|
202
|
+
export function pendingHighImpactPrints(events: readonly EconEvent[], now: number): EconEvent[] {
|
|
203
|
+
return events.filter((event) => {
|
|
204
|
+
if (event.impact !== "high" || event.actual) return false;
|
|
205
|
+
const releasedFor = now - event.date.getTime();
|
|
206
|
+
return releasedFor >= 0 && releasedFor <= PRINT_PENDING_MS;
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function hasPendingHighImpactPrint(events: readonly EconEvent[], now: number): boolean {
|
|
211
|
+
return pendingHighImpactPrints(events, now).length > 0;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** High-impact rows that were blank in `previous` and have an actual in `next`. */
|
|
215
|
+
export function landedHighImpactPrints(
|
|
216
|
+
previous: ReadonlyMap<string, string | null>,
|
|
217
|
+
next: readonly EconEvent[],
|
|
218
|
+
): EconEvent[] {
|
|
219
|
+
return next.filter((event) => {
|
|
220
|
+
if (event.impact !== "high" || !event.actual || !previous.has(event.id)) return false;
|
|
221
|
+
return !previous.get(event.id);
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
194
225
|
export function formatCountdown(ms: number): string {
|
|
195
226
|
if (ms <= 60_000) return "in <1m";
|
|
196
227
|
const totalMin = Math.floor(ms / 60_000);
|
|
@@ -229,6 +260,10 @@ export function getCalendarCache(options?: { allowExpired?: boolean }): EconCale
|
|
|
229
260
|
return cache.get(CACHE_KEY, options);
|
|
230
261
|
}
|
|
231
262
|
|
|
263
|
+
export function subscribeCalendar(listener: () => void): () => void {
|
|
264
|
+
return cache.subscribe(CACHE_KEY, listener);
|
|
265
|
+
}
|
|
266
|
+
|
|
232
267
|
export interface EconCalendarLoadResult extends EconCalendarCacheEntry {
|
|
233
268
|
refreshError?: string;
|
|
234
269
|
}
|
|
@@ -20,6 +20,7 @@ import type { PluginModule } from "../plugin-module";
|
|
|
20
20
|
import { colors, blendHex } from "../../../theme/colors";
|
|
21
21
|
import type { EconEvent } from "./types";
|
|
22
22
|
import { FRED_CATALOG_SERIES } from "./fred-catalog";
|
|
23
|
+
import { ECON_PRINT_NOTIFICATIONS_ENABLED_KEY, retainVisiblePrintRefresh, setupHighImpactPrintNotifications } from "./print-watch";
|
|
23
24
|
import { EconDetailView } from "./detail-view";
|
|
24
25
|
import {
|
|
25
26
|
COUNTRY_CYCLE,
|
|
@@ -35,6 +36,7 @@ import {
|
|
|
35
36
|
formatCountdown,
|
|
36
37
|
formatStaleness,
|
|
37
38
|
getCalendarCache,
|
|
39
|
+
subscribeCalendar,
|
|
38
40
|
impactIndicator,
|
|
39
41
|
loadCalendar,
|
|
40
42
|
matchesCountry,
|
|
@@ -75,10 +77,16 @@ function EconCalendarPane({ focused, width, height }: PaneProps) {
|
|
|
75
77
|
// loadCalendar serves a fresh cache without a request, so the pane can always
|
|
76
78
|
// ask and still follow the global cadence once the cache goes stale.
|
|
77
79
|
const calendar = useAsyncResource<CalendarResult>(loadEvents, { initialData: cachedEvents });
|
|
80
|
+
// A load made elsewhere, such as the print watch asking while a release is
|
|
81
|
+
// due, reaches the open pane through the cache; the newer of the two wins.
|
|
82
|
+
const [cacheVersion, setCacheVersion] = useState(0);
|
|
83
|
+
useEffect(() => subscribeCalendar(() => setCacheVersion((version) => version + 1)), []);
|
|
84
|
+
const cached = useMemo(() => getCalendarCache(), [cacheVersion]);
|
|
85
|
+
const shown = cached && (!calendar.data || cached.fetchedAt > calendar.data.fetchedAt) ? cached : calendar.data;
|
|
78
86
|
const { loading } = calendar;
|
|
79
|
-
const events =
|
|
80
|
-
const stale =
|
|
81
|
-
const fetchedAt =
|
|
87
|
+
const events = shown?.data ?? NO_EVENTS;
|
|
88
|
+
const stale = shown?.stale ?? false;
|
|
89
|
+
const fetchedAt = shown?.fetchedAt ?? null;
|
|
82
90
|
// A cache that could not be refreshed arrives as a result, not a failure.
|
|
83
91
|
const error = calendar.error ?? (loading ? null : calendar.data?.refreshError ?? null);
|
|
84
92
|
// The selected row and the open event are remembered by event id, so a
|
|
@@ -94,8 +102,11 @@ function EconCalendarPane({ focused, width, height }: PaneProps) {
|
|
|
94
102
|
|
|
95
103
|
useAutoRefresh(stale ? null : fetchedAt, calendar.load);
|
|
96
104
|
|
|
97
|
-
// Tick every 30s to update staleness + countdown, only while the pane can be seen.
|
|
98
105
|
const paneVisible = usePaneVisible();
|
|
106
|
+
// In view, a due high-impact print is asked for on the print watch's timing
|
|
107
|
+
// and lands here through the cache; a covered or hidden pane asks nothing.
|
|
108
|
+
useEffect(() => (paneVisible ? retainVisiblePrintRefresh() : undefined), [paneVisible]);
|
|
109
|
+
// Tick every 30s to update staleness + countdown, only while the pane can be seen.
|
|
99
110
|
useEffect(() => {
|
|
100
111
|
if (!paneVisible) return;
|
|
101
112
|
setNow(Date.now());
|
|
@@ -437,6 +448,8 @@ function EconCalendarPane({ focused, width, height }: PaneProps) {
|
|
|
437
448
|
);
|
|
438
449
|
}
|
|
439
450
|
|
|
451
|
+
let stopPrintWatch: (() => void) | null = null;
|
|
452
|
+
|
|
440
453
|
export const economicCalendarModule: PluginModule = {
|
|
441
454
|
series: FRED_CATALOG_SERIES,
|
|
442
455
|
panes: [{
|
|
@@ -449,6 +462,16 @@ export const economicCalendarModule: PluginModule = {
|
|
|
449
462
|
defaultMode: "floating",
|
|
450
463
|
defaultFloatingSize: { width: 100, height: 30 },
|
|
451
464
|
tableExport: true,
|
|
465
|
+
settings: {
|
|
466
|
+
title: "Economic Calendar Settings",
|
|
467
|
+
fields: [{
|
|
468
|
+
key: ECON_PRINT_NOTIFICATIONS_ENABLED_KEY,
|
|
469
|
+
label: "Notifications",
|
|
470
|
+
description: "Notify when a high-impact release prints, even while this pane is closed.",
|
|
471
|
+
type: "toggle",
|
|
472
|
+
storage: "plugin",
|
|
473
|
+
}],
|
|
474
|
+
},
|
|
452
475
|
}],
|
|
453
476
|
paneTemplates: [{
|
|
454
477
|
id: "econ-calendar-pane",
|
|
@@ -461,8 +484,12 @@ export const economicCalendarModule: PluginModule = {
|
|
|
461
484
|
}],
|
|
462
485
|
setup(ctx) {
|
|
463
486
|
attachEconCalendarPersistence(ctx.persistence);
|
|
487
|
+
stopPrintWatch?.();
|
|
488
|
+
stopPrintWatch = setupHighImpactPrintNotifications(ctx);
|
|
464
489
|
},
|
|
465
490
|
dispose() {
|
|
491
|
+
stopPrintWatch?.();
|
|
492
|
+
stopPrintWatch = null;
|
|
466
493
|
resetEconCalendarPersistence();
|
|
467
494
|
},
|
|
468
495
|
};
|