gloomberb 0.7.2 → 0.7.4
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 +79 -132
- package/package.json +1 -1
- package/src/app.tsx +73 -67
- package/src/components/chart/chart-data.ts +7 -2
- package/src/components/chart/chart-pane-settings.ts +7 -4
- package/src/components/chart/chart-renderer.ts +158 -47
- package/src/components/chart/chart-types.ts +20 -2
- package/src/components/chart/chart.test.ts +179 -13
- package/src/components/chart/comparison-chart-renderer.test.ts +14 -30
- package/src/components/chart/comparison-chart-renderer.ts +50 -11
- package/src/components/chart/comparison-stock-chart.tsx +129 -129
- package/src/components/chart/market-session.ts +187 -0
- package/src/components/chart/native/chart-rasterizer.ts +108 -19
- package/src/components/chart/price-axis-labels.tsx +25 -9
- package/src/components/chart/stock-chart-renderer-switch.test.tsx +97 -1
- package/src/components/chart/stock-chart.tsx +95 -49
- package/src/components/chart/time-axis-label.tsx +56 -2
- package/src/components/command-bar/command-bar.test.tsx +446 -120
- package/src/components/command-bar/command-bar.tsx +535 -384
- package/src/components/command-bar/command-registry.ts +16 -6
- package/src/components/command-bar/list-model.ts +8 -4
- package/src/components/command-bar/root-shortcuts.ts +4 -5
- package/src/components/command-bar/view-model.test.ts +21 -0
- package/src/components/command-bar/view-model.ts +17 -4
- package/src/components/command-bar/workflow-ops.ts +11 -32
- package/src/components/data-table-view.test.tsx +24 -1
- package/src/components/data-table-view.tsx +3 -2
- package/src/components/feed-data-table-stack-view.tsx +3 -2
- package/src/components/layout/detached-pane-shell.tsx +2 -2
- package/src/components/layout/floating-pane.tsx +9 -2
- package/src/components/layout/pane-footer.tsx +1 -1
- package/src/components/layout/pane-header.tsx +4 -2
- package/src/components/layout/pane-sizing.ts +12 -0
- package/src/components/layout/pane.tsx +24 -2
- package/src/components/layout/shell.tsx +24 -10
- package/src/components/layout/status-bar.test.tsx +4 -1
- package/src/components/layout/status-bar.tsx +4 -24
- package/src/components/markdown-text.tsx +53 -8
- package/src/components/onboarding/onboarding-wizard.tsx +7 -6
- package/src/components/pane-settings-dialog.tsx +9 -6
- package/src/components/pane-template-wizard.tsx +3 -2
- package/src/components/price-selector-dialog.tsx +3 -2
- package/src/components/price-sparkline-view.tsx +463 -0
- package/src/components/price-sparkline.ts +46 -0
- package/src/components/table-view-shared.tsx +21 -3
- package/src/components/ticker-list-table-view.tsx +3 -2
- package/src/components/ticker-list-table.tsx +15 -66
- package/src/components/toggle-list.tsx +3 -3
- package/src/components/ui/choice-dialog.tsx +3 -2
- package/src/components/ui/multi-select-dialog.tsx +3 -2
- package/src/components/ui/native-select.tsx +3 -3
- package/src/core/state/app-state.ts +210 -80
- package/src/core/state/session-persistence.ts +25 -0
- package/src/data/config-store-node.ts +113 -25
- package/src/data/config-store.test.ts +65 -3
- package/src/data/plugin-state-store.ts +39 -13
- package/src/data/sqlite-cache.test.ts +13 -0
- package/src/market-data/coordinator-subscriptions.test.ts +102 -18
- package/src/market-data/coordinator.test.ts +122 -2
- package/src/market-data/coordinator.ts +200 -56
- package/src/market-data/hooks.test.tsx +31 -0
- package/src/market-data/hooks.tsx +1 -0
- package/src/news/aggregator.test.ts +41 -0
- package/src/news/aggregator.ts +61 -9
- package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +25 -6
- package/src/plugins/builtin/ai/screener-pane.tsx +3 -0
- package/src/plugins/builtin/ai/settings.ts +3 -0
- package/src/plugins/builtin/broker-manager/index.tsx +4 -3
- package/src/plugins/builtin/chat-controller.test.ts +398 -17
- package/src/plugins/builtin/chat-controller.ts +263 -48
- package/src/plugins/builtin/chat.test.tsx +312 -6
- package/src/plugins/builtin/chat.tsx +245 -66
- package/src/plugins/builtin/comparison-chart.test.tsx +117 -3
- package/src/plugins/builtin/correlation/colors.test.ts +39 -0
- package/src/plugins/builtin/correlation/colors.ts +87 -0
- package/src/plugins/builtin/correlation/index.test.tsx +151 -0
- package/src/plugins/builtin/correlation/index.tsx +92 -21
- package/src/plugins/builtin/debug.tsx +4 -3
- package/src/plugins/builtin/econ/index.tsx +3 -2
- package/src/plugins/builtin/help.test.tsx +2 -2
- package/src/plugins/builtin/holders/index.tsx +25 -13
- package/src/plugins/builtin/layout-manager.tsx +1 -7
- package/src/plugins/builtin/market-movers/index.tsx +72 -40
- package/src/plugins/builtin/news-wire/breaking-pane.test.tsx +156 -0
- package/src/plugins/builtin/news-wire/breaking-pane.tsx +0 -90
- package/src/plugins/builtin/news-wire/index.tsx +1 -3
- package/src/plugins/builtin/news-wire/news-detail-view.tsx +3 -2
- package/src/plugins/builtin/news-wire/news-preset-pane.tsx +4 -1
- package/src/plugins/builtin/news-wire/persisted-articles.ts +7 -1
- package/src/plugins/builtin/options.tsx +9 -17
- package/src/plugins/builtin/portfolio-list/cli/commands.ts +6 -10
- package/src/plugins/builtin/portfolio-list/metrics.ts +13 -2
- package/src/plugins/builtin/portfolio-list/mutations.ts +17 -0
- package/src/plugins/builtin/portfolio-list/pane.tsx +139 -23
- package/src/plugins/builtin/portfolio-list/quick-add.tsx +422 -0
- package/src/plugins/builtin/portfolio-list/settings.ts +20 -12
- package/src/plugins/builtin/portfolio-list/table.tsx +3 -3
- package/src/plugins/builtin/portfolio-list.test.tsx +311 -9
- package/src/plugins/builtin/quote-monitor.test.tsx +158 -25
- package/src/plugins/builtin/sectors/index.tsx +275 -43
- package/src/plugins/builtin/sectors/sector-data.ts +41 -1
- package/src/plugins/builtin/ticker-detail/chart-tab.tsx +3 -2
- package/src/plugins/builtin/ticker-detail/index.tsx +30 -9
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +25 -15
- package/src/plugins/builtin/ticker-detail/pane.tsx +18 -7
- package/src/plugins/builtin/ticker-detail/quote-monitor.tsx +445 -63
- package/src/plugins/builtin/ticker-detail/settings.ts +84 -3
- package/src/plugins/builtin/ticker-detail.test.tsx +2 -2
- package/src/plugins/builtin/world-indices/index.tsx +58 -19
- package/src/plugins/ibkr/trading-pane.tsx +3 -2
- package/src/plugins/plugin-runtime.tsx +2 -2
- package/src/plugins/prediction-markets/settings.ts +4 -0
- package/src/plugins/registry.test.ts +55 -0
- package/src/plugins/registry.ts +11 -4
- package/src/plugins/ticker-navigation.test.ts +35 -5
- package/src/plugins/ticker-navigation.ts +16 -22
- package/src/renderers/electrobun/bun/application-menu-click.test.ts +9 -0
- package/src/renderers/electrobun/bun/application-menu-click.ts +7 -4
- package/src/renderers/electrobun/bun/application-menu.ts +6 -1
- package/src/renderers/electrobun/bun/desktop-workspace.ts +47 -29
- package/src/renderers/electrobun/bun/index.ts +52 -13
- package/src/renderers/electrobun/view/app-services.ts +51 -9
- package/src/renderers/electrobun/view/backend-rpc.ts +5 -8
- package/src/renderers/electrobun/view/desktop-controls.tsx +4 -4
- package/src/renderers/electrobun/view/main.tsx +0 -16
- package/src/renderers/electrobun/view/styles.css +23 -21
- package/src/renderers/electrobun/view/ui-host.tsx +6 -4
- package/src/renderers/opentui/host.tsx +2 -1
- package/src/renderers/opentui/start.tsx +1 -1
- package/src/renderers/opentui/test-utils.tsx +6 -5
- package/src/sources/gloomberb-cloud.test.ts +55 -2
- package/src/sources/gloomberb-cloud.ts +180 -29
- package/src/sources/provider-router.test.ts +567 -10
- package/src/sources/provider-router.ts +323 -93
- package/src/sources/yahoo-finance.test.ts +37 -0
- package/src/sources/yahoo-finance.ts +48 -2
- package/src/state/app-bootstrap.test.ts +50 -23
- package/src/state/app-bootstrap.ts +29 -13
- package/src/state/app-context.test.ts +113 -0
- package/src/state/app-context.tsx +8 -4
- package/src/state/session-persistence.test.ts +5 -3
- package/src/state/use-inline-tickers.ts +3 -0
- package/src/state/use-quote-streaming.ts +11 -0
- package/src/theme/colors.ts +13 -14
- package/src/types/config.ts +9 -4
- package/src/types/data-provider.ts +5 -1
- package/src/types/financials.ts +1 -0
- package/src/types/plugin.ts +8 -1
- package/src/ui/context-menu.tsx +4 -4
- package/src/utils/api-client.test.ts +248 -0
- package/src/utils/api-client.ts +178 -26
- package/src/utils/exchanges.test.ts +16 -0
- package/src/utils/exchanges.ts +33 -0
- package/src/utils/fuzzy-search.ts +12 -1
- package/src/utils/keyboard.ts +31 -0
- package/src/utils/options.ts +1 -1
- package/src/utils/price-history.test.ts +28 -1
- package/src/utils/price-history.ts +16 -0
- package/src/utils/quote-freshness.test.ts +150 -0
- package/src/utils/quote-freshness.ts +42 -1
- package/src/utils/quote-resolution.ts +2 -0
- package/src/utils/quote-time.test.ts +34 -0
- package/src/utils/quote-time.ts +18 -5
- package/src/utils/ticker-search.test.ts +2 -2
- package/src/utils/ticker-search.ts +48 -1
- package/src/version.ts +1 -1
- package/src/plugins/builtin/news-wire/digest-store.ts +0 -44
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { CANONICAL_EXCHANGE_ALIASES, EXCHANGE_TIME_ZONES } from "./exchanges";
|
|
3
|
+
|
|
4
|
+
describe("exchange metadata", () => {
|
|
5
|
+
test("has a valid timezone for every canonical exchange", () => {
|
|
6
|
+
const canonicalExchanges = [...new Set(Object.values(CANONICAL_EXCHANGE_ALIASES))].sort();
|
|
7
|
+
const missing = canonicalExchanges.filter((exchange) => !EXCHANGE_TIME_ZONES[exchange]);
|
|
8
|
+
|
|
9
|
+
expect(missing).toEqual([]);
|
|
10
|
+
for (const exchange of canonicalExchanges) {
|
|
11
|
+
expect(() => new Intl.DateTimeFormat("en-US", {
|
|
12
|
+
timeZone: EXCHANGE_TIME_ZONES[exchange],
|
|
13
|
+
})).not.toThrow();
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
});
|
package/src/utils/exchanges.ts
CHANGED
|
@@ -31,6 +31,7 @@ export const CANONICAL_EXCHANGE_ALIASES: Record<string, string> = {
|
|
|
31
31
|
TSX: "TSX",
|
|
32
32
|
XTSE: "TSX",
|
|
33
33
|
TSXV: "TSXV",
|
|
34
|
+
VAN: "TSXV",
|
|
34
35
|
CN: "CSE",
|
|
35
36
|
CNSX: "CSE",
|
|
36
37
|
CSE: "CSE",
|
|
@@ -118,6 +119,7 @@ export const CANONICAL_EXCHANGE_ALIASES: Record<string, string> = {
|
|
|
118
119
|
XJSE: "JSE",
|
|
119
120
|
TLV: "TASE",
|
|
120
121
|
XTAE: "TASE",
|
|
122
|
+
CCC: "CCC",
|
|
121
123
|
};
|
|
122
124
|
|
|
123
125
|
export const PUBLIC_EXCHANGE_ALIASES: Record<string, string> = {
|
|
@@ -128,6 +130,8 @@ export const PUBLIC_EXCHANGE_ALIASES: Record<string, string> = {
|
|
|
128
130
|
HKEX: "XHKG",
|
|
129
131
|
LSE: "XLON",
|
|
130
132
|
TSX: "XTSE",
|
|
133
|
+
TSXV: "XTSX",
|
|
134
|
+
CSE: "XCNQ",
|
|
131
135
|
ASX: "XASX",
|
|
132
136
|
NZX: "XNZE",
|
|
133
137
|
SWX: "XSWX",
|
|
@@ -172,6 +176,21 @@ export const EXCHANGE_TIME_ZONES: Record<string, string> = {
|
|
|
172
176
|
SFB: "Europe/Stockholm",
|
|
173
177
|
HKEX: "Asia/Hong_Kong",
|
|
174
178
|
JPX: "Asia/Tokyo",
|
|
179
|
+
TSX: "America/Toronto",
|
|
180
|
+
TSXV: "America/Toronto",
|
|
181
|
+
CSE: "America/Toronto",
|
|
182
|
+
EPA: "Europe/Paris",
|
|
183
|
+
AMS: "Europe/Amsterdam",
|
|
184
|
+
BRU: "Europe/Brussels",
|
|
185
|
+
LIS: "Europe/Lisbon",
|
|
186
|
+
BIT: "Europe/Rome",
|
|
187
|
+
HEL: "Europe/Helsinki",
|
|
188
|
+
CPH: "Europe/Copenhagen",
|
|
189
|
+
OSL: "Europe/Oslo",
|
|
190
|
+
ICEX: "Atlantic/Reykjavik",
|
|
191
|
+
WSE: "Europe/Warsaw",
|
|
192
|
+
PSE: "Europe/Prague",
|
|
193
|
+
VIE: "Europe/Vienna",
|
|
175
194
|
TPEX: "Asia/Taipei",
|
|
176
195
|
TWSE: "Asia/Taipei",
|
|
177
196
|
NSE: "Asia/Kolkata",
|
|
@@ -181,6 +200,15 @@ export const EXCHANGE_TIME_ZONES: Record<string, string> = {
|
|
|
181
200
|
SGX: "Asia/Singapore",
|
|
182
201
|
KRX: "Asia/Seoul",
|
|
183
202
|
KOSDAQ: "Asia/Seoul",
|
|
203
|
+
NZX: "Pacific/Auckland",
|
|
204
|
+
SSE: "Asia/Shanghai",
|
|
205
|
+
SZSE: "Asia/Shanghai",
|
|
206
|
+
BMV: "America/Mexico_City",
|
|
207
|
+
B3: "America/Sao_Paulo",
|
|
208
|
+
BYMA: "America/Argentina/Buenos_Aires",
|
|
209
|
+
JSE: "Africa/Johannesburg",
|
|
210
|
+
TASE: "Asia/Jerusalem",
|
|
211
|
+
CCC: "UTC",
|
|
184
212
|
};
|
|
185
213
|
|
|
186
214
|
export function normalizeSymbol(value: string): string {
|
|
@@ -196,6 +224,11 @@ export function canonicalExchange(value?: string): string {
|
|
|
196
224
|
return CANONICAL_EXCHANGE_ALIASES[normalized] ?? normalized;
|
|
197
225
|
}
|
|
198
226
|
|
|
227
|
+
export function resolveExchangeTimeZone(value?: string): string | null {
|
|
228
|
+
const canonical = canonicalExchange(value);
|
|
229
|
+
return canonical ? EXCHANGE_TIME_ZONES[canonical] ?? null : null;
|
|
230
|
+
}
|
|
231
|
+
|
|
199
232
|
export function publicExchange(value?: string): string {
|
|
200
233
|
const canonical = canonicalExchange(value);
|
|
201
234
|
return PUBLIC_EXCHANGE_ALIASES[canonical] ?? canonical;
|
|
@@ -5,7 +5,18 @@ export function fuzzyMatch(query: string, target: string): { match: boolean; sco
|
|
|
5
5
|
const t = target.toLowerCase();
|
|
6
6
|
|
|
7
7
|
if (q.length === 0) return { match: true, score: 0 };
|
|
8
|
-
if (t
|
|
8
|
+
if (t === q) return { match: true, score: 2500 - q.length };
|
|
9
|
+
|
|
10
|
+
const tokens = t.split(/\s+/).filter(Boolean);
|
|
11
|
+
const exactTokenIndex = tokens.findIndex((token) => token === q);
|
|
12
|
+
if (exactTokenIndex >= 0) return { match: true, score: 2200 - q.length - exactTokenIndex };
|
|
13
|
+
|
|
14
|
+
const prefixTokenIndex = tokens.findIndex((token) => token.startsWith(q));
|
|
15
|
+
if (prefixTokenIndex >= 0) {
|
|
16
|
+
const token = tokens[prefixTokenIndex]!;
|
|
17
|
+
return { match: true, score: 1000 - q.length - prefixTokenIndex - (token.length - q.length) };
|
|
18
|
+
}
|
|
19
|
+
|
|
9
20
|
if (t.includes(q)) return { match: true, score: 500 - q.length }; // substring match
|
|
10
21
|
|
|
11
22
|
let qi = 0;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export interface KeyboardModifierEventLike {
|
|
2
|
+
ctrl?: boolean;
|
|
3
|
+
meta?: boolean;
|
|
4
|
+
super?: boolean;
|
|
5
|
+
alt?: boolean;
|
|
6
|
+
option?: boolean;
|
|
7
|
+
shift?: boolean;
|
|
8
|
+
name?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function hasKeyboardModifier(event: KeyboardModifierEventLike): boolean {
|
|
12
|
+
return !!(
|
|
13
|
+
event.ctrl
|
|
14
|
+
|| event.meta
|
|
15
|
+
|| event.super
|
|
16
|
+
|| event.alt
|
|
17
|
+
|| event.option
|
|
18
|
+
|| event.shift
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function isPlainKeyboardEvent(event: KeyboardModifierEventLike): boolean {
|
|
23
|
+
return !hasKeyboardModifier(event);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function isPlainKey(
|
|
27
|
+
event: KeyboardModifierEventLike,
|
|
28
|
+
...names: string[]
|
|
29
|
+
): boolean {
|
|
30
|
+
return isPlainKeyboardEvent(event) && names.includes(event.name ?? "");
|
|
31
|
+
}
|
package/src/utils/options.ts
CHANGED
|
@@ -23,7 +23,7 @@ export function parseOptionSymbol(symbol: string): { underlying: string; expTs:
|
|
|
23
23
|
const year = 2000 + parseInt(yy!, 10);
|
|
24
24
|
const month = parseInt(mm!, 10) - 1;
|
|
25
25
|
const day = parseInt(dd!, 10);
|
|
26
|
-
const expTs = Math.floor(
|
|
26
|
+
const expTs = Math.floor(Date.UTC(year, month, day) / 1000);
|
|
27
27
|
return { underlying: underlying!, expTs, strike, side: side as "C" | "P" };
|
|
28
28
|
}
|
|
29
29
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import { normalizePriceHistory } from "./price-history";
|
|
2
|
+
import { isPriceHistoryStaleForCurrentWindow, normalizePriceHistory } from "./price-history";
|
|
3
3
|
|
|
4
4
|
describe("normalizePriceHistory", () => {
|
|
5
5
|
test("drops poisoned cached history when every timestamp collapses to the same value", () => {
|
|
@@ -22,4 +22,31 @@ describe("normalizePriceHistory", () => {
|
|
|
22
22
|
|
|
23
23
|
expect(history.map((point) => point.close)).toEqual([101, 102, 103]);
|
|
24
24
|
});
|
|
25
|
+
|
|
26
|
+
test("drops zero-price bars instead of treating missing upstream prices as real data", () => {
|
|
27
|
+
const history = normalizePriceHistory([
|
|
28
|
+
{ date: new Date("2026-05-13T13:30:00Z"), close: 64 },
|
|
29
|
+
{ date: new Date("2026-05-13T13:45:00Z"), close: 0 },
|
|
30
|
+
{ date: new Date("2026-05-13T14:00:00Z"), close: Number.NaN },
|
|
31
|
+
{ date: new Date("2026-05-13T14:15:00Z"), close: 65 },
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
expect(history.map((point) => point.close)).toEqual([64, 65]);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("detects intraday history that is old even when the cache record is fresh", () => {
|
|
38
|
+
expect(
|
|
39
|
+
isPriceHistoryStaleForCurrentWindow(
|
|
40
|
+
[{ date: new Date("2026-04-17T20:00:00Z"), close: 67 }],
|
|
41
|
+
Date.parse("2026-05-13T23:00:00Z"),
|
|
42
|
+
),
|
|
43
|
+
).toBe(true);
|
|
44
|
+
|
|
45
|
+
expect(
|
|
46
|
+
isPriceHistoryStaleForCurrentWindow(
|
|
47
|
+
[{ date: new Date("2026-05-13T22:45:00Z"), close: 67 }],
|
|
48
|
+
Date.parse("2026-05-13T23:00:00Z"),
|
|
49
|
+
),
|
|
50
|
+
).toBe(false);
|
|
51
|
+
});
|
|
25
52
|
});
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { PricePoint, TickerFinancials } from "../types/financials";
|
|
2
2
|
|
|
3
|
+
const MAX_CURRENT_INTRADAY_HISTORY_LAG_MS = 18 * 60 * 60 * 1000;
|
|
4
|
+
|
|
3
5
|
function getPointTimestamp(point: PricePoint): number {
|
|
4
6
|
const value = point.date as Date | string | number | null | undefined;
|
|
5
7
|
if (value instanceof Date) return value.getTime();
|
|
@@ -7,6 +9,10 @@ function getPointTimestamp(point: PricePoint): number {
|
|
|
7
9
|
return new Date(value).getTime();
|
|
8
10
|
}
|
|
9
11
|
|
|
12
|
+
function hasValidClose(point: PricePoint): boolean {
|
|
13
|
+
return Number.isFinite(point.close) && point.close > 0;
|
|
14
|
+
}
|
|
15
|
+
|
|
10
16
|
function comparePricePointsByDate(left: PricePoint, right: PricePoint): number {
|
|
11
17
|
const leftTime = getPointTimestamp(left);
|
|
12
18
|
const rightTime = getPointTimestamp(right);
|
|
@@ -31,6 +37,7 @@ export function normalizePriceHistory(points: PricePoint[]): PricePoint[] {
|
|
|
31
37
|
for (const point of points) {
|
|
32
38
|
const time = getPointTimestamp(point);
|
|
33
39
|
if (!Number.isFinite(time)) continue;
|
|
40
|
+
if (!hasValidClose(point)) continue;
|
|
34
41
|
|
|
35
42
|
if (firstTimestamp === null) {
|
|
36
43
|
firstTimestamp = time;
|
|
@@ -55,6 +62,15 @@ export function normalizePriceHistory(points: PricePoint[]): PricePoint[] {
|
|
|
55
62
|
return validPoints.length === points.length ? points : validPoints;
|
|
56
63
|
}
|
|
57
64
|
|
|
65
|
+
export function isPriceHistoryStaleForCurrentWindow(points: PricePoint[], now = Date.now()): boolean {
|
|
66
|
+
const normalized = normalizePriceHistory(points);
|
|
67
|
+
const latest = normalized.at(-1);
|
|
68
|
+
if (!latest) return false;
|
|
69
|
+
|
|
70
|
+
const latestTime = getPointTimestamp(latest);
|
|
71
|
+
return Number.isFinite(latestTime) && now - latestTime > MAX_CURRENT_INTRADAY_HISTORY_LAG_MS;
|
|
72
|
+
}
|
|
73
|
+
|
|
58
74
|
export function normalizeTickerFinancialsPriceHistory(financials: TickerFinancials): TickerFinancials {
|
|
59
75
|
const priceHistory = normalizePriceHistory(financials.priceHistory ?? []);
|
|
60
76
|
return priceHistory === financials.priceHistory
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import type { Quote } from "../types/financials";
|
|
3
|
+
import { isQuoteStaleForCurrentSession } from "./quote-freshness";
|
|
4
|
+
|
|
5
|
+
function quote(overrides: Partial<Quote>): Quote {
|
|
6
|
+
return {
|
|
7
|
+
symbol: "2337",
|
|
8
|
+
price: 168,
|
|
9
|
+
currency: "TWD",
|
|
10
|
+
change: 0,
|
|
11
|
+
changePercent: 0,
|
|
12
|
+
lastUpdated: Date.parse("2026-05-08T06:00:00Z"),
|
|
13
|
+
listingExchangeName: "TWSE",
|
|
14
|
+
exchangeName: "TWSE",
|
|
15
|
+
marketState: "CLOSED",
|
|
16
|
+
dataSource: "delayed",
|
|
17
|
+
...overrides,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe("quote freshness", () => {
|
|
22
|
+
test("treats multi-business-day-old non-US closed quotes as stale", () => {
|
|
23
|
+
expect(
|
|
24
|
+
isQuoteStaleForCurrentSession(
|
|
25
|
+
quote({}),
|
|
26
|
+
Date.parse("2026-05-13T21:00:00Z"),
|
|
27
|
+
),
|
|
28
|
+
).toBe(true);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("treats stale Paris and Toronto quotes as refreshable", () => {
|
|
32
|
+
const now = Date.parse("2026-05-13T21:00:00Z");
|
|
33
|
+
|
|
34
|
+
expect(
|
|
35
|
+
isQuoteStaleForCurrentSession(
|
|
36
|
+
quote({
|
|
37
|
+
symbol: "ALRIB",
|
|
38
|
+
currency: "EUR",
|
|
39
|
+
lastUpdated: Date.parse("2026-05-08T15:30:00Z"),
|
|
40
|
+
listingExchangeName: "PAR",
|
|
41
|
+
exchangeName: "PAR",
|
|
42
|
+
}),
|
|
43
|
+
now,
|
|
44
|
+
),
|
|
45
|
+
).toBe(true);
|
|
46
|
+
|
|
47
|
+
expect(
|
|
48
|
+
isQuoteStaleForCurrentSession(
|
|
49
|
+
quote({
|
|
50
|
+
symbol: "HPS-A.TO",
|
|
51
|
+
currency: "CAD",
|
|
52
|
+
lastUpdated: Date.parse("2026-05-08T20:00:00Z"),
|
|
53
|
+
listingExchangeName: "TOR",
|
|
54
|
+
exchangeName: "TOR",
|
|
55
|
+
}),
|
|
56
|
+
now,
|
|
57
|
+
),
|
|
58
|
+
).toBe(true);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("treats old crypto quotes as stale on the 24/7 venue", () => {
|
|
62
|
+
expect(
|
|
63
|
+
isQuoteStaleForCurrentSession(
|
|
64
|
+
quote({
|
|
65
|
+
symbol: "BTC-USD",
|
|
66
|
+
currency: "USD",
|
|
67
|
+
lastUpdated: Date.parse("2026-05-09T15:29:00Z"),
|
|
68
|
+
listingExchangeName: "CCC",
|
|
69
|
+
exchangeName: "CCC",
|
|
70
|
+
marketState: "REGULAR",
|
|
71
|
+
}),
|
|
72
|
+
Date.parse("2026-05-13T21:00:00Z"),
|
|
73
|
+
),
|
|
74
|
+
).toBe(true);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("allows the previous local business day before the market reopens", () => {
|
|
78
|
+
expect(
|
|
79
|
+
isQuoteStaleForCurrentSession(
|
|
80
|
+
quote({
|
|
81
|
+
symbol: "1211",
|
|
82
|
+
currency: "HKD",
|
|
83
|
+
lastUpdated: Date.parse("2026-05-13T08:00:00Z"),
|
|
84
|
+
listingExchangeName: "SEHK",
|
|
85
|
+
exchangeName: "SEHK",
|
|
86
|
+
}),
|
|
87
|
+
Date.parse("2026-05-13T21:00:00Z"),
|
|
88
|
+
),
|
|
89
|
+
).toBe(false);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("rejects previous-day weekday quotes that are older than a normal overnight close", () => {
|
|
93
|
+
expect(
|
|
94
|
+
isQuoteStaleForCurrentSession(
|
|
95
|
+
quote({
|
|
96
|
+
symbol: "285A.T",
|
|
97
|
+
currency: "JPY",
|
|
98
|
+
lastUpdated: Date.parse("2026-05-12T21:00:00Z"),
|
|
99
|
+
listingExchangeName: "JPX",
|
|
100
|
+
exchangeName: "JPX",
|
|
101
|
+
}),
|
|
102
|
+
Date.parse("2026-05-13T21:00:00Z"),
|
|
103
|
+
),
|
|
104
|
+
).toBe(true);
|
|
105
|
+
|
|
106
|
+
expect(
|
|
107
|
+
isQuoteStaleForCurrentSession(
|
|
108
|
+
quote({
|
|
109
|
+
symbol: "285A.T",
|
|
110
|
+
currency: "JPY",
|
|
111
|
+
lastUpdated: Date.parse("2026-05-13T06:24:00Z"),
|
|
112
|
+
listingExchangeName: "JPX",
|
|
113
|
+
exchangeName: "JPX",
|
|
114
|
+
}),
|
|
115
|
+
Date.parse("2026-05-13T21:00:00Z"),
|
|
116
|
+
),
|
|
117
|
+
).toBe(false);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("allows Friday closes before Monday reopen", () => {
|
|
121
|
+
expect(
|
|
122
|
+
isQuoteStaleForCurrentSession(
|
|
123
|
+
quote({
|
|
124
|
+
symbol: "285A.T",
|
|
125
|
+
currency: "JPY",
|
|
126
|
+
lastUpdated: Date.parse("2026-05-08T06:24:00Z"),
|
|
127
|
+
listingExchangeName: "JPX",
|
|
128
|
+
exchangeName: "JPX",
|
|
129
|
+
}),
|
|
130
|
+
Date.parse("2026-05-10T21:00:00Z"),
|
|
131
|
+
),
|
|
132
|
+
).toBe(false);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("treats prior-date regular-session quotes as stale immediately", () => {
|
|
136
|
+
expect(
|
|
137
|
+
isQuoteStaleForCurrentSession(
|
|
138
|
+
quote({
|
|
139
|
+
symbol: "HY9H",
|
|
140
|
+
currency: "EUR",
|
|
141
|
+
lastUpdated: Date.parse("2026-04-07T19:55:00Z"),
|
|
142
|
+
listingExchangeName: "FWB2",
|
|
143
|
+
exchangeName: "FWB2",
|
|
144
|
+
marketState: "REGULAR",
|
|
145
|
+
}),
|
|
146
|
+
Date.parse("2026-04-08T10:49:00Z"),
|
|
147
|
+
),
|
|
148
|
+
).toBe(true);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
@@ -3,6 +3,8 @@ import { canonicalExchange, EXCHANGE_TIME_ZONES } from "./exchanges";
|
|
|
3
3
|
|
|
4
4
|
const US_EXTENDED_HOURS_EXCHANGES = new Set(["NASDAQ", "NYSE", "AMEX", "ARCA", "BATS"]);
|
|
5
5
|
const exchangeLocalDateFormatters = new Map<string, Intl.DateTimeFormat>();
|
|
6
|
+
const MS_PER_DAY = 24 * 60 * 60 * 1000;
|
|
7
|
+
const OVERNIGHT_CLOSE_MAX_AGE_MS = 20 * 60 * 60 * 1000;
|
|
6
8
|
const usSessionFormatter = new Intl.DateTimeFormat("en-US", {
|
|
7
9
|
timeZone: "America/New_York",
|
|
8
10
|
weekday: "short",
|
|
@@ -43,6 +45,38 @@ function exchangeLocalDate(exchange: string, timestampMs: number): string | null
|
|
|
43
45
|
return `${year}-${month}-${day}`;
|
|
44
46
|
}
|
|
45
47
|
|
|
48
|
+
function isoLocalDateToUtcDay(date: string): number | null {
|
|
49
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(date);
|
|
50
|
+
if (!match) return null;
|
|
51
|
+
|
|
52
|
+
const year = Number(match[1]);
|
|
53
|
+
const month = Number(match[2]);
|
|
54
|
+
const day = Number(match[3]);
|
|
55
|
+
if (!Number.isFinite(year) || !Number.isFinite(month) || !Number.isFinite(day)) return null;
|
|
56
|
+
|
|
57
|
+
return Math.floor(Date.UTC(year, month - 1, day) / MS_PER_DAY);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function localWeekdaysBetween(earlierDate: string, laterDate: string): number {
|
|
61
|
+
const earlierDay = isoLocalDateToUtcDay(earlierDate);
|
|
62
|
+
const laterDay = isoLocalDateToUtcDay(laterDate);
|
|
63
|
+
if (earlierDay == null || laterDay == null || laterDay <= earlierDay) return 0;
|
|
64
|
+
|
|
65
|
+
let weekdays = 0;
|
|
66
|
+
for (let day = earlierDay + 1; day <= laterDay; day += 1) {
|
|
67
|
+
const weekday = new Date(day * MS_PER_DAY).getUTCDay();
|
|
68
|
+
if (weekday !== 0 && weekday !== 6) {
|
|
69
|
+
weekdays += 1;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return weekdays;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function localWeekday(date: string): number | null {
|
|
76
|
+
const day = isoLocalDateToUtcDay(date);
|
|
77
|
+
return day == null ? null : new Date(day * MS_PER_DAY).getUTCDay();
|
|
78
|
+
}
|
|
79
|
+
|
|
46
80
|
function usSessionState(timestampMs: number): UsSessionState {
|
|
47
81
|
const parts = usSessionFormatter.formatToParts(new Date(timestampMs));
|
|
48
82
|
|
|
@@ -85,9 +119,16 @@ export function isQuoteStaleForCurrentSession(quote: Quote | null | undefined, n
|
|
|
85
119
|
|
|
86
120
|
if (isUsExtendedHoursExchange(exchange)) {
|
|
87
121
|
const session = usSessionState(now);
|
|
88
|
-
|
|
122
|
+
if (session === "PRE" || session === "REGULAR" || session === "POST") {
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
89
125
|
}
|
|
90
126
|
|
|
127
|
+
const weekdaysBehind = localWeekdaysBetween(quoteDate, currentDate);
|
|
128
|
+
if (weekdaysBehind > 1) return true;
|
|
129
|
+
if (weekdaysBehind === 1 && now - quote.lastUpdated > OVERNIGHT_CLOSE_MAX_AGE_MS) {
|
|
130
|
+
return localWeekday(currentDate) !== 1;
|
|
131
|
+
}
|
|
91
132
|
return false;
|
|
92
133
|
}
|
|
93
134
|
|
|
@@ -30,6 +30,7 @@ const PRICE_FIELD_KEYS = [
|
|
|
30
30
|
"low",
|
|
31
31
|
"mark",
|
|
32
32
|
"lastUpdated",
|
|
33
|
+
"receivedAt",
|
|
33
34
|
] as const;
|
|
34
35
|
|
|
35
36
|
const SESSION_FIELD_KEYS = [
|
|
@@ -516,6 +517,7 @@ export function resolveCanonicalQuote(
|
|
|
516
517
|
volume: resolved.volume as Quote["volume"],
|
|
517
518
|
name: resolved.name as Quote["name"],
|
|
518
519
|
lastUpdated: Number(resolved.lastUpdated ?? priceProvider?.lastUpdated ?? sessionProvider?.lastUpdated ?? now),
|
|
520
|
+
receivedAt: resolved.receivedAt as Quote["receivedAt"],
|
|
519
521
|
exchangeName: resolved.exchangeName as Quote["exchangeName"],
|
|
520
522
|
fullExchangeName: resolved.fullExchangeName as Quote["fullExchangeName"],
|
|
521
523
|
listingExchangeName: resolved.listingExchangeName as Quote["listingExchangeName"],
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { formatQuoteAge, formatQuoteAgeWithSource, getMostRecentQuoteUpdate } from "./quote-time";
|
|
3
|
+
|
|
4
|
+
describe("quote-time", () => {
|
|
5
|
+
test("formats sub-second quote age in milliseconds", () => {
|
|
6
|
+
const now = 1_700_000_030_000;
|
|
7
|
+
|
|
8
|
+
expect(formatQuoteAge(now - 999, now)).toBe("999ms");
|
|
9
|
+
expect(formatQuoteAge(now, now)).toBe("0ms");
|
|
10
|
+
expect(formatQuoteAgeWithSource({
|
|
11
|
+
lastUpdated: now - 100,
|
|
12
|
+
dataSource: "delayed",
|
|
13
|
+
}, now)).toBe("◷100ms");
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test("uses stream receipt time for displayed quote age when available", () => {
|
|
17
|
+
const now = 1_700_000_030_000;
|
|
18
|
+
const quote = {
|
|
19
|
+
lastUpdated: 1_700_000_000_000,
|
|
20
|
+
receivedAt: 1_700_000_029_000,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
expect(formatQuoteAgeWithSource(quote, now)).toBe("1s");
|
|
24
|
+
expect(getMostRecentQuoteUpdate([quote], now)).toBe(1_700_000_029_000);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("keeps delayed source marker while using receipt time for age", () => {
|
|
28
|
+
expect(formatQuoteAgeWithSource({
|
|
29
|
+
lastUpdated: 1_700_000_000_000,
|
|
30
|
+
receivedAt: 1_700_000_029_000,
|
|
31
|
+
dataSource: "delayed",
|
|
32
|
+
}, 1_700_000_030_000)).toBe("◷1s");
|
|
33
|
+
});
|
|
34
|
+
});
|
package/src/utils/quote-time.ts
CHANGED
|
@@ -11,20 +11,33 @@ export function formatQuoteAge(lastUpdated: number | undefined, now = Date.now()
|
|
|
11
11
|
const clamped = clampQuoteTimestamp(lastUpdated, now);
|
|
12
12
|
if (clamped == null) return "—";
|
|
13
13
|
|
|
14
|
-
const
|
|
14
|
+
const ageMs = Math.max(0, now - clamped);
|
|
15
|
+
if (ageMs < 1000) return `${Math.floor(ageMs)}ms`;
|
|
16
|
+
|
|
17
|
+
const ageSeconds = ageMs / 1000;
|
|
15
18
|
if (ageSeconds < 60) return `${Math.floor(ageSeconds)}s`;
|
|
16
19
|
if (ageSeconds < 3600) return `${Math.floor(ageSeconds / 60)}m`;
|
|
17
20
|
if (ageSeconds < 86400) return `${Math.floor(ageSeconds / 3600)}h`;
|
|
18
21
|
return `${Math.floor(ageSeconds / 86400)}d`;
|
|
19
22
|
}
|
|
20
23
|
|
|
24
|
+
export function resolveQuoteAgeTimestamp(
|
|
25
|
+
quote: Pick<Quote, "lastUpdated" | "receivedAt"> | null | undefined,
|
|
26
|
+
now = Date.now(),
|
|
27
|
+
): number | null {
|
|
28
|
+
if (!quote) return null;
|
|
29
|
+
const receivedAt = clampQuoteTimestamp(quote.receivedAt, now);
|
|
30
|
+
return receivedAt ?? clampQuoteTimestamp(quote.lastUpdated, now);
|
|
31
|
+
}
|
|
32
|
+
|
|
21
33
|
export function formatQuoteAgeWithSource(
|
|
22
|
-
quote: Pick<Quote, "lastUpdated" | "dataSource"> | null | undefined,
|
|
34
|
+
quote: Pick<Quote, "lastUpdated" | "receivedAt" | "dataSource"> | null | undefined,
|
|
23
35
|
now = Date.now(),
|
|
24
36
|
): string {
|
|
25
37
|
if (!quote) return "—";
|
|
26
38
|
|
|
27
|
-
const
|
|
39
|
+
const timestamp = resolveQuoteAgeTimestamp(quote, now);
|
|
40
|
+
const age = timestamp == null ? "—" : formatQuoteAge(timestamp, now);
|
|
28
41
|
if (age === "—") return age;
|
|
29
42
|
|
|
30
43
|
const prefix = quote.dataSource === "delayed" ? "◷" : "";
|
|
@@ -32,12 +45,12 @@ export function formatQuoteAgeWithSource(
|
|
|
32
45
|
}
|
|
33
46
|
|
|
34
47
|
export function getMostRecentQuoteUpdate(
|
|
35
|
-
quotes: Iterable<Pick<Quote, "lastUpdated"> | null | undefined>,
|
|
48
|
+
quotes: Iterable<Pick<Quote, "lastUpdated" | "receivedAt"> | null | undefined>,
|
|
36
49
|
now = Date.now(),
|
|
37
50
|
): number | null {
|
|
38
51
|
let latest: number | null = null;
|
|
39
52
|
for (const quote of quotes) {
|
|
40
|
-
const timestamp =
|
|
53
|
+
const timestamp = resolveQuoteAgeTimestamp(quote, now);
|
|
41
54
|
if (timestamp != null && (latest == null || timestamp > latest)) {
|
|
42
55
|
latest = timestamp;
|
|
43
56
|
}
|
|
@@ -82,7 +82,7 @@ describe("ticker-search utilities", () => {
|
|
|
82
82
|
]),
|
|
83
83
|
});
|
|
84
84
|
|
|
85
|
-
expect(results.map((item) => item.id)).toEqual(["goto:AAPL", "search:APP"]);
|
|
85
|
+
expect(results.map((item) => item.id)).toEqual(["goto:AAPL", "search:APP:NASDAQ:TEST"]);
|
|
86
86
|
expect(findExactTickerSearchMatch(results, "AAPL")?.id).toBe("goto:AAPL");
|
|
87
87
|
});
|
|
88
88
|
|
|
@@ -97,7 +97,7 @@ describe("ticker-search utilities", () => {
|
|
|
97
97
|
]),
|
|
98
98
|
});
|
|
99
99
|
|
|
100
|
-
expect(results.map((item) => item.id)).toEqual(["goto:AAPL", "search:APLE"]);
|
|
100
|
+
expect(results.map((item) => item.id)).toEqual(["goto:AAPL", "search:APLE:NASDAQ:TEST"]);
|
|
101
101
|
expect(results[0]).toMatchObject({
|
|
102
102
|
id: "goto:AAPL",
|
|
103
103
|
detail: "Apple Inc",
|
|
@@ -140,7 +140,7 @@ export function createProviderTickerSearchCandidates(
|
|
|
140
140
|
const symbol = getSearchResultSymbol(result);
|
|
141
141
|
const saved = localTickers.has(symbol);
|
|
142
142
|
return [{
|
|
143
|
-
id:
|
|
143
|
+
id: buildProviderCandidateId(result, symbol),
|
|
144
144
|
label: symbol,
|
|
145
145
|
symbol,
|
|
146
146
|
detail: [result.name, result.brokerLabel, result.type].filter(Boolean).join(" | "),
|
|
@@ -357,6 +357,7 @@ export function rankTickerSearchItems<T extends Pick<TickerSearchRankableItem, "
|
|
|
357
357
|
item,
|
|
358
358
|
index,
|
|
359
359
|
normalizedSymbol: normalizeSearchText(item.symbol || item.label),
|
|
360
|
+
symbolMatchRank: scoreSymbolMatchRank(intent, item),
|
|
360
361
|
textScore,
|
|
361
362
|
score: textScore + priorityScore + (textScore > 0 && saved ? 900 : 0),
|
|
362
363
|
};
|
|
@@ -375,6 +376,7 @@ export function rankTickerSearchItems<T extends Pick<TickerSearchRankableItem, "
|
|
|
375
376
|
return !matchedLocalSymbols.has(normalizedSymbol);
|
|
376
377
|
})
|
|
377
378
|
.sort((a, b) => {
|
|
379
|
+
if (b.symbolMatchRank !== a.symbolMatchRank) return b.symbolMatchRank - a.symbolMatchRank;
|
|
378
380
|
if (b.score !== a.score) return b.score - a.score;
|
|
379
381
|
const aSaved = isSavedSearchItem(a.item);
|
|
380
382
|
const bSaved = isSavedSearchItem(b.item);
|
|
@@ -463,6 +465,16 @@ function buildProviderSearchResultKey(result: InstrumentSearchResult): string {
|
|
|
463
465
|
].join("|");
|
|
464
466
|
}
|
|
465
467
|
|
|
468
|
+
function buildProviderCandidateId(result: InstrumentSearchResult, symbol: string): string {
|
|
469
|
+
return [
|
|
470
|
+
"search",
|
|
471
|
+
normalizeTickerSymbol(symbol),
|
|
472
|
+
normalizeSearchText(result.exchange || result.primaryExchange || result.type || ""),
|
|
473
|
+
normalizeSearchText(result.currency || ""),
|
|
474
|
+
normalizeSearchText(result.providerId || ""),
|
|
475
|
+
].filter(Boolean).join(":");
|
|
476
|
+
}
|
|
477
|
+
|
|
466
478
|
function getProviderHintRichness(result: InstrumentSearchResult): number {
|
|
467
479
|
let score = 0;
|
|
468
480
|
if (result.name) score += Math.min(120, result.name.length);
|
|
@@ -728,6 +740,41 @@ function scoreSearchAlias(intent: SearchQueryIntent, alias: string): number {
|
|
|
728
740
|
return score;
|
|
729
741
|
}
|
|
730
742
|
|
|
743
|
+
function scoreSymbolMatchRank(
|
|
744
|
+
intent: SearchQueryIntent,
|
|
745
|
+
item: Pick<TickerSearchRankableItem, "label"> & Partial<TickerSearchRankableItem>,
|
|
746
|
+
): number {
|
|
747
|
+
if (!intent.normalizedQuery && !intent.compactQuery) return 0;
|
|
748
|
+
const displaySymbol = normalizeSearchText(item.symbol || item.label);
|
|
749
|
+
const compactDisplaySymbol = compactSearchText(item.symbol || item.label);
|
|
750
|
+
|
|
751
|
+
if (
|
|
752
|
+
displaySymbol === intent.normalizedQuery
|
|
753
|
+
|| (intent.compactQuery && compactDisplaySymbol === intent.compactQuery)
|
|
754
|
+
) {
|
|
755
|
+
return 3;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
const aliases = getItemSearchAliases(item);
|
|
759
|
+
if (aliases.some((alias) => {
|
|
760
|
+
const normalizedAlias = normalizeSearchText(alias);
|
|
761
|
+
const compactAlias = compactSearchText(alias);
|
|
762
|
+
return normalizedAlias === intent.normalizedQuery
|
|
763
|
+
|| (intent.compactQuery && compactAlias === intent.compactQuery);
|
|
764
|
+
})) {
|
|
765
|
+
return 2;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
if (
|
|
769
|
+
displaySymbol.startsWith(intent.normalizedQuery)
|
|
770
|
+
|| (intent.compactQuery && compactDisplaySymbol.startsWith(intent.compactQuery))
|
|
771
|
+
) {
|
|
772
|
+
return 1;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
return 0;
|
|
776
|
+
}
|
|
777
|
+
|
|
731
778
|
function scoreAssetPreference(intent: SearchQueryIntent, instrumentClass?: TickerSearchInstrumentClass): number {
|
|
732
779
|
const itemClass = instrumentClass || "other";
|
|
733
780
|
if (!intent.assetPreference) {
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = "0.7.
|
|
1
|
+
export const VERSION = "0.7.4";
|