@trdrs/quickcharts 2.1.0 → 2.3.0
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/CHANGELOG.md +28 -0
- package/README.md +110 -2
- package/dist/adapters/rest.d.ts +1 -1
- package/dist/{assets-DOuBBsOp.d.ts → assets-M5emOz5c.d.ts} +1 -1
- package/dist/chunk-6OLK55Q2.js +145 -0
- package/dist/chunk-6OLK55Q2.js.map +1 -0
- package/dist/chunk-GFQQSY47.js +41 -0
- package/dist/chunk-GFQQSY47.js.map +1 -0
- package/dist/chunk-QYGSDD2Q.js +1868 -0
- package/dist/chunk-QYGSDD2Q.js.map +1 -0
- package/dist/drawings.d.ts +3 -3
- package/dist/feature-manifest.json +1 -1
- package/dist/format.d.ts +37 -0
- package/dist/format.js +7 -0
- package/dist/format.js.map +1 -0
- package/dist/glyphs.d.ts +17 -0
- package/dist/glyphs.js +11 -0
- package/dist/glyphs.js.map +1 -0
- package/dist/index.d.ts +67 -358
- package/dist/index.js +447 -2044
- package/dist/index.js.map +1 -1
- package/dist/{resources-Le6flcEn.d.ts → resources-C0cYryO9.d.ts} +1 -1
- package/dist/symbology-D3SLclrU.d.ts +105 -0
- package/dist/symbols-BL1fD4CS.d.ts +222 -0
- package/dist/symbols.d.ts +2 -0
- package/dist/symbols.js +9 -0
- package/dist/symbols.js.map +1 -0
- package/package.json +13 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,33 @@
|
|
|
1
1
|
# @trdrs/quickcharts
|
|
2
2
|
|
|
3
|
+
## 2.3.0
|
|
4
|
+
|
|
5
|
+
The symbol naming rule for hosts that name markets in views of their own. No public name changes.
|
|
6
|
+
|
|
7
|
+
`@trdrs/quickcharts/symbols` carries the symbol naming rule on its own, free of any DOM name: `symbolNames`, with its `SymbolNames` type, and `bareTicker`, so a view of the host's own with no DOM names a market as the chart's pill, legend and search rows do.
|
|
8
|
+
|
|
9
|
+
## 2.2.0
|
|
10
|
+
|
|
11
|
+
The way back to the live edge and the plot a host control floats over, touch that follows the hand, and the formatter, the glyph lists and the icon artwork for hosts that draw their own. No public name changes.
|
|
12
|
+
|
|
13
|
+
The return to the live edge glides: `goLive`, its command and `Alt+L` ease the view home sideways over about half a second, keeping the zoom and the price scale as they are. From far back the view steps in to a width and a half first, and a touch, a drag, a wheel or any other navigation stops it where it stands. Under a reduced-motion preference it goes in one step.
|
|
14
|
+
|
|
15
|
+
A chart reads whether its view sits back from the live edge: `awayFromLiveEdge()` and the `liveEdge` event, reported once per change, with a glide under way and bar replay reading as returned. `plotArea()` and the `plotArea` event give the main pane's plot in the pixels of the host's element, so a control floating over the bars stays beside the price scale as it widens.
|
|
16
|
+
|
|
17
|
+
Two fingers zoom proportionally: the bars spread exactly as far as the fingers do, and the point under the fingers stays under them as they travel. A pan one finger began before the second landed stays a pan, and a drawing that holds the pointer holds the pinch off.
|
|
18
|
+
|
|
19
|
+
`@trdrs/quickcharts/format` carries the price formatter and its types alone, with nothing that names a window, a document or a DOM type, so a native view or a server writes a price exactly as the chart's axis does.
|
|
20
|
+
|
|
21
|
+
A finger held on the plot scrubs the crosshair for as long as it stays down: the crosshair follows the finger, and lifting it takes the crosshair away, so the next one-finger drag pans the chart.
|
|
22
|
+
|
|
23
|
+
A flick of one finger coasts the chart calmly: the throw leaves the finger at no more than two pixels a millisecond and slows smoothly to rest within about a second, and a touch or any other navigation stops it where it stands.
|
|
24
|
+
|
|
25
|
+
`touch.freePan` makes the chart a canvas under the finger: a one-finger drag on the plot releases the main price scale's framing, as a drag on the price scale does, so the price follows the finger as freely as the time and nothing re-frames while the viewer pans, flicks or pinches, and a pinch zooms the time and leaves the price where it stands. A double-tap on the price scale, a new symbol or timeframe, and `goLive` frame the bars again. It is off unless the host turns it on.
|
|
26
|
+
|
|
27
|
+
`chartIconArtwork(id)` answers the chart's own drawings of an icon as standalone svg markup on `currentColor`, each on its own grid, so a host draws the same icons the chart does in a toolbar of its own or a native view. It answers the chart's artwork whatever `icons` draws instead; an illustration drawn in the theme's roles answers none.
|
|
28
|
+
|
|
29
|
+
`@trdrs/quickcharts/glyphs` carries the glyph picker's lists on their own, free of any DOM name: `EMOJI_CATEGORIES` and `ICON_CATEGORIES`, each category with the catalog key of its `heading`, and `isEmojiGlyph`, so a picker of the host's own offers the same glyphs and arms a glyph tool through `chart.drawings.arm` with `{ tool, props: { glyph } }`.
|
|
30
|
+
|
|
3
31
|
## 2.1.0
|
|
4
32
|
|
|
5
33
|
Long press for the crosshair, and emoji artwork that loads the first time the chart draws an emoji. No public name changes.
|
package/README.md
CHANGED
|
@@ -256,6 +256,18 @@ formatter.parse('110.5') // null, because that symbol does not write decimals
|
|
|
256
256
|
formatter.precision() // 0, because a fractional format writes no decimal digits
|
|
257
257
|
```
|
|
258
258
|
|
|
259
|
+
A surface with no DOM at all, a native view or a server, imports the same formatter from
|
|
260
|
+
`@trdrs/quickcharts/format`. The entry carries the formatter and its types and nothing that names a
|
|
261
|
+
window, a document or a DOM type, so it loads anywhere JavaScript runs and writes a price exactly as
|
|
262
|
+
the chart's axis does:
|
|
263
|
+
|
|
264
|
+
```ts
|
|
265
|
+
import { createPriceFormatter, type PriceFormat } from '@trdrs/quickcharts/format'
|
|
266
|
+
|
|
267
|
+
const bonds: PriceFormat = { pricescale: 128, minmov: 1, minmove2: 4, fractional: true }
|
|
268
|
+
createPriceFormatter(bonds).format(110.515625) // "110'16'2"
|
|
269
|
+
```
|
|
270
|
+
|
|
259
271
|
A ladder changes width by band, because the symbol declared those bands:
|
|
260
272
|
|
|
261
273
|
```ts
|
|
@@ -302,6 +314,18 @@ symbolNames({ symbol: 'NASDAQ:AAPL', name: 'Apple Inc', exchange: 'NASDAQ', type
|
|
|
302
314
|
symbolNames('HYPERLIQUID:ETH').mark // 'ETH', before the symbol resolves
|
|
303
315
|
```
|
|
304
316
|
|
|
317
|
+
`@trdrs/quickcharts/symbols` carries the same rule on its own, `symbolNames` and `bareTicker`, with
|
|
318
|
+
nothing that names a window, a document or a DOM type, so a view of your own with no DOM, a native
|
|
319
|
+
search list among them, names a market exactly as the chart does. `bareTicker` is the ticker inside
|
|
320
|
+
a feed symbol, its venue prefix shed:
|
|
321
|
+
|
|
322
|
+
```ts
|
|
323
|
+
import { bareTicker, symbolNames } from '@trdrs/quickcharts/symbols'
|
|
324
|
+
|
|
325
|
+
symbolNames({ symbol: 'HYPERLIQUID:BTC', name: 'Bitcoin perpetual', exchange: 'Hyperliquid', type: 'crypto', currencyCode: 'USDC' }).title // 'BTC / USDC'
|
|
326
|
+
bareTicker('CME:ES1!') // 'ES1!'
|
|
327
|
+
```
|
|
328
|
+
|
|
305
329
|
Symbology is display truth: how the chart writes a market's prices. Execution facts, such as lot
|
|
306
330
|
size and pip value, stay with your trading integration, and its execution grid can differ from the
|
|
307
331
|
chart's display grid.
|
|
@@ -920,6 +944,57 @@ function drive(widget: ChartWidget): void {
|
|
|
920
944
|
}
|
|
921
945
|
```
|
|
922
946
|
|
|
947
|
+
### The live edge and the plot
|
|
948
|
+
|
|
949
|
+
`goLive()` glides the view back to the live edge on an easing curve, moving it sideways only: the
|
|
950
|
+
zoom and the price scale stay as they are. From far back it steps in to a width and a half first, so
|
|
951
|
+
the return reads the same from any distance, and a touch, a drag, a wheel or any other navigation
|
|
952
|
+
stops it where it stands. Under a reduced-motion preference it goes in one step. `Alt+L` and the
|
|
953
|
+
`chart.view.goLive` command run the same glide.
|
|
954
|
+
|
|
955
|
+
`awayFromLiveEdge()` says whether the view sits back from the live edge, and the `liveEdge` event
|
|
956
|
+
reports each change, so a control offering the way back shows exactly while there is somewhere to go
|
|
957
|
+
back from. A glide under way reads as returned from its first frame, and bar replay, which has its
|
|
958
|
+
own way back, reads as returned throughout. `plotArea()` and the `plotArea` event give the main
|
|
959
|
+
pane's plot in the pixels of the element you handed the widget, as the four distances an absolutely
|
|
960
|
+
positioned element takes, so a control floating over the bars stays beside the price scale as it
|
|
961
|
+
widens:
|
|
962
|
+
|
|
963
|
+
```ts
|
|
964
|
+
import { type ChartHandle } from '@trdrs/quickcharts'
|
|
965
|
+
|
|
966
|
+
function backToLive(chart: ChartHandle, button: HTMLButtonElement): () => void {
|
|
967
|
+
const place = (): void => {
|
|
968
|
+
const plot = chart.plotArea()
|
|
969
|
+
if (!plot) return
|
|
970
|
+
button.style.right = `${plot.right + 8}px`
|
|
971
|
+
button.style.bottom = `${plot.bottom + 8}px`
|
|
972
|
+
}
|
|
973
|
+
button.onclick = () => chart.goLive()
|
|
974
|
+
button.hidden = !chart.awayFromLiveEdge()
|
|
975
|
+
place()
|
|
976
|
+
const offEdge = chart.on('liveEdge', (away) => {
|
|
977
|
+
button.hidden = !away
|
|
978
|
+
})
|
|
979
|
+
const offPlot = chart.on('plotArea', place)
|
|
980
|
+
return () => {
|
|
981
|
+
offEdge()
|
|
982
|
+
offPlot()
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
```
|
|
986
|
+
|
|
987
|
+
On a touch screen two fingers zoom proportionally: the bars spread exactly as far as the fingers
|
|
988
|
+
do, and the point under the fingers stays under them as they travel. A finger held on the plot
|
|
989
|
+
scrubs the crosshair while it stays down, and a flick coasts the chart calmly to rest.
|
|
990
|
+
|
|
991
|
+
With `touch: { freePan: true }` the chart is a canvas under the finger: a one-finger drag on the plot
|
|
992
|
+
releases the main price scale's framing, as a drag on the price scale does, so the price follows the
|
|
993
|
+
finger as freely as the time and nothing re-frames while the viewer pans, flicks or pinches. A pinch
|
|
994
|
+
zooms the time and leaves the price where it stands. A double-tap on the price scale, a new symbol or
|
|
995
|
+
timeframe, and `goLive()` frame the bars again; `goLive()` frames them from the tap, and the price
|
|
996
|
+
follows the window home as the time glides. It is off unless you turn it on.
|
|
997
|
+
|
|
923
998
|
### Chart styles
|
|
924
999
|
|
|
925
1000
|
`candles`, `hollow`, `bars`, `line`, `area`, `baseline`, `stepline`, listed as `CHART_STYLES`. A
|
|
@@ -1215,8 +1290,8 @@ function watch(widget: ChartWidget): () => void {
|
|
|
1215
1290
|
|
|
1216
1291
|
Widget events: `ready`, `activeChart`, `theme`, `locale`, `saveNeeded`, `saveConflict`,
|
|
1217
1292
|
`fullscreen`, `dispose`. Chart events: `symbol`, `timeframe`, `style`, `visibleRange`,
|
|
1218
|
-
`logicalRange`, `dataLoaded`, `feedStatus`, `scaleMode`, `timezone`, `indicator`,
|
|
1219
|
-
`replay`, `compare`, `history`.
|
|
1293
|
+
`logicalRange`, `liveEdge`, `plotArea`, `dataLoaded`, `feedStatus`, `scaleMode`, `timezone`, `indicator`,
|
|
1294
|
+
`drawing`, `replay`, `compare`, `history`.
|
|
1220
1295
|
|
|
1221
1296
|
### Commands
|
|
1222
1297
|
|
|
@@ -1736,6 +1811,21 @@ chart asks each factory again for every glyph it drew, a `toolbarButton` glyph i
|
|
|
1736
1811
|
the new drawing in place of the old one. A factory that refuses the new direction leaves the
|
|
1737
1812
|
previous drawing standing and records the failure.
|
|
1738
1813
|
|
|
1814
|
+
`chartIconArtwork(id)` answers the chart's own drawings of an icon as markup, for a host that draws
|
|
1815
|
+
the chart's icons outside it: a toolbar of its own, or a native view. Each drawing carries its grid,
|
|
1816
|
+
its body with every ink on `currentColor`, and the whole drawing as one standalone `<svg>`. An icon
|
|
1817
|
+
the chart draws at more than one size answers each, on its own grid, so you pick the one that suits
|
|
1818
|
+
your box; the drawing toolbar's are on a 28 grid. It answers the chart's own artwork whatever `icons`
|
|
1819
|
+
draws instead. An illustration drawn in the theme's roles answers none.
|
|
1820
|
+
|
|
1821
|
+
```ts
|
|
1822
|
+
import { chartIconArtwork } from '@trdrs/quickcharts'
|
|
1823
|
+
|
|
1824
|
+
const [trendLine] = chartIconArtwork('tool.trend_line')
|
|
1825
|
+
const toolButton = document.createElement('button')
|
|
1826
|
+
toolButton.innerHTML = trendLine!.svg // the chart's own trend line, in the button's ink
|
|
1827
|
+
```
|
|
1828
|
+
|
|
1739
1829
|
The chart mirrors the glyphs in `MIRRORED_ICONS` for a right-to-left language whatever artwork they
|
|
1740
1830
|
wear, so draw their left-to-right form. The line-end icons are drawn for a line's left end, and the
|
|
1741
1831
|
right end's picker wears the same drawing mirrored.
|
|
@@ -2573,6 +2663,24 @@ const saved = drawings?.export() // the persistence wire format (SerializedDrawi
|
|
|
2573
2663
|
note(`${selected?.type ?? 'nothing'} selected, ${saved?.length ?? 0} drawings`)
|
|
2574
2664
|
```
|
|
2575
2665
|
|
|
2666
|
+
`@trdrs/quickcharts/glyphs` carries the glyph picker's lists on their own, with nothing that names a
|
|
2667
|
+
window, a document or a DOM type, so a picker of your own, a native view among them, offers the same
|
|
2668
|
+
glyphs the chart's picker does: `EMOJI_CATEGORIES` and `ICON_CATEGORIES`, each category with its
|
|
2669
|
+
`id`, the catalog key of its `heading` and its `glyphs`, and `isEmojiGlyph`. The command arms a glyph
|
|
2670
|
+
tool with the glyph its next placement draws:
|
|
2671
|
+
|
|
2672
|
+
```ts
|
|
2673
|
+
import { createChartI18n, type ChartWidget } from '@trdrs/quickcharts'
|
|
2674
|
+
import { EMOJI_CATEGORIES } from '@trdrs/quickcharts/glyphs'
|
|
2675
|
+
|
|
2676
|
+
const { t } = createChartI18n()
|
|
2677
|
+
for (const category of EMOJI_CATEGORIES) note(`${t(category.heading)}: ${category.glyphs.length} emoji`)
|
|
2678
|
+
|
|
2679
|
+
function armEmoji(widget: ChartWidget, glyph: string): void {
|
|
2680
|
+
widget.commands.execute('chart.drawings.arm', { tool: 'emoji', props: { glyph } })
|
|
2681
|
+
}
|
|
2682
|
+
```
|
|
2683
|
+
|
|
2576
2684
|
### The drawing toolbar
|
|
2577
2685
|
|
|
2578
2686
|
The drawing toolbar runs down the chart's leading edge. Its buttons are the cursor with its three
|
package/dist/adapters/rest.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Time, IChartApi, ISeriesApi, SeriesType } from 'lightweight-charts';
|
|
2
|
-
import {
|
|
2
|
+
import { R as ResourceStore, T as TemplateMeta, a as TemplateBody, b as ResourceRef, W as WriteOutcome } from './resources-C0cYryO9.js';
|
|
3
3
|
|
|
4
4
|
/** One timeframe bucket's visibility rule: enabled + the inclusive value range it covers. */
|
|
5
5
|
interface VisibilityRange {
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// src/symbology.ts
|
|
2
|
+
function parseTickBands(variableTickSize) {
|
|
3
|
+
const parts = variableTickSize.trim().split(/\s+/).filter(Boolean);
|
|
4
|
+
if (parts.length === 0 || parts.length % 2 === 0) return [];
|
|
5
|
+
const bands = [];
|
|
6
|
+
for (let i = 0; i < parts.length; i += 2) {
|
|
7
|
+
const size = Number(parts[i]);
|
|
8
|
+
if (!Number.isFinite(size) || size <= 0) return [];
|
|
9
|
+
const raw = parts[i + 1];
|
|
10
|
+
if (raw === void 0) {
|
|
11
|
+
bands.push({ size, below: Number.POSITIVE_INFINITY });
|
|
12
|
+
break;
|
|
13
|
+
}
|
|
14
|
+
const below = Number(raw);
|
|
15
|
+
if (!Number.isFinite(below)) return [];
|
|
16
|
+
if (bands.length > 0 && below <= bands[bands.length - 1].below) return [];
|
|
17
|
+
bands.push({ size, below });
|
|
18
|
+
}
|
|
19
|
+
return bands;
|
|
20
|
+
}
|
|
21
|
+
function tickBandFor(bands, price) {
|
|
22
|
+
const at = Math.abs(price);
|
|
23
|
+
for (const band of bands) if (at < band.below) return band;
|
|
24
|
+
return bands.length > 0 ? bands[bands.length - 1] : null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// src/priceFormatter.ts
|
|
28
|
+
var FRACTION_SIGN = "'";
|
|
29
|
+
function decimalsOfPriceScale(pricescale) {
|
|
30
|
+
if (!Number.isFinite(pricescale) || pricescale <= 1) return 0;
|
|
31
|
+
return Math.min(16, Math.ceil(Math.log10(pricescale) - 1e-9));
|
|
32
|
+
}
|
|
33
|
+
function localeDecimalSign(locale) {
|
|
34
|
+
try {
|
|
35
|
+
const part = new Intl.NumberFormat(locale).formatToParts(1.1).find((p) => p.type === "decimal");
|
|
36
|
+
return part?.value ?? ".";
|
|
37
|
+
} catch {
|
|
38
|
+
return ".";
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function groupDigits(digits, groupSign) {
|
|
42
|
+
if (!groupSign) return digits;
|
|
43
|
+
let out = "";
|
|
44
|
+
for (let i = 0; i < digits.length; i++) {
|
|
45
|
+
if (i > 0 && (digits.length - i) % 3 === 0) out += groupSign;
|
|
46
|
+
out += digits[i];
|
|
47
|
+
}
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
function fixedText(value, decimals) {
|
|
51
|
+
return Math.abs(value).toFixed(Math.min(20, decimals));
|
|
52
|
+
}
|
|
53
|
+
function writeDecimal(price, decimals, punctuation) {
|
|
54
|
+
const fixed = fixedText(price, decimals);
|
|
55
|
+
const dot = fixed.indexOf(".");
|
|
56
|
+
const whole = dot < 0 ? fixed : fixed.slice(0, dot);
|
|
57
|
+
const frac = dot < 0 ? "" : fixed.slice(dot + 1);
|
|
58
|
+
const sign = price < 0 && Number(fixed) !== 0 ? "-" : "";
|
|
59
|
+
const body = groupDigits(whole, punctuation.groupSign);
|
|
60
|
+
return frac ? `${sign}${body}${punctuation.decimalSign}${frac}` : `${sign}${body}`;
|
|
61
|
+
}
|
|
62
|
+
function writeFractional(price, format, punctuation) {
|
|
63
|
+
const scale = format.pricescale;
|
|
64
|
+
const second = format.minmove2 && format.minmove2 > 1 ? format.minmove2 : 1;
|
|
65
|
+
const at = Math.abs(price);
|
|
66
|
+
const total = Math.round(at * scale);
|
|
67
|
+
const whole = Math.floor(total / scale);
|
|
68
|
+
const units = total - whole * scale;
|
|
69
|
+
const outer = Math.floor(units / second);
|
|
70
|
+
const inner = units - outer * second;
|
|
71
|
+
const outerWidth = String(Math.ceil(scale / second) - 1).length;
|
|
72
|
+
const sign = price < 0 && total !== 0 ? "-" : "";
|
|
73
|
+
const body = groupDigits(String(whole), punctuation.groupSign);
|
|
74
|
+
const outerText = String(outer).padStart(outerWidth, "0");
|
|
75
|
+
return second > 1 ? `${sign}${body}${FRACTION_SIGN}${outerText}${FRACTION_SIGN}${inner}` : `${sign}${body}${FRACTION_SIGN}${outerText}`;
|
|
76
|
+
}
|
|
77
|
+
function parseFractional(text, format) {
|
|
78
|
+
const scale = format.pricescale;
|
|
79
|
+
const second = format.minmove2 && format.minmove2 > 1 ? format.minmove2 : 1;
|
|
80
|
+
const parts = text.split(FRACTION_SIGN);
|
|
81
|
+
if (parts.length < 2 || parts.length > 3) return null;
|
|
82
|
+
const negative = parts[0].trim().startsWith("-");
|
|
83
|
+
const whole = Number(parts[0].replace(/[^0-9]/g, ""));
|
|
84
|
+
const outer = Number(parts[1].trim());
|
|
85
|
+
const inner = parts.length === 3 ? Number(parts[2].trim()) : 0;
|
|
86
|
+
if (!Number.isInteger(whole) || !Number.isInteger(outer) || !Number.isInteger(inner)) return null;
|
|
87
|
+
if (outer < 0 || inner < 0 || inner >= second) return null;
|
|
88
|
+
if (outer * second + inner >= scale) return null;
|
|
89
|
+
const value = whole + (outer * second + inner) / scale;
|
|
90
|
+
return negative ? -value : value;
|
|
91
|
+
}
|
|
92
|
+
function createPriceFormatter(format, options) {
|
|
93
|
+
const explicit = options?.numericPunctuation;
|
|
94
|
+
const punctuation = {
|
|
95
|
+
decimalSign: explicit?.decimalSign ?? (options?.locale ? localeDecimalSign(options.locale) : "."),
|
|
96
|
+
groupSign: explicit?.groupSign ?? ""
|
|
97
|
+
};
|
|
98
|
+
const fractional = format.fractional === true;
|
|
99
|
+
const bands = format.variableTickSize ? parseTickBands(format.variableTickSize) : [];
|
|
100
|
+
const bandDecimals = (band) => {
|
|
101
|
+
const text = decimalText(band.size);
|
|
102
|
+
const dot = text.indexOf(".");
|
|
103
|
+
return dot < 0 ? 0 : text.length - dot - 1;
|
|
104
|
+
};
|
|
105
|
+
const fixedDecimals = decimalsOfPriceScale(format.pricescale);
|
|
106
|
+
const widest = bands.length > 0 ? Math.max(...bands.map(bandDecimals)) : fixedDecimals;
|
|
107
|
+
const decimalsFor = (price) => {
|
|
108
|
+
if (bands.length === 0) return fixedDecimals;
|
|
109
|
+
const band = tickBandFor(bands, price);
|
|
110
|
+
return band ? bandDecimals(band) : fixedDecimals;
|
|
111
|
+
};
|
|
112
|
+
return {
|
|
113
|
+
format(price) {
|
|
114
|
+
if (!Number.isFinite(price)) return "";
|
|
115
|
+
return fractional ? writeFractional(price, format, punctuation) : writeDecimal(price, decimalsFor(price), punctuation);
|
|
116
|
+
},
|
|
117
|
+
parse(text) {
|
|
118
|
+
const trimmed = text.trim();
|
|
119
|
+
if (trimmed === "") return null;
|
|
120
|
+
if (fractional) return parseFractional(trimmed, format);
|
|
121
|
+
let plain = trimmed;
|
|
122
|
+
if (punctuation.groupSign) plain = plain.split(punctuation.groupSign).join("");
|
|
123
|
+
if (punctuation.decimalSign !== ".") plain = plain.split(punctuation.decimalSign).join(".");
|
|
124
|
+
if (!/^[+-]?(\d+(\.\d*)?|\.\d+)$/.test(plain)) return null;
|
|
125
|
+
const value = Number(plain);
|
|
126
|
+
return Number.isFinite(value) ? value : null;
|
|
127
|
+
},
|
|
128
|
+
precision: () => fractional ? 0 : widest
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
function decimalText(value) {
|
|
132
|
+
const text = String(value);
|
|
133
|
+
const exp = /^(-?)(\d)(?:\.(\d+))?e-(\d+)$/.exec(text);
|
|
134
|
+
if (!exp) return text;
|
|
135
|
+
const [, sign, lead, rest = "", power] = exp;
|
|
136
|
+
const zeros = Number(power) - 1;
|
|
137
|
+
return `${sign}0.${"0".repeat(zeros)}${lead}${rest}`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export {
|
|
141
|
+
parseTickBands,
|
|
142
|
+
tickBandFor,
|
|
143
|
+
createPriceFormatter
|
|
144
|
+
};
|
|
145
|
+
//# sourceMappingURL=chunk-6OLK55Q2.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/symbology.ts","../src/priceFormatter.ts"],"sourcesContent":["// Quick Charts SYMBOLOGY — the display facts `ChartDatafeed.resolve` serves for one symbol, and the\n// only place a chart price display gets its precision from. The datafeed says how a market's prices\n// are WRITTEN on the chart; a host that trades keeps its own execution facts, and the display and\n// execution grids may differ.\n//\n// What lives here: identity, venue and type, supported resolutions, exchange timezone and session,\n// data status, currency and unit, volume precision, and the price-format facts. Every one is a\n// display fact. A host keeps its execution facts and its quote values (last, change, volume) on its\n// own side of the chart.\n//\n// This module is self-contained by the same rule as `datafeed.ts` — the contract must never drag a\n// backend SDK into the chart's dependency surface. It imports nothing.\n\n/** How one symbol's prices are WRITTEN, in five facts. Together they express\n * every supported form: decimal, pip, fractional, fraction of a fraction, and variable tick.\n *\n * - `pricescale` — price units per whole unit. 100 writes cents, 100000 writes FX pipettes,\n * 100000000 writes satoshis, 32 writes thirty-seconds. It alone sets the decimal column width.\n * - `minmov` — the smallest move, in those units. It declares the grid a price moves on; it never\n * widens or narrows the decimal column (ES at `{100, 25}` writes 4500.25, the same two decimals\n * as `{100, 1}`).\n * - `minmove2` — the further division of one `minmov` step for a fraction-of-a-fraction market:\n * 4 means quarters of a thirty-second.\n * - `fractional` — write the sub-unit part as a counted fraction (110'16) instead of decimals.\n * - `variableTickSize` — an ordered ladder of tick sizes by price band, written as a\n * space-separated string: alternating tick and upper bound, ending with the tick that applies\n * above the last bound. \"0.01 10 0.02 100 0.05\" means 0.01 below 10, 0.02 below 100, 0.05 at or\n * above 100. The band a price falls in is a DECLARED fact of the symbol, not a guess from its\n * magnitude. */\nexport interface PriceFormat {\n pricescale: number\n minmov: number\n minmove2?: number\n fractional?: boolean\n variableTickSize?: string\n}\n\n/** How live the served data is, in three states. `delayed_streaming` is a real\n * stream behind a delay, not a degraded `streaming`. */\nexport type DataStatus = 'streaming' | 'endofday' | 'delayed_streaming'\n\n/** The subsession ids a symbol may name. `regular` is the weekly `session`; `premarket` and\n * `postmarket` are the extended spans before and after it; `extended` is the whole span from\n * pre-market open through post-market close. */\nexport type SubsessionId = 'regular' | 'extended' | 'premarket' | 'postmarket'\n\n/** One named session of a market with extended hours. `session`\n * uses the grammar of `SymbolInfo.session`; `sessionCorrections` uses the grammar of\n * `SymbolInfo.corrections` and lists only the days that shorten THIS subsession. `description`\n * is a label a feed may state; the chart labels a subsession by its `id` from its own catalog. */\nexport interface Subsession {\n id: SubsessionId\n session: string\n description?: string\n sessionCorrections?: string\n}\n\n/** Resolved metadata for ONE symbol: everything the chart needs to title it, page it, session it,\n * and write its prices.\n *\n * `timezone`, `session`, `sessionHolidays`, `corrections` and `subsessions` are the\n * exchange-hours facts and travel with the symbol. The chart's session model (`sessionModel.ts`)\n * is built from them alone: market state, session shading, the regular-hours filter and the\n * status popup all read what the feed said, and a feed that states no extended hours gets none. */\nexport interface SymbolInfo {\n /** The symbol the datafeed answers to on every later call. */\n ticker: string\n /** The short display identity, e.g. 'ESZ2026'. */\n name: string\n /** The long human description, e.g. 'E-mini S&P 500 Dec 2026'. */\n description: string\n /** The venue a UI attributes the symbol to. */\n exchange: string\n /** The venue that actually lists it, when it differs from `exchange`. */\n listedExchange: string\n /** The venue's own type token, e.g. 'futures', 'stock', 'crypto', 'forex'. */\n type: string\n /** The resolutions this symbol serves, as chart timeframe tokens ('1m', '4h', '1d', '1mo'). An\n * EMPTY list declares no restriction: the feed serves any token the grammar admits (a futures\n * or spread feed says exactly this), and the chart never reads it as \"serves nothing\". A finite\n * list is a promise the chart may enforce. */\n supportedResolutions: readonly string[]\n /** The exchange's IANA zone, e.g. 'America/New_York'. */\n timezone: string\n /** The session string, e.g. '1700-1600' or '0930-1600'. */\n session: string\n /** The session-holidays string: comma-separated 'YYYYMMDD' full closures. */\n sessionHolidays?: string\n /** The corrections string: `;`-separated `<session>:<dates>` entries, each a session in\n * the grammar of `session` that holds on the `,`-separated 'YYYYMMDD' trading days named. A\n * correction outranks a holiday on the same date. */\n corrections?: string\n /** The symbol's named sessions, for a market whose exchange keeps extended hours. Absent for a\n * market with one continuous session. */\n subsessions?: readonly Subsession[]\n dataStatus: DataStatus\n /** ISO 4217 for a money-quoted symbol; absent when the quote unit is not a currency. */\n currencyCode?: string\n /** The unit a non-currency symbol is quoted in, e.g. an energy or weight unit id. */\n unitId?: string\n /** Decimal places for a volume value. 0 for a whole-contract market. */\n volumePrecision: number\n format: PriceFormat\n}\n\n\n/** One band of a `variableTickSize` ladder: `size` is the tick that applies to prices strictly below\n * `below`, and the last band's `below` is `Infinity`. */\nexport interface TickBand {\n /** The tick size in this band. */\n size: number\n /** The exclusive upper bound of the band. */\n below: number\n}\n\n/** Parse the space-separated variable-tick ladder into ordered bands. Returns an\n * empty array for a string the grammar cannot read, which is how a caller tells \"no ladder\" from\n * \"a ladder I must honour\" without guessing at a half-parsed one. */\nexport function parseTickBands(variableTickSize: string): TickBand[] {\n const parts = variableTickSize.trim().split(/\\s+/).filter(Boolean)\n if (parts.length === 0 || parts.length % 2 === 0) return []\n const bands: TickBand[] = []\n for (let i = 0; i < parts.length; i += 2) {\n const size = Number(parts[i])\n if (!Number.isFinite(size) || size <= 0) return []\n const raw = parts[i + 1]\n if (raw === undefined) {\n bands.push({ size, below: Number.POSITIVE_INFINITY })\n break\n }\n const below = Number(raw)\n if (!Number.isFinite(below)) return []\n if (bands.length > 0 && below <= bands[bands.length - 1]!.below) return []\n bands.push({ size, below })\n }\n return bands\n}\n\n/** The band a price falls in, by the ladder's declared bounds (magnitude of the SYMBOL's own\n * declared ladder, never a generic magnitude rule). Negative prices read on their absolute value,\n * so a spread writes both sides in one column. */\nexport function tickBandFor(bands: readonly TickBand[], price: number): TickBand | null {\n const at = Math.abs(price)\n for (const band of bands) if (at < band.below) return band\n return bands.length > 0 ? bands[bands.length - 1]! : null\n}\n","// The one Quick Charts PRICE FORMATTER. Every price a chart writes comes through here: the price\n// scale, the OHLC legend, the crosshair, drawing labels, marks, indicator price plots and exported\n// image text, plus a host's own surfaces through the public factory. One resolved formatter per\n// symbol means a layout cannot draw one market's prices at another's precision.\n//\n// The rule that makes it trustworthy: PRECISION COMES FROM THE SYMBOL'S DECLARED FACTS, NEVER FROM\n// THE PRICE. There is no \"small numbers get more decimals\" branch anywhere below. A variable-tick\n// symbol does change width by price band, but the bands are a fact the datafeed declared, not a\n// guess the formatter made.\n//\n// Deterministic in the strict sense: the same `PriceFormat` and options always produce the same\n// string for the same number, on every machine and in every locale, because the only locale input\n// is the punctuation the caller passes (or asks to be derived once, at construction).\nimport { parseTickBands, tickBandFor, type PriceFormat, type TickBand } from './symbology'\n\n/** How a formatted price is punctuated. A price localizes the decimal sign only, so\n * `groupSign` defaults to '' (no thousands separator) and a host that wants grouping asks for it\n * explicitly. */\nexport interface NumericPunctuation {\n /** What separates the whole part from the decimals. Default '.', or the locale's sign when a\n * `locale` is given. */\n decimalSign?: string\n /** What separates thousands in the whole part. Default '': a price is written ungrouped, because\n * a grouped price is harder to compare against the axis beside it. */\n groupSign?: string\n}\n\nexport interface PriceFormatterOptions {\n /** A BCP 47 tag used ONCE, at construction, to derive the decimal sign. Anything explicit in\n * `numericPunctuation` wins over it. */\n locale?: string\n numericPunctuation?: NumericPunctuation\n}\n\n/** A resolved formatter for one symbol. `format` and `parse` are exact inverses over every price\n * that lands on the symbol's own grid. */\nexport interface PriceFormatter {\n /** The price as the symbol writes it. */\n format(price: number): string\n /** The number a viewer typed, or null when the text is not a price in this symbol's format. */\n parse(text: string): number | null\n /** How many digits `format` writes after the decimal sign. 0 for a fractional format, which\n * writes none; the widest band's width for a variable-tick format, so a column sized from it\n * never has to reflow. */\n precision(): number\n}\n\n/** The apostrophe written between a whole price and its counted fraction, and again\n * between that fraction and its sub-fraction: 110'16 and 110'16'2. */\nconst FRACTION_SIGN = \"'\"\n\n/** Decimal digits implied by a price scale: the digits it takes to write `pricescale - 1`. A power\n * of ten resolves exactly (100 → 2, 100000000 → 8); 32 resolves to 2, the width of '31'. */\nfunction decimalsOfPriceScale(pricescale: number): number {\n if (!Number.isFinite(pricescale) || pricescale <= 1) return 0\n return Math.min(16, Math.ceil(Math.log10(pricescale) - 1e-9))\n}\n\n/** The decimal sign a locale writes, resolved once. Falls back to '.' where `Intl` cannot answer. */\nfunction localeDecimalSign(locale: string): string {\n try {\n const part = new Intl.NumberFormat(locale).formatToParts(1.1).find((p) => p.type === 'decimal')\n return part?.value ?? '.'\n } catch {\n return '.'\n }\n}\n\n/** A non-negative integer written with `groupSign` every three digits from the right. */\nfunction groupDigits(digits: string, groupSign: string): string {\n if (!groupSign) return digits\n let out = ''\n for (let i = 0; i < digits.length; i++) {\n if (i > 0 && (digits.length - i) % 3 === 0) out += groupSign\n out += digits[i]\n }\n return out\n}\n\n/** The magnitude of `value` written at exactly `decimals` places. Rounding is the double's own: a\n * price arrives as a binary float, and `toFixed` rounds the value that is actually stored, so the\n * same input yields the same digits on every engine. The sign is applied by the caller, which is\n * what keeps a price that rounds to zero from writing '-0.00'. */\nfunction fixedText(value: number, decimals: number): string {\n return Math.abs(value).toFixed(Math.min(20, decimals))\n}\n\ninterface Punctuation {\n decimalSign: string\n groupSign: string\n}\n\nfunction writeDecimal(price: number, decimals: number, punctuation: Punctuation): string {\n const fixed = fixedText(price, decimals)\n const dot = fixed.indexOf('.')\n const whole = dot < 0 ? fixed : fixed.slice(0, dot)\n const frac = dot < 0 ? '' : fixed.slice(dot + 1)\n const sign = price < 0 && Number(fixed) !== 0 ? '-' : ''\n const body = groupDigits(whole, punctuation.groupSign)\n return frac ? `${sign}${body}${punctuation.decimalSign}${frac}` : `${sign}${body}`\n}\n\n/** The fractional form. `units` is the count of `1/pricescale` steps in the sub-unit part, split\n * into whole `minmove2`-sized groups (the thirty-seconds) and a remainder (the quarters of one\n * thirty-second). With no `minmove2` the sub-unit part is the count itself: 110'16. */\nfunction writeFractional(price: number, format: PriceFormat, punctuation: Punctuation): string {\n const scale = format.pricescale\n const second = format.minmove2 && format.minmove2 > 1 ? format.minmove2 : 1\n const at = Math.abs(price)\n const total = Math.round(at * scale)\n const whole = Math.floor(total / scale)\n const units = total - whole * scale\n const outer = Math.floor(units / second)\n const inner = units - outer * second\n const outerWidth = String(Math.ceil(scale / second) - 1).length\n const sign = price < 0 && total !== 0 ? '-' : ''\n const body = groupDigits(String(whole), punctuation.groupSign)\n const outerText = String(outer).padStart(outerWidth, '0')\n return second > 1\n ? `${sign}${body}${FRACTION_SIGN}${outerText}${FRACTION_SIGN}${inner}`\n : `${sign}${body}${FRACTION_SIGN}${outerText}`\n}\n\nfunction parseFractional(text: string, format: PriceFormat): number | null {\n const scale = format.pricescale\n const second = format.minmove2 && format.minmove2 > 1 ? format.minmove2 : 1\n const parts = text.split(FRACTION_SIGN)\n if (parts.length < 2 || parts.length > 3) return null\n const negative = parts[0]!.trim().startsWith('-')\n const whole = Number(parts[0]!.replace(/[^0-9]/g, ''))\n const outer = Number(parts[1]!.trim())\n const inner = parts.length === 3 ? Number(parts[2]!.trim()) : 0\n if (!Number.isInteger(whole) || !Number.isInteger(outer) || !Number.isInteger(inner)) return null\n if (outer < 0 || inner < 0 || inner >= second) return null\n if (outer * second + inner >= scale) return null\n const value = whole + (outer * second + inner) / scale\n return negative ? -value : value\n}\n\n/** Build the formatter for one symbol's price format. The result is stateless and reusable; a host\n * resolves a symbol once and keeps its formatter for as long as the symbol is on screen. */\nexport function createPriceFormatter(format: PriceFormat, options?: PriceFormatterOptions): PriceFormatter {\n const explicit = options?.numericPunctuation\n const punctuation: Punctuation = {\n decimalSign: explicit?.decimalSign ?? (options?.locale ? localeDecimalSign(options.locale) : '.'),\n groupSign: explicit?.groupSign ?? '',\n }\n const fractional = format.fractional === true\n const bands: readonly TickBand[] = format.variableTickSize ? parseTickBands(format.variableTickSize) : []\n const bandDecimals = (band: TickBand): number => {\n const text = decimalText(band.size)\n const dot = text.indexOf('.')\n return dot < 0 ? 0 : text.length - dot - 1\n }\n const fixedDecimals = decimalsOfPriceScale(format.pricescale)\n const widest = bands.length > 0 ? Math.max(...bands.map(bandDecimals)) : fixedDecimals\n const decimalsFor = (price: number): number => {\n if (bands.length === 0) return fixedDecimals\n const band = tickBandFor(bands, price)\n return band ? bandDecimals(band) : fixedDecimals\n }\n\n return {\n format(price) {\n if (!Number.isFinite(price)) return ''\n return fractional ? writeFractional(price, format, punctuation) : writeDecimal(price, decimalsFor(price), punctuation)\n },\n parse(text) {\n const trimmed = text.trim()\n if (trimmed === '') return null\n if (fractional) return parseFractional(trimmed, format)\n let plain = trimmed\n if (punctuation.groupSign) plain = plain.split(punctuation.groupSign).join('')\n if (punctuation.decimalSign !== '.') plain = plain.split(punctuation.decimalSign).join('.')\n if (!/^[+-]?(\\d+(\\.\\d*)?|\\.\\d+)$/.test(plain)) return null\n const value = Number(plain)\n return Number.isFinite(value) ? value : null\n },\n precision: () => (fractional ? 0 : widest),\n }\n}\n\n/** A number's shortest decimal text, with exponent notation expanded, so a tick of 1e-8 reads\n * '0.00000001' and its decimal width can be counted. */\nfunction decimalText(value: number): string {\n const text = String(value)\n const exp = /^(-?)(\\d)(?:\\.(\\d+))?e-(\\d+)$/.exec(text)\n if (!exp) return text\n const [, sign, lead, rest = '', power] = exp\n const zeros = Number(power) - 1\n return `${sign}0.${'0'.repeat(zeros)}${lead}${rest}`\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// src/symbolLabel.ts
|
|
2
|
+
function bareTicker(symbol) {
|
|
3
|
+
return /^[A-Za-z][A-Za-z0-9._-]*:([A-Za-z0-9][A-Za-z0-9._!]*)$/.exec(symbol)?.[1] ?? symbol;
|
|
4
|
+
}
|
|
5
|
+
var SLASH_PAIR = /^[A-Za-z][A-Za-z0-9.]*\/[A-Za-z][A-Za-z0-9.]*$/;
|
|
6
|
+
function symbolLabel(symbol) {
|
|
7
|
+
const ticker = bareTicker(symbol);
|
|
8
|
+
return SLASH_PAIR.test(ticker) ? ticker.replace("/", "") : ticker;
|
|
9
|
+
}
|
|
10
|
+
var PAIR_MARKETS = /* @__PURE__ */ new Set(["crypto", "forex"]);
|
|
11
|
+
function pairLabel(ticker, type, currencyCode) {
|
|
12
|
+
const quote = currencyCode?.trim();
|
|
13
|
+
if (!quote || !PAIR_MARKETS.has(type.toLowerCase())) return null;
|
|
14
|
+
const bare = bareTicker(ticker);
|
|
15
|
+
const base = bare.length > quote.length && bare.toUpperCase().endsWith(quote.toUpperCase()) ? bare.slice(0, -quote.length) : bare;
|
|
16
|
+
return base && base.toUpperCase() !== quote.toUpperCase() ? `${base} / ${quote}` : null;
|
|
17
|
+
}
|
|
18
|
+
function factsOf(symbol) {
|
|
19
|
+
if (typeof symbol === "string") return { ticker: symbol, type: "", shortName: "", longName: "", currencyCode: void 0 };
|
|
20
|
+
if ("symbol" in symbol) return { ticker: symbol.symbol, type: symbol.type, shortName: "", longName: symbol.name.trim(), currencyCode: symbol.currencyCode };
|
|
21
|
+
const name = symbol.name.trim();
|
|
22
|
+
const description = symbol.description.trim();
|
|
23
|
+
return { ticker: symbol.ticker, type: symbol.type, shortName: name !== description ? name : "", longName: description, currencyCode: symbol.currencyCode };
|
|
24
|
+
}
|
|
25
|
+
function symbolNames(symbol) {
|
|
26
|
+
const facts = factsOf(symbol);
|
|
27
|
+
if (facts.type.toLowerCase() === "spread") return { mark: facts.ticker, title: facts.ticker, description: facts.ticker };
|
|
28
|
+
const bare = bareTicker(facts.ticker);
|
|
29
|
+
const pair = pairLabel(facts.ticker, facts.type, facts.currencyCode) ?? (SLASH_PAIR.test(bare) ? bare.replace("/", " / ") : null);
|
|
30
|
+
return {
|
|
31
|
+
mark: pair ? pair.replace(" / ", "") : symbolLabel(facts.ticker),
|
|
32
|
+
title: pair ?? (facts.shortName || bare),
|
|
33
|
+
description: pair ?? (facts.longName || bare)
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export {
|
|
38
|
+
bareTicker,
|
|
39
|
+
symbolNames
|
|
40
|
+
};
|
|
41
|
+
//# sourceMappingURL=chunk-GFQQSY47.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/symbolLabel.ts"],"sourcesContent":["// How a symbol is WRITTEN on screen: one rule, three faces, kept in one file so they cannot drift.\n// The toolbar pill wears the compact mark, the legend wears the title, and a search row wears the\n// mark beside the description. Every face drops the venue prefix, which is feed identity rather\n// than anything a viewer reads, and every surface that names a market, in this package or over it,\n// reads the same faces from `symbolNames`.\nimport type { SymbolRow } from './datafeed'\nimport type { SymbolInfo } from './symbology'\n\n/** The ticker inside a feed symbol. Only a well-formed `VENUE:TICKER` sheds its prefix; a spread\n * expression, or anything else the pattern does not admit, is its own label and passes through. */\nexport function bareTicker(symbol: string): string {\n return /^[A-Za-z][A-Za-z0-9._-]*:([A-Za-z0-9][A-Za-z0-9._!]*)$/.exec(symbol)?.[1] ?? symbol\n}\n\n/** A ticker written as a slash pair (`BTC/USD`): catalog identity that already says what it is\n * priced in. A reciprocal expression (`1/ES`) starts with a digit and is not one. */\nconst SLASH_PAIR = /^[A-Za-z][A-Za-z0-9.]*\\/[A-Za-z][A-Za-z0-9.]*$/\n\n/** Compact presentation of one symbol. Feed identity and spread expressions stay untouched. */\nexport function symbolLabel(symbol: string): string {\n const ticker = bareTicker(symbol)\n return SLASH_PAIR.test(ticker) ? ticker.replace('/', '') : ticker\n}\n\n/** The markets whose symbols ARE a pair: one thing priced in another, where the ticker alone says\n * what is being bought but not what it is priced in. Those read as `BASE / QUOTE`.\n *\n * Every other market is deliberately absent. Nearly every symbol carries a `currencyCode`, so the\n * gate cannot be \"has a currency\": a share quoted in dollars is not a pair, and `AAPL / USD` would\n * claim one that does not exist. */\nconst PAIR_MARKETS = new Set(['crypto', 'forex'])\n\n/** `BASE / QUOTE` for a pair market, or null for anything that is not one.\n *\n * The quote comes from the feed's own currency; a ticker that already carries it as a suffix\n * (`BTCUSDT`, `EURUSD`) sheds it rather than repeating it, and the base is whatever the ticker\n * states, so an inverted quotation reads the way it is written. */\nexport function pairLabel(ticker: string, type: string, currencyCode: string | undefined): string | null {\n const quote = currencyCode?.trim()\n if (!quote || !PAIR_MARKETS.has(type.toLowerCase())) return null\n const bare = bareTicker(ticker)\n const base = bare.length > quote.length && bare.toUpperCase().endsWith(quote.toUpperCase()) ? bare.slice(0, -quote.length) : bare\n return base && base.toUpperCase() !== quote.toUpperCase() ? `${base} / ${quote}` : null\n}\n\n/** The three ways one market is written, from one rule. */\nexport interface SymbolNames {\n /** The compact mark a label wears at a glance: a pair closed up (`BTCUSDC`, `EURUSD`), otherwise\n * the bare ticker (`ES1!`, `AAPL`). A spread expression is its own mark, operators and all. */\n mark: string\n /** The title a reading wears: a pair spaced (`BTC / USDC`), otherwise the feed's short name where\n * it stated one apart from the description, otherwise the bare ticker. */\n title: string\n /** The description beside a mark: a pair spaced, otherwise the feed's long description, otherwise\n * the bare ticker, so a market never repeats its own mark for want of a name. */\n description: string\n}\n\n/** The facts the rule reads, in one shape whatever the surface holds. */\ninterface Facts {\n ticker: string\n type: string\n shortName: string\n longName: string\n currencyCode: string | undefined\n}\n\nfunction factsOf(symbol: SymbolInfo | SymbolRow | string): Facts {\n if (typeof symbol === 'string') return { ticker: symbol, type: '', shortName: '', longName: '', currencyCode: undefined }\n // A search row states one name, the long one; a resolved symbol states a short name beside its\n // description, and a short name that merely repeats the description has not been given.\n if ('symbol' in symbol) return { ticker: symbol.symbol, type: symbol.type, shortName: '', longName: symbol.name.trim(), currencyCode: symbol.currencyCode }\n const name = symbol.name.trim()\n const description = symbol.description.trim()\n return { ticker: symbol.ticker, type: symbol.type, shortName: name !== description ? name : '', longName: description, currencyCode: symbol.currencyCode }\n}\n\n/** How a market is named, from a resolved `SymbolInfo`, a search row, or the feed symbol alone\n * before either has landed.\n *\n * A pair market reads as `BTC / USDC` or `EUR / USD`, both sides in the codes the market itself\n * trades in, never a spelled-out currency name. The quote comes from `currencyCode`, and a ticker\n * written as a slash pair (`BTC/USD`) needs no currency to say so. Before a symbol resolves there\n * is no pair to write, so the bare ticker stands on its own rather than guessing at a quote. A\n * market that is not a pair keeps the feed's own words: the short name as its title, the long\n * description beside its mark. */\nexport function symbolNames(symbol: SymbolInfo | SymbolRow | string): SymbolNames {\n const facts = factsOf(symbol)\n if (facts.type.toLowerCase() === 'spread') return { mark: facts.ticker, title: facts.ticker, description: facts.ticker }\n const bare = bareTicker(facts.ticker)\n const pair = pairLabel(facts.ticker, facts.type, facts.currencyCode) ?? (SLASH_PAIR.test(bare) ? bare.replace('/', ' / ') : null)\n return {\n mark: pair ? pair.replace(' / ', '') : symbolLabel(facts.ticker),\n title: pair ?? (facts.shortName || bare),\n description: pair ?? (facts.longName || bare),\n }\n}\n"],"mappings":";AAUO,SAAS,WAAW,QAAwB;AACjD,SAAO,yDAAyD,KAAK,MAAM,IAAI,CAAC,KAAK;AACvF;AAIA,IAAM,aAAa;AAGZ,SAAS,YAAY,QAAwB;AAClD,QAAM,SAAS,WAAW,MAAM;AAChC,SAAO,WAAW,KAAK,MAAM,IAAI,OAAO,QAAQ,KAAK,EAAE,IAAI;AAC7D;AAQA,IAAM,eAAe,oBAAI,IAAI,CAAC,UAAU,OAAO,CAAC;AAOzC,SAAS,UAAU,QAAgB,MAAc,cAAiD;AACvG,QAAM,QAAQ,cAAc,KAAK;AACjC,MAAI,CAAC,SAAS,CAAC,aAAa,IAAI,KAAK,YAAY,CAAC,EAAG,QAAO;AAC5D,QAAM,OAAO,WAAW,MAAM;AAC9B,QAAM,OAAO,KAAK,SAAS,MAAM,UAAU,KAAK,YAAY,EAAE,SAAS,MAAM,YAAY,CAAC,IAAI,KAAK,MAAM,GAAG,CAAC,MAAM,MAAM,IAAI;AAC7H,SAAO,QAAQ,KAAK,YAAY,MAAM,MAAM,YAAY,IAAI,GAAG,IAAI,MAAM,KAAK,KAAK;AACrF;AAwBA,SAAS,QAAQ,QAAgD;AAC/D,MAAI,OAAO,WAAW,SAAU,QAAO,EAAE,QAAQ,QAAQ,MAAM,IAAI,WAAW,IAAI,UAAU,IAAI,cAAc,OAAU;AAGxH,MAAI,YAAY,OAAQ,QAAO,EAAE,QAAQ,OAAO,QAAQ,MAAM,OAAO,MAAM,WAAW,IAAI,UAAU,OAAO,KAAK,KAAK,GAAG,cAAc,OAAO,aAAa;AAC1J,QAAM,OAAO,OAAO,KAAK,KAAK;AAC9B,QAAM,cAAc,OAAO,YAAY,KAAK;AAC5C,SAAO,EAAE,QAAQ,OAAO,QAAQ,MAAM,OAAO,MAAM,WAAW,SAAS,cAAc,OAAO,IAAI,UAAU,aAAa,cAAc,OAAO,aAAa;AAC3J;AAWO,SAAS,YAAY,QAAsD;AAChF,QAAM,QAAQ,QAAQ,MAAM;AAC5B,MAAI,MAAM,KAAK,YAAY,MAAM,SAAU,QAAO,EAAE,MAAM,MAAM,QAAQ,OAAO,MAAM,QAAQ,aAAa,MAAM,OAAO;AACvH,QAAM,OAAO,WAAW,MAAM,MAAM;AACpC,QAAM,OAAO,UAAU,MAAM,QAAQ,MAAM,MAAM,MAAM,YAAY,MAAM,WAAW,KAAK,IAAI,IAAI,KAAK,QAAQ,KAAK,KAAK,IAAI;AAC5H,SAAO;AAAA,IACL,MAAM,OAAO,KAAK,QAAQ,OAAO,EAAE,IAAI,YAAY,MAAM,MAAM;AAAA,IAC/D,OAAO,SAAS,MAAM,aAAa;AAAA,IACnC,aAAa,SAAS,MAAM,YAAY;AAAA,EAC1C;AACF;","names":[]}
|