@luxalgo/vela 0.7.8 → 0.8.1
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/NOTICE +28 -28
- package/README.md +15 -7
- package/dist/{DataProvider-CGa4KHRa.d.cts → DataProvider-BrlxntPN.d.cts} +1 -1
- package/dist/{DataProvider-CmWUtDVL.d.ts → DataProvider-DwFIoV4r.d.ts} +1 -1
- package/dist/{chunk-N4ZZYLMP.js → chunk-ATGTCWHJ.js} +726 -273
- package/dist/{chunk-CAFCLMPF.js → chunk-BZQM2XO7.js} +1 -0
- package/dist/{chunk-M6GP7DAX.js → chunk-DBDXKZUF.js} +474 -47
- package/dist/{chunk-BFA32GOU.js → chunk-EZ5FWVLA.js} +5 -2
- package/dist/{chunk-PVGWNF7V.js → chunk-LMBYEGUR.js} +2 -2
- package/dist/{chunk-WTWGEB7K.js → chunk-NELQJCGK.js} +298 -5
- package/dist/{contributions-DF_Ea07m.d.cts → contributions-CtlptxR8.d.cts} +187 -5
- package/dist/{contributions-nKOSeLy8.d.ts → contributions-tjNExo1o.d.ts} +187 -5
- package/dist/index.cjs +1044 -272
- package/dist/index.d.cts +24 -8
- package/dist/index.d.ts +24 -8
- package/dist/index.js +28 -4
- package/dist/{options-D1AJKsn_.d.ts → options-BnEPUuVV.d.cts} +1 -1
- package/dist/{options-D1AJKsn_.d.cts → options-BnEPUuVV.d.ts} +1 -1
- package/dist/{plugin-6F04gjQe.d.cts → plugin-BWxJRKzS.d.cts} +10 -4
- package/dist/{plugin-BLsNipkc.d.ts → plugin-e2WUEtiI.d.ts} +10 -4
- package/dist/plugin.cjs +1 -0
- package/dist/plugin.d.cts +4 -4
- package/dist/plugin.d.ts +4 -4
- package/dist/plugin.js +2 -2
- package/dist/providers/binance.d.cts +2 -2
- package/dist/providers/binance.d.ts +2 -2
- package/dist/providers/coinbase.d.cts +2 -2
- package/dist/providers/coinbase.d.ts +2 -2
- package/dist/providers/hyperliquid.d.cts +2 -2
- package/dist/providers/hyperliquid.d.ts +2 -2
- package/dist/{statusline-model-DAFyqVmR.d.ts → statusline-model-DFY7tuHj.d.cts} +5 -3
- package/dist/{statusline-model-BoWJ3cWQ.d.cts → statusline-model-zKXtGIjj.d.ts} +5 -3
- package/dist/ui.cjs +301 -3
- package/dist/ui.d.cts +75 -2
- package/dist/ui.d.ts +75 -2
- package/dist/ui.js +3 -3
- package/dist/vela.global.js +1046 -274
- package/dist/vela.global.min.js +82 -87
- package/dist/widget.cjs +1504 -327
- package/dist/widget.d.cts +26 -7
- package/dist/widget.d.ts +26 -7
- package/dist/widget.js +7 -7
- package/dist/workspace.cjs +1507 -327
- package/dist/workspace.d.cts +125 -7
- package/dist/workspace.d.ts +125 -7
- package/dist/workspace.js +6 -6
- package/package.json +2 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { deserializeDrawing, chartTypes, rendererLayers, isLineLikeSeries, seriesShownOn, foldBaseModulation, registerChartType, rendererDefaults, getDrawingType, settingsRowVisible, normalizeSettingsRow, Magnifier, TextLabel, Callout, resetDrawingSettings, registerNativeIndicator, chartType, getNativeIndicator, nativeIndicatorDescriptors, nativeInstanceChannel, drawingTypes, settingsRowValueKeys, formatDuration, RegressionChannel, FixedRangeVolumeProfile, DEFAULT_DRAWING_COLOR, SegmentDrawing, ArrowMark, GlyphStamp, RadialFib, FibSpiral, DedekindTessellation, MachFigure, GannSquare, FibRatios, MeasureBox, PositionTool, AnchoredVwap, PatternDrawing, Comment, PriceNote, Signpost, Note, PriceLabel, GANN_SQUARE_ARCS, magnifierTimeframeLabel, lineSegmentIntersection, MAGNIFIER_TIMEFRAME_OPTIONS, GLYPH_OPTIONS, STAMP_SIZE_OPTIONS, LINE_STYLE_OPTIONS, DEDEKIND_CURVATURE_OPTIONS, MACH_NUMBER_OPTIONS, MACH_WAVE_COUNT_OPTIONS, TEXT_SIZE_OPTIONS, createDrawing, inputVisible, seriesInScale, tickerModifierIds, CalloutBase, DIRECTION_OPTIONS, stableSeriesId } from './chunk-
|
|
2
|
-
import { themeTokens, Dialog, closeOpenPopovers, closeWidthPopover, fieldSection, buildFieldControl, fieldSeparator, fieldGrid, fieldRow, CALLOUT_STYLE_ID, CALLOUT_CSS, Popover, blendOver, splitColor, CalloutBubble, Menu, TextArea, NumberInput, buildColorPicker, isPopoverOpen, eventDismissedPopover, toggleSelectList, openPopoverTrigger, fieldGridColumns, FIELD_GAP_PX, STATIC_TOKENS } from './chunk-
|
|
3
|
-
import { icon, iconAt, withAlpha, WARNING, ACCENT, NEUTRAL, INFO, BULLISH, BEARISH, isDarkColor, injectStyles, SERIES_LINE, CHIP_PLATE, CROSSHAIR, TRADE_EXIT, TRADE_SHORT, TRADE_LONG, iconMarkup, overlayScrollbarCss, SLATE_DEEP, VALID, INVALID, SLATE, FIELD_FOCUS_CSS, FIELD_FOCUS_RING, svg24, svg24Solid } from './chunk-
|
|
1
|
+
import { deserializeDrawing, chartTypes, rendererLayers, isLineLikeSeries, seriesShownOn, foldBaseModulation, registerChartType, rendererDefaults, getDrawingType, settingsRowVisible, normalizeSettingsRow, Magnifier, TextLabel, Callout, resetDrawingSettings, registerNativeIndicator, chartType, getNativeIndicator, nativeIndicatorDescriptors, nativeInstanceChannel, drawingTypes, settingsRowValueKeys, formatDuration, RegressionChannel, FixedRangeVolumeProfile, DEFAULT_DRAWING_COLOR, SegmentDrawing, ArrowMark, GlyphStamp, RadialFib, FibSpiral, DedekindTessellation, MachFigure, GannSquare, FibRatios, MeasureBox, PositionTool, AnchoredVwap, PatternDrawing, Comment, PriceNote, Signpost, Note, PriceLabel, GANN_SQUARE_ARCS, magnifierTimeframeLabel, lineSegmentIntersection, MAGNIFIER_TIMEFRAME_OPTIONS, GLYPH_OPTIONS, STAMP_SIZE_OPTIONS, LINE_STYLE_OPTIONS, DEDEKIND_CURVATURE_OPTIONS, MACH_NUMBER_OPTIONS, MACH_WAVE_COUNT_OPTIONS, TEXT_SIZE_OPTIONS, createDrawing, inputVisible, seriesInScale, tickerModifierIds, CalloutBase, DIRECTION_OPTIONS, stableSeriesId } from './chunk-EZ5FWVLA.js';
|
|
2
|
+
import { themeTokens, Dialog, closeOpenPopovers, closeWidthPopover, fieldSection, buildFieldControl, fieldSeparator, fieldGrid, fieldRow, CALLOUT_STYLE_ID, CALLOUT_CSS, Popover, blendOver, splitColor, CalloutBubble, Menu, TextArea, NumberInput, buildColorPicker, isPopoverOpen, DatePicker, eventDismissedPopover, toggleSelectList, normalizeDateInput, openPopoverTrigger, fieldGridColumns, FIELD_GAP_PX, STATIC_TOKENS } from './chunk-NELQJCGK.js';
|
|
3
|
+
import { icon, iconAt, withAlpha, WARNING, ACCENT, NEUTRAL, INFO, BULLISH, BEARISH, isDarkColor, injectStyles, SERIES_LINE, CHIP_PLATE, CROSSHAIR, TRADE_EXIT, TRADE_SHORT, TRADE_LONG, iconMarkup, overlayScrollbarCss, SLATE_DEEP, VALID, INVALID, SLATE, FIELD_FOCUS_CSS, FIELD_FOCUS_RING, svg24, svg24Solid } from './chunk-BZQM2XO7.js';
|
|
4
4
|
|
|
5
5
|
// src/core/events/EventBus.ts
|
|
6
6
|
var TypedEventBus = class {
|
|
@@ -514,6 +514,12 @@ var RendererControl = class {
|
|
|
514
514
|
onCrosshairMove(cb) {
|
|
515
515
|
return this.renderer.onCrosshairMove(cb);
|
|
516
516
|
}
|
|
517
|
+
/** A click — or a touch tap — on the plot, never the end of a pan: the open time of
|
|
518
|
+
* the bar under it (`null` off the bars). On touch, where a tap moves no crosshair,
|
|
519
|
+
* this is how an interaction learns which bar was chosen. */
|
|
520
|
+
onClick(cb) {
|
|
521
|
+
return this.renderer.onClick(cb);
|
|
522
|
+
}
|
|
517
523
|
/** Touch long-press on a price or time axis strip — silent no-op without the seam. */
|
|
518
524
|
onAxisLongPress(cb) {
|
|
519
525
|
return this.renderer.onAxisLongPress?.(cb) ?? (() => void 0);
|
|
@@ -1780,11 +1786,76 @@ var MarksControl = class {
|
|
|
1780
1786
|
}
|
|
1781
1787
|
};
|
|
1782
1788
|
|
|
1789
|
+
// src/core/ReplayControl.ts
|
|
1790
|
+
var ReplayControl = class {
|
|
1791
|
+
constructor(ctrl) {
|
|
1792
|
+
this.ctrl = ctrl;
|
|
1793
|
+
}
|
|
1794
|
+
/**
|
|
1795
|
+
* Enter replay (or seek, when already replaying) at `opts.from`. Resolves once the
|
|
1796
|
+
* chart shows the rewound history — after any in-flight history load has finished.
|
|
1797
|
+
* Starts paused; call {@link play} or {@link step}.
|
|
1798
|
+
*/
|
|
1799
|
+
start(opts) {
|
|
1800
|
+
return this.ctrl.replayStart(opts);
|
|
1801
|
+
}
|
|
1802
|
+
/** Reveal the next bar — or, mid-way through a bar played tick by tick, complete it.
|
|
1803
|
+
* Returns false when replay is off or nothing is left. */
|
|
1804
|
+
step() {
|
|
1805
|
+
return this.ctrl.replayStep();
|
|
1806
|
+
}
|
|
1807
|
+
/** Reveal the next UPDATE: with {@link setTicks}, the next tick of the forming bar — or
|
|
1808
|
+
* the next bar opened at its first tick (once its ticks are in, if still loading);
|
|
1809
|
+
* otherwise the next whole bar, like {@link step}. Returns false when nothing is left. */
|
|
1810
|
+
stepUpdate() {
|
|
1811
|
+
return this.ctrl.replayStepUpdate();
|
|
1812
|
+
}
|
|
1813
|
+
/** Reveal one bar (one tick, with {@link setTicks}) every `intervalMs` (default: the last
|
|
1814
|
+
* interval used, else 1000). Calling it again while playing changes the pace. */
|
|
1815
|
+
play(intervalMs2) {
|
|
1816
|
+
this.ctrl.replayPlay(intervalMs2);
|
|
1817
|
+
return this;
|
|
1818
|
+
}
|
|
1819
|
+
pause() {
|
|
1820
|
+
this.ctrl.replayPause();
|
|
1821
|
+
return this;
|
|
1822
|
+
}
|
|
1823
|
+
/** Leave replay: the full history comes back and live updates resume. Also cancels a
|
|
1824
|
+
* `start()` still loading older history (the chart returns to its previous depth). */
|
|
1825
|
+
stop() {
|
|
1826
|
+
this.ctrl.replayStop();
|
|
1827
|
+
return this;
|
|
1828
|
+
}
|
|
1829
|
+
get state() {
|
|
1830
|
+
return this.ctrl.replayState();
|
|
1831
|
+
}
|
|
1832
|
+
/** The loaded history a replay can walk — hidden bars included while replaying. Null
|
|
1833
|
+
* before any bar loaded. A start older than `first` deepens the history first. */
|
|
1834
|
+
get bounds() {
|
|
1835
|
+
return this.ctrl.replayBounds();
|
|
1836
|
+
}
|
|
1837
|
+
/**
|
|
1838
|
+
* Play each revealed bar as a series of intrabar updates instead of whole: while
|
|
1839
|
+
* playing, the forming candle opens at the first tick, then every tick moves its close
|
|
1840
|
+
* and stretches its high and low, exactly like a live tick — indicators, chart types
|
|
1841
|
+
* and the `bar` event follow — and the last one settles it on the stored bar. The play
|
|
1842
|
+
* interval is then the time between two TICKS: a bar lasts as many intervals as it
|
|
1843
|
+
* has ticks (below ~16 ms, ticks are batched per frame). The next bar's ticks are
|
|
1844
|
+
* requested while the current one plays; playback waits for them. `null` goes back to
|
|
1845
|
+
* whole bars. Takes effect from the next bar; a bar forming when the source changes
|
|
1846
|
+
* completes at once.
|
|
1847
|
+
*/
|
|
1848
|
+
setTicks(source) {
|
|
1849
|
+
this.ctrl.replaySetTicks(source);
|
|
1850
|
+
return this;
|
|
1851
|
+
}
|
|
1852
|
+
};
|
|
1853
|
+
|
|
1783
1854
|
// src/core/util/wall-clock.ts
|
|
1784
1855
|
var BOUNDARY_SLACK_MS = 5;
|
|
1785
1856
|
var SecondClock = class _SecondClock {
|
|
1786
|
-
constructor(
|
|
1787
|
-
this.now =
|
|
1857
|
+
constructor(now2 = () => Date.now()) {
|
|
1858
|
+
this.now = now2;
|
|
1788
1859
|
this.subs = /* @__PURE__ */ new Set();
|
|
1789
1860
|
this.timer = null;
|
|
1790
1861
|
}
|
|
@@ -1800,15 +1871,15 @@ var SecondClock = class _SecondClock {
|
|
|
1800
1871
|
};
|
|
1801
1872
|
}
|
|
1802
1873
|
/** Milliseconds from `now` to just past the next second boundary. */
|
|
1803
|
-
static delayToNextSecond(
|
|
1804
|
-
const intoSecond = (
|
|
1874
|
+
static delayToNextSecond(now2) {
|
|
1875
|
+
const intoSecond = (now2 % 1e3 + 1e3) % 1e3;
|
|
1805
1876
|
return 1e3 - intoSecond + BOUNDARY_SLACK_MS;
|
|
1806
1877
|
}
|
|
1807
1878
|
arm() {
|
|
1808
1879
|
this.timer = setTimeout(() => {
|
|
1809
1880
|
this.timer = null;
|
|
1810
|
-
const
|
|
1811
|
-
for (const cb of this.subs) cb(
|
|
1881
|
+
const now2 = this.now();
|
|
1882
|
+
for (const cb of this.subs) cb(now2);
|
|
1812
1883
|
if (this.subs.size > 0) this.arm();
|
|
1813
1884
|
}, _SecondClock.delayToNextSecond(this.now()));
|
|
1814
1885
|
}
|
|
@@ -3801,10 +3872,16 @@ var IndicatorInputsDialog = class {
|
|
|
3801
3872
|
this.calendarPop = null;
|
|
3802
3873
|
this.calendarAnchor = null;
|
|
3803
3874
|
},
|
|
3804
|
-
content: (el) =>
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
|
|
3875
|
+
content: (el) => {
|
|
3876
|
+
const picker = new DatePicker({
|
|
3877
|
+
value: current,
|
|
3878
|
+
onPick: (iso) => {
|
|
3879
|
+
pop.hide();
|
|
3880
|
+
onPick(iso);
|
|
3881
|
+
}
|
|
3882
|
+
});
|
|
3883
|
+
el.append(picker.el);
|
|
3884
|
+
}
|
|
3808
3885
|
});
|
|
3809
3886
|
this.calendarPop = pop;
|
|
3810
3887
|
this.calendarAnchor = anchor;
|
|
@@ -3929,28 +4006,6 @@ function ensureDialogStyles() {
|
|
|
3929
4006
|
.vela-ind-combo-chevron{position:absolute;right:0;top:0;bottom:0;width:26px;border:none;background:transparent;color:inherit;opacity:0.55;cursor:pointer;display:flex;align-items:center;justify-content:center;padding:0;}
|
|
3930
4007
|
.vela-ind-combo-chevron:hover{opacity:0.9;}
|
|
3931
4008
|
.vela-ind-cal{background:var(--vela-bg);color:var(--vela-fg);border:none;border-radius:6px;box-shadow:var(--vela-shadow);font:14px var(--vela-font);padding:10px 12px;user-select:none;}
|
|
3932
|
-
.vela-ind-cal [hidden]{display:none !important;}
|
|
3933
|
-
.vela-ind-cal-head{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:8px;}
|
|
3934
|
-
.vela-ind-cal-title{flex:1;display:flex;align-items:center;justify-content:center;gap:2px;min-width:0;}
|
|
3935
|
-
.vela-ind-cal-switch{border:none;background:transparent;color:var(--vela-fg-bright);font:inherit;font-weight:600;font-size:14px;padding:2px 6px;border-radius:4px;cursor:pointer;}
|
|
3936
|
-
.vela-ind-cal-switch:not(:disabled):hover{background:var(--vela-hover);}
|
|
3937
|
-
.vela-ind-cal-switch:disabled{cursor:default;}
|
|
3938
|
-
.vela-ind-cal-nav{width:24px;height:24px;border:none;background:transparent;color:var(--vela-fg-muted);border-radius:4px;padding:0;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;}
|
|
3939
|
-
.vela-ind-cal-nav:hover{background:var(--vela-hover);color:var(--vela-fg-bright);}
|
|
3940
|
-
.vela-ind-cal-week,.vela-ind-cal-grid{display:grid;grid-template-columns:repeat(7,28px);gap:2px;}
|
|
3941
|
-
.vela-ind-cal-week{margin-bottom:4px;color:var(--vela-fg-muted);font-size:11px;text-align:center;}
|
|
3942
|
-
.vela-ind-cal-week span{line-height:20px;}
|
|
3943
|
-
.vela-ind-cal-blank{width:28px;height:28px;}
|
|
3944
|
-
.vela-ind-cal-day{width:28px;height:28px;border:none;background:transparent;color:inherit;border-radius:4px;padding:0;cursor:pointer;font:inherit;font-size:14px;}
|
|
3945
|
-
.vela-ind-cal-day:hover{background:var(--vela-hover);}
|
|
3946
|
-
.vela-ind-cal-day[data-checked]{background:var(--vela-hover-strong);color:var(--vela-fg-bright);}
|
|
3947
|
-
.vela-ind-cal-day[data-today]:not([data-checked]){box-shadow:inset 0 0 0 1px var(--vela-border-strong);}
|
|
3948
|
-
.vela-ind-cal-cells{display:grid;grid-template-columns:repeat(3,1fr);gap:4px;width:calc(7 * 28px + 6 * 2px);}
|
|
3949
|
-
.vela-ind-cal-cell{height:36px;border:none;background:transparent;color:inherit;border-radius:4px;padding:0 4px;cursor:pointer;font:inherit;font-size:14px;}
|
|
3950
|
-
.vela-ind-cal-cell:hover{background:var(--vela-hover);}
|
|
3951
|
-
.vela-ind-cal-cell[data-checked]{background:var(--vela-hover-strong);color:var(--vela-fg-bright);}
|
|
3952
|
-
.vela-ind-cal-cell[data-today]:not([data-checked]){box-shadow:inset 0 0 0 1px var(--vela-border-strong);}
|
|
3953
|
-
.vela-ind-cal-cell[data-outside]{opacity:0.45;}
|
|
3954
4009
|
${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
|
|
3955
4010
|
.vela-ind-tab{font-weight:600;font-size:13px;line-height:20px;transition:color var(--vela-dur-fast) ease,border-color var(--vela-dur-fast) ease;}
|
|
3956
4011
|
.vela-ind-tab:not(.vela-ind-tab-active):hover{color:var(--vela-fg-bright);}
|
|
@@ -3984,174 +4039,6 @@ var TIME_OPTIONS = Array.from({ length: 48 }, (_, i) => {
|
|
|
3984
4039
|
const v = `${hh}:${mm}`;
|
|
3985
4040
|
return { value: v, label: v };
|
|
3986
4041
|
});
|
|
3987
|
-
var MONTH_LABELS = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
|
|
3988
|
-
var MONTH_SHORT = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
|
3989
|
-
var WEEKDAY_LABELS = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];
|
|
3990
|
-
function normalizeDateInput(raw) {
|
|
3991
|
-
const t = raw.trim();
|
|
3992
|
-
const m = /^(\d{4})-(\d{1,2})-(\d{1,2})$/.exec(t);
|
|
3993
|
-
if (!m) return null;
|
|
3994
|
-
const y = Number(m[1]);
|
|
3995
|
-
const mo = Number(m[2]);
|
|
3996
|
-
const d = Number(m[3]);
|
|
3997
|
-
const dt = new Date(y, mo - 1, d);
|
|
3998
|
-
if (dt.getFullYear() !== y || dt.getMonth() !== mo - 1 || dt.getDate() !== d) return null;
|
|
3999
|
-
return isoDate(dt);
|
|
4000
|
-
}
|
|
4001
|
-
function parseIsoDate(raw) {
|
|
4002
|
-
const iso = normalizeDateInput(raw);
|
|
4003
|
-
if (!iso) return null;
|
|
4004
|
-
const [y, mo, d] = iso.split("-").map(Number);
|
|
4005
|
-
return new Date(y, mo - 1, d);
|
|
4006
|
-
}
|
|
4007
|
-
function isoDate(d) {
|
|
4008
|
-
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
|
4009
|
-
}
|
|
4010
|
-
function fillCalendar(el, current, onPick) {
|
|
4011
|
-
const today = /* @__PURE__ */ new Date();
|
|
4012
|
-
const selected = parseIsoDate(current);
|
|
4013
|
-
let year = selected?.getFullYear() ?? today.getFullYear();
|
|
4014
|
-
let month = selected?.getMonth() ?? today.getMonth();
|
|
4015
|
-
let mode = "date";
|
|
4016
|
-
const prev2 = document.createElement("button");
|
|
4017
|
-
prev2.type = "button";
|
|
4018
|
-
prev2.className = "vela-ind-cal-nav";
|
|
4019
|
-
prev2.innerHTML = iconAt("chevron-left", 14);
|
|
4020
|
-
const next = document.createElement("button");
|
|
4021
|
-
next.type = "button";
|
|
4022
|
-
next.className = "vela-ind-cal-nav";
|
|
4023
|
-
next.innerHTML = iconAt("chevron-right", 14);
|
|
4024
|
-
const monthBtn = document.createElement("button");
|
|
4025
|
-
monthBtn.type = "button";
|
|
4026
|
-
monthBtn.className = "vela-ind-cal-switch";
|
|
4027
|
-
monthBtn.setAttribute("aria-label", "Choose month");
|
|
4028
|
-
const yearBtn = document.createElement("button");
|
|
4029
|
-
yearBtn.type = "button";
|
|
4030
|
-
yearBtn.className = "vela-ind-cal-switch";
|
|
4031
|
-
yearBtn.setAttribute("aria-label", "Choose year");
|
|
4032
|
-
const title = document.createElement("div");
|
|
4033
|
-
title.className = "vela-ind-cal-title";
|
|
4034
|
-
title.append(monthBtn, yearBtn);
|
|
4035
|
-
const head = document.createElement("div");
|
|
4036
|
-
head.className = "vela-ind-cal-head";
|
|
4037
|
-
head.append(prev2, title, next);
|
|
4038
|
-
const week = document.createElement("div");
|
|
4039
|
-
week.className = "vela-ind-cal-week";
|
|
4040
|
-
for (const d of WEEKDAY_LABELS) {
|
|
4041
|
-
const cell = document.createElement("span");
|
|
4042
|
-
cell.textContent = d;
|
|
4043
|
-
week.appendChild(cell);
|
|
4044
|
-
}
|
|
4045
|
-
const grid = document.createElement("div");
|
|
4046
|
-
const paint = () => {
|
|
4047
|
-
const decade = Math.floor(year / 10) * 10;
|
|
4048
|
-
monthBtn.hidden = mode !== "date";
|
|
4049
|
-
monthBtn.textContent = MONTH_LABELS[month] ?? "";
|
|
4050
|
-
yearBtn.textContent = mode === "year" ? `${decade}-${decade + 9}` : String(year);
|
|
4051
|
-
yearBtn.disabled = mode === "year";
|
|
4052
|
-
week.hidden = mode !== "date";
|
|
4053
|
-
prev2.setAttribute("aria-label", mode === "date" ? "Previous month" : mode === "month" ? "Previous year" : "Previous decade");
|
|
4054
|
-
next.setAttribute("aria-label", mode === "date" ? "Next month" : mode === "month" ? "Next year" : "Next decade");
|
|
4055
|
-
grid.className = mode === "date" ? "vela-ind-cal-grid" : "vela-ind-cal-cells";
|
|
4056
|
-
grid.replaceChildren();
|
|
4057
|
-
if (mode === "date") {
|
|
4058
|
-
const startPad = new Date(year, month, 1).getDay();
|
|
4059
|
-
const days = new Date(year, month + 1, 0).getDate();
|
|
4060
|
-
const todayIso = isoDate(today);
|
|
4061
|
-
const selectedIso = selected ? isoDate(selected) : "";
|
|
4062
|
-
for (let i = 0; i < startPad; i++) {
|
|
4063
|
-
const blank = document.createElement("span");
|
|
4064
|
-
blank.className = "vela-ind-cal-blank";
|
|
4065
|
-
grid.appendChild(blank);
|
|
4066
|
-
}
|
|
4067
|
-
for (let day = 1; day <= days; day++) {
|
|
4068
|
-
const iso = `${year}-${String(month + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
|
|
4069
|
-
const b = document.createElement("button");
|
|
4070
|
-
b.type = "button";
|
|
4071
|
-
b.className = "vela-ind-cal-day";
|
|
4072
|
-
b.textContent = String(day);
|
|
4073
|
-
if (iso === selectedIso) b.dataset.checked = "1";
|
|
4074
|
-
if (iso === todayIso) b.dataset.today = "1";
|
|
4075
|
-
b.addEventListener("click", (e) => {
|
|
4076
|
-
e.stopPropagation();
|
|
4077
|
-
onPick(iso);
|
|
4078
|
-
});
|
|
4079
|
-
grid.appendChild(b);
|
|
4080
|
-
}
|
|
4081
|
-
return;
|
|
4082
|
-
}
|
|
4083
|
-
if (mode === "month") {
|
|
4084
|
-
for (let m = 0; m < 12; m++) {
|
|
4085
|
-
const b = document.createElement("button");
|
|
4086
|
-
b.type = "button";
|
|
4087
|
-
b.className = "vela-ind-cal-cell";
|
|
4088
|
-
b.textContent = MONTH_SHORT[m] ?? "";
|
|
4089
|
-
if (selected?.getFullYear() === year && selected.getMonth() === m) b.dataset.checked = "1";
|
|
4090
|
-
if (today.getFullYear() === year && today.getMonth() === m) b.dataset.today = "1";
|
|
4091
|
-
b.addEventListener("click", (e) => {
|
|
4092
|
-
e.stopPropagation();
|
|
4093
|
-
month = m;
|
|
4094
|
-
mode = "date";
|
|
4095
|
-
paint();
|
|
4096
|
-
});
|
|
4097
|
-
grid.appendChild(b);
|
|
4098
|
-
}
|
|
4099
|
-
return;
|
|
4100
|
-
}
|
|
4101
|
-
for (let y = decade - 1; y <= decade + 10; y++) {
|
|
4102
|
-
const b = document.createElement("button");
|
|
4103
|
-
b.type = "button";
|
|
4104
|
-
b.className = "vela-ind-cal-cell";
|
|
4105
|
-
b.textContent = String(y);
|
|
4106
|
-
if (y < decade || y > decade + 9) b.dataset.outside = "1";
|
|
4107
|
-
if (selected?.getFullYear() === y) b.dataset.checked = "1";
|
|
4108
|
-
if (today.getFullYear() === y) b.dataset.today = "1";
|
|
4109
|
-
b.addEventListener("click", (e) => {
|
|
4110
|
-
e.stopPropagation();
|
|
4111
|
-
year = y;
|
|
4112
|
-
mode = "month";
|
|
4113
|
-
paint();
|
|
4114
|
-
});
|
|
4115
|
-
grid.appendChild(b);
|
|
4116
|
-
}
|
|
4117
|
-
};
|
|
4118
|
-
const step = (dir) => {
|
|
4119
|
-
if (mode === "date") {
|
|
4120
|
-
month += dir;
|
|
4121
|
-
if (month < 0) {
|
|
4122
|
-
month = 11;
|
|
4123
|
-
year -= 1;
|
|
4124
|
-
}
|
|
4125
|
-
if (month > 11) {
|
|
4126
|
-
month = 0;
|
|
4127
|
-
year += 1;
|
|
4128
|
-
}
|
|
4129
|
-
} else {
|
|
4130
|
-
year += mode === "month" ? dir : dir * 10;
|
|
4131
|
-
}
|
|
4132
|
-
paint();
|
|
4133
|
-
};
|
|
4134
|
-
prev2.addEventListener("click", (e) => {
|
|
4135
|
-
e.stopPropagation();
|
|
4136
|
-
step(-1);
|
|
4137
|
-
});
|
|
4138
|
-
next.addEventListener("click", (e) => {
|
|
4139
|
-
e.stopPropagation();
|
|
4140
|
-
step(1);
|
|
4141
|
-
});
|
|
4142
|
-
monthBtn.addEventListener("click", (e) => {
|
|
4143
|
-
e.stopPropagation();
|
|
4144
|
-
mode = "month";
|
|
4145
|
-
paint();
|
|
4146
|
-
});
|
|
4147
|
-
yearBtn.addEventListener("click", (e) => {
|
|
4148
|
-
e.stopPropagation();
|
|
4149
|
-
mode = "year";
|
|
4150
|
-
paint();
|
|
4151
|
-
});
|
|
4152
|
-
paint();
|
|
4153
|
-
el.append(head, week, grid);
|
|
4154
|
-
}
|
|
4155
4042
|
function normalizeTimeInput(raw) {
|
|
4156
4043
|
const t = raw.trim();
|
|
4157
4044
|
const m = /^(\d{1,2}):(\d{2})$/.exec(t) ?? /^(\d{2})(\d{2})$/.exec(t);
|
|
@@ -5529,6 +5416,22 @@ function priceStyleIds() {
|
|
|
5529
5416
|
for (const t of chartTypes()) if (!out.includes(t.id)) out.push(t.id);
|
|
5530
5417
|
return out;
|
|
5531
5418
|
}
|
|
5419
|
+
function sanitizeCrosshairOverride(value) {
|
|
5420
|
+
if (!value || typeof value !== "object") return null;
|
|
5421
|
+
const v = value;
|
|
5422
|
+
const out = {};
|
|
5423
|
+
if (isBool(v.vertical)) out.vertical = v.vertical;
|
|
5424
|
+
if (isBool(v.horizontal)) out.horizontal = v.horizontal;
|
|
5425
|
+
if (isColor(v.color)) out.color = v.color;
|
|
5426
|
+
if (isNum(v.width)) out.width = Math.max(0.5, Math.min(8, v.width));
|
|
5427
|
+
if (isLineStyle(v.style)) out.style = v.style;
|
|
5428
|
+
if (isNum(v.opacity)) out.opacity = clampOpacity(v.opacity);
|
|
5429
|
+
const shade = v.shadeRight;
|
|
5430
|
+
if (shade && typeof shade === "object" && isColor(shade.color)) {
|
|
5431
|
+
out.shadeRight = isNum(shade.opacity) ? { color: shade.color, opacity: clampOpacity(shade.opacity) } : { color: shade.color };
|
|
5432
|
+
}
|
|
5433
|
+
return Object.keys(out).length > 0 ? out : null;
|
|
5434
|
+
}
|
|
5532
5435
|
function isColor(v) {
|
|
5533
5436
|
return typeof v === "string" && v.trim().length > 0;
|
|
5534
5437
|
}
|
|
@@ -7190,7 +7093,7 @@ var Scheduler = class {
|
|
|
7190
7093
|
|
|
7191
7094
|
// src/renderers/native/core/Animator.ts
|
|
7192
7095
|
var Animator = class {
|
|
7193
|
-
constructor(tick, raf, cancel,
|
|
7096
|
+
constructor(tick, raf, cancel, now2) {
|
|
7194
7097
|
this.tick = tick;
|
|
7195
7098
|
this.handle = null;
|
|
7196
7099
|
this.last = 0;
|
|
@@ -7204,7 +7107,7 @@ var Animator = class {
|
|
|
7204
7107
|
};
|
|
7205
7108
|
this.raf = raf ?? ((cb) => requestAnimationFrame(cb));
|
|
7206
7109
|
this.cancel = cancel ?? ((h) => cancelAnimationFrame(h));
|
|
7207
|
-
this.now =
|
|
7110
|
+
this.now = now2 ?? (() => performance.now());
|
|
7208
7111
|
}
|
|
7209
7112
|
get active() {
|
|
7210
7113
|
return this.handle !== null;
|
|
@@ -7336,6 +7239,12 @@ var InputController = class {
|
|
|
7336
7239
|
this.lastTapT = 0;
|
|
7337
7240
|
this.lastTapX = 0;
|
|
7338
7241
|
this.lastTapY = 0;
|
|
7242
|
+
// Whether the last two primary presses were claimed by the drawings layer. A double-click
|
|
7243
|
+
// is the last press plus the one before it; a tool-driven click pair must never fall through
|
|
7244
|
+
// to the pane maximize toggle, even when the tool disarmed itself between the two clicks
|
|
7245
|
+
// (a finished placement leaves plain cursor mode for the second click).
|
|
7246
|
+
this.lastPressDrawing = false;
|
|
7247
|
+
this.prevPressDrawing = false;
|
|
7339
7248
|
this.onTouchContextMenu = (e) => {
|
|
7340
7249
|
if (this.touches.size === 0) return;
|
|
7341
7250
|
if (!(e.target instanceof Node) || !this.el?.contains(e.target)) return;
|
|
@@ -7390,19 +7299,24 @@ var InputController = class {
|
|
|
7390
7299
|
this.moved = false;
|
|
7391
7300
|
this.startX = x;
|
|
7392
7301
|
this.startY = y;
|
|
7302
|
+
this.prevPressDrawing = this.lastPressDrawing;
|
|
7303
|
+
this.lastPressDrawing = false;
|
|
7393
7304
|
if (this.deps.drawingsClaim?.(x, y)) {
|
|
7394
7305
|
this.region = "drawing";
|
|
7306
|
+
this.lastPressDrawing = true;
|
|
7395
7307
|
this.deps.drawingsPointerDown?.(x, y, this.snapMode(e), e.shiftKey, e.ctrlKey || e.metaKey);
|
|
7396
7308
|
this.capture(e.pointerId);
|
|
7397
7309
|
return;
|
|
7398
7310
|
}
|
|
7399
7311
|
if (e.shiftKey && this.regionAt(x, y) === "data" && this.deps.drawingsMeasureStart?.(x, y, this.snapMode(e))) {
|
|
7400
7312
|
this.region = "drawing";
|
|
7313
|
+
this.lastPressDrawing = true;
|
|
7401
7314
|
this.capture(e.pointerId);
|
|
7402
7315
|
return;
|
|
7403
7316
|
}
|
|
7404
7317
|
if ((e.ctrlKey || e.metaKey) && this.regionAt(x, y) === "data" && this.deps.drawingsMarqueeStart?.(x, y)) {
|
|
7405
7318
|
this.region = "drawing";
|
|
7319
|
+
this.lastPressDrawing = true;
|
|
7406
7320
|
this.capture(e.pointerId);
|
|
7407
7321
|
return;
|
|
7408
7322
|
}
|
|
@@ -7724,6 +7638,7 @@ var InputController = class {
|
|
|
7724
7638
|
if (region === "price") this.deps.resetPriceScale(x, y);
|
|
7725
7639
|
else if (region === "separator") this.deps.resetPaneSize(y);
|
|
7726
7640
|
else if (region === "time") this.deps.resetView();
|
|
7641
|
+
else if (this.lastPressDrawing || this.prevPressDrawing) return;
|
|
7727
7642
|
else this.deps.dataDblClick(x, y);
|
|
7728
7643
|
}
|
|
7729
7644
|
};
|
|
@@ -8039,6 +7954,8 @@ var SceneGraph = class {
|
|
|
8039
7954
|
/** VPVR-layer config pushed by the VPVR native indicator (null ⇒ layer off). Ephemeral. */
|
|
8040
7955
|
this.vpvrLayer = null;
|
|
8041
7956
|
this.crosshair = null;
|
|
7957
|
+
/** Runtime override of the crosshair's lines and style (the `crosshairOverride` feature). Ephemeral. */
|
|
7958
|
+
this.crosshairOverride = null;
|
|
8042
7959
|
/** How the base price series is drawn on the price pane (candles by default). */
|
|
8043
7960
|
this.priceStyle = "candles";
|
|
8044
7961
|
/** Price-series base painting for the ACTIVE style (see ChartTypeDefinition.basePainting). */
|
|
@@ -9923,6 +9840,7 @@ function timeTicks(fromMs, toMs, target = 8, offsetMs = 0) {
|
|
|
9923
9840
|
const step = pickStep(span / Math.max(1, target));
|
|
9924
9841
|
const zFrom = fromMs + offsetMs;
|
|
9925
9842
|
const zTo = toMs + offsetMs;
|
|
9843
|
+
if (step >= MONTH) return calendarTicks(zFrom, zTo, step >= YEAR ? 12 : step >= 3 * MONTH ? 3 : 1, offsetMs);
|
|
9926
9844
|
const first = Math.ceil(zFrom / step) * step;
|
|
9927
9845
|
const out = [];
|
|
9928
9846
|
for (let zt = first; zt <= zTo; zt += step) {
|
|
@@ -9953,11 +9871,23 @@ function timeTicks(fromMs, toMs, target = 8, offsetMs = 0) {
|
|
|
9953
9871
|
}
|
|
9954
9872
|
return out;
|
|
9955
9873
|
}
|
|
9874
|
+
function calendarTicks(zFrom, zTo, months, offsetMs) {
|
|
9875
|
+
const d0 = new Date(zFrom);
|
|
9876
|
+
let idx = d0.getUTCFullYear() * 12 + d0.getUTCMonth();
|
|
9877
|
+
if (Date.UTC(Math.floor(idx / 12), idx % 12, 1) < zFrom) idx += 1;
|
|
9878
|
+
idx = Math.ceil(idx / months) * months;
|
|
9879
|
+
const out = [];
|
|
9880
|
+
for (let zt = Date.UTC(Math.floor(idx / 12), idx % 12, 1); zt <= zTo; idx += months, zt = Date.UTC(Math.floor(idx / 12), idx % 12, 1)) {
|
|
9881
|
+
const label = months === 12 ? String(Math.floor(idx / 12)) : MONTHS[idx % 12];
|
|
9882
|
+
out.push({ time: zt - offsetMs, label, major: true });
|
|
9883
|
+
}
|
|
9884
|
+
return out;
|
|
9885
|
+
}
|
|
9956
9886
|
|
|
9957
9887
|
// src/renderers/native/chrome/countdown.ts
|
|
9958
|
-
function countdownText(barOpen, barMs,
|
|
9888
|
+
function countdownText(barOpen, barMs, now2) {
|
|
9959
9889
|
if (!(barMs > 0)) return null;
|
|
9960
|
-
const remaining = barOpen + barMs -
|
|
9890
|
+
const remaining = barOpen + barMs - now2;
|
|
9961
9891
|
if (remaining <= 0) return null;
|
|
9962
9892
|
return formatCountdown(remaining);
|
|
9963
9893
|
}
|
|
@@ -10769,19 +10699,35 @@ var CrosshairRenderer = class {
|
|
|
10769
10699
|
const dataH = coords.height;
|
|
10770
10700
|
if (!ch || ch.x < 0 || ch.x > dataW || ch.y < 0 || ch.y > dataH) return;
|
|
10771
10701
|
const cs = scene.style.crosshair;
|
|
10702
|
+
const ov = scene.crosshairOverride;
|
|
10703
|
+
const vertical = ov?.vertical !== false;
|
|
10704
|
+
const horizontal = ov?.horizontal !== false;
|
|
10772
10705
|
ctx.font = `${scene.style.fontSize}px ${theme.fontFamily}`;
|
|
10773
10706
|
ctx.textBaseline = "middle";
|
|
10774
10707
|
const logical = Math.round(coords.xToLogical(ch.x));
|
|
10775
|
-
const x =
|
|
10776
|
-
|
|
10777
|
-
|
|
10778
|
-
|
|
10779
|
-
|
|
10708
|
+
const x = crisp(coords.logicalToX(logical), ov?.width ?? cs.width);
|
|
10709
|
+
if (vertical && ov?.shadeRight) {
|
|
10710
|
+
const from = Math.max(0, Math.round(coords.logicalToX(logical + 0.5)));
|
|
10711
|
+
if (from < dataW) {
|
|
10712
|
+
ctx.fillStyle = ov.shadeRight.color;
|
|
10713
|
+
ctx.globalAlpha = ov.shadeRight.opacity ?? 1;
|
|
10714
|
+
ctx.fillRect(from, 0, dataW - from, dataH);
|
|
10715
|
+
ctx.globalAlpha = 1;
|
|
10716
|
+
}
|
|
10717
|
+
}
|
|
10718
|
+
ctx.strokeStyle = ov?.color ?? cs.color ?? theme.textColor;
|
|
10719
|
+
ctx.lineWidth = ov?.width ?? cs.width;
|
|
10720
|
+
ctx.globalAlpha = ov?.opacity ?? cs.opacity;
|
|
10721
|
+
setDash3(ctx, ov?.style ?? cs.style);
|
|
10780
10722
|
ctx.beginPath();
|
|
10781
|
-
|
|
10782
|
-
|
|
10783
|
-
|
|
10784
|
-
|
|
10723
|
+
if (vertical) {
|
|
10724
|
+
ctx.moveTo(x, 0);
|
|
10725
|
+
ctx.lineTo(x, dataH);
|
|
10726
|
+
}
|
|
10727
|
+
if (horizontal) {
|
|
10728
|
+
ctx.moveTo(0, Math.round(ch.y) + 0.5);
|
|
10729
|
+
ctx.lineTo(dataW, Math.round(ch.y) + 0.5);
|
|
10730
|
+
}
|
|
10785
10731
|
ctx.stroke();
|
|
10786
10732
|
ctx.setLineDash([]);
|
|
10787
10733
|
ctx.lineWidth = 1;
|
|
@@ -10794,11 +10740,11 @@ var CrosshairRenderer = class {
|
|
|
10794
10740
|
}
|
|
10795
10741
|
}
|
|
10796
10742
|
const chipBg = cs.labelBackground ?? theme.borderColor;
|
|
10797
|
-
if (pane && pane.axisFormat !== "none") {
|
|
10743
|
+
if (horizontal && pane && pane.axisFormat !== "none") {
|
|
10798
10744
|
const price = coords.yToPrice(ch.y, pane.scale, pane.bounds);
|
|
10799
10745
|
this.chip(ctx, dataW + 1, ch.y, formatAxisValue(pane.scale, pane.bounds.height, price, percentScaleFor(scene, pane), scene.priceMintick, pane.axisFormat), chipBg, "left", false, theme.background);
|
|
10800
10746
|
}
|
|
10801
|
-
this.chip(ctx, x, dataH + 1, formatTimeStamp(coords.logicalToTime(logical), scene.timezone, coords.barInterval), chipBg, "center", true, theme.background);
|
|
10747
|
+
if (vertical) this.chip(ctx, x, dataH + 1, formatTimeStamp(coords.logicalToTime(logical), scene.timezone, coords.barInterval), chipBg, "center", true, theme.background);
|
|
10802
10748
|
}
|
|
10803
10749
|
destroy() {
|
|
10804
10750
|
this.canvas = null;
|
|
@@ -10809,23 +10755,37 @@ var CrosshairRenderer = class {
|
|
|
10809
10755
|
* along), with that bar's time chip in this chart's own timezone and — when the
|
|
10810
10756
|
* level resolved — the price chip on the right axis. The snap happened upstream
|
|
10811
10757
|
* (`externalCrossPx`, floor-to-containing-bar) — this method only draws. Chips
|
|
10812
|
-
* render slightly dimmed so the ghost still reads as foreign.
|
|
10758
|
+
* render slightly dimmed so the ghost still reads as foreign. A `crosshairOverride`
|
|
10759
|
+
* applies to the ghost as well — its line style, a dropped horizontal line, and the
|
|
10760
|
+
* `shadeRight` veil from the ghost bar's right edge — so a pick that spans several
|
|
10761
|
+
* charts reads the same on every one of them. */
|
|
10813
10762
|
drawExternal(ctx, ext, scene, coords, theme) {
|
|
10814
10763
|
const cs = scene.style.crosshair;
|
|
10764
|
+
const ov = scene.crosshairOverride;
|
|
10815
10765
|
const dataW = coords.width;
|
|
10816
10766
|
const dataH = coords.height;
|
|
10817
|
-
const x =
|
|
10818
|
-
if (
|
|
10767
|
+
const x = crisp(ext.x, ov?.width ?? cs.width);
|
|
10768
|
+
if (ov?.vertical === false) return;
|
|
10769
|
+
if (ov?.shadeRight) {
|
|
10770
|
+
const from = Math.max(0, Math.round(coords.logicalToX(Math.round(coords.xToLogical(ext.x)) + 0.5)));
|
|
10771
|
+
if (from < dataW) {
|
|
10772
|
+
ctx.fillStyle = ov.shadeRight.color;
|
|
10773
|
+
ctx.globalAlpha = ov.shadeRight.opacity ?? 1;
|
|
10774
|
+
ctx.fillRect(from, 0, dataW - from, dataH);
|
|
10775
|
+
ctx.globalAlpha = 1;
|
|
10776
|
+
}
|
|
10777
|
+
}
|
|
10778
|
+
if (ext.line === false || x < 0 || x > dataW) return;
|
|
10819
10779
|
ctx.font = `${scene.style.fontSize}px ${theme.fontFamily}`;
|
|
10820
10780
|
ctx.textBaseline = "middle";
|
|
10821
|
-
ctx.strokeStyle = cs.color ?? theme.textColor;
|
|
10822
|
-
ctx.lineWidth = cs.width;
|
|
10823
|
-
ctx.globalAlpha = cs.opacity * 0.55;
|
|
10824
|
-
setDash3(ctx, cs.style);
|
|
10781
|
+
ctx.strokeStyle = ov?.color ?? cs.color ?? theme.textColor;
|
|
10782
|
+
ctx.lineWidth = ov?.width ?? cs.width;
|
|
10783
|
+
ctx.globalAlpha = (ov?.opacity ?? cs.opacity) * 0.55;
|
|
10784
|
+
setDash3(ctx, ov?.style ?? cs.style);
|
|
10825
10785
|
ctx.beginPath();
|
|
10826
10786
|
ctx.moveTo(x, 0);
|
|
10827
10787
|
ctx.lineTo(x, dataH);
|
|
10828
|
-
if (ext.y != null) {
|
|
10788
|
+
if (ext.y != null && ov?.horizontal !== false) {
|
|
10829
10789
|
ctx.moveTo(0, Math.round(ext.y) + 0.5);
|
|
10830
10790
|
ctx.lineTo(dataW, Math.round(ext.y) + 0.5);
|
|
10831
10791
|
}
|
|
@@ -10834,7 +10794,7 @@ var CrosshairRenderer = class {
|
|
|
10834
10794
|
ctx.lineWidth = 1;
|
|
10835
10795
|
ctx.globalAlpha = 0.8;
|
|
10836
10796
|
const chipBg = cs.labelBackground ?? theme.borderColor;
|
|
10837
|
-
if (ext.y != null && ext.price != null) {
|
|
10797
|
+
if (ext.y != null && ext.price != null && ov?.horizontal !== false) {
|
|
10838
10798
|
let pane;
|
|
10839
10799
|
for (const p of scene.panes.values()) {
|
|
10840
10800
|
if (ext.y >= p.bounds.top && ext.y <= p.bounds.top + p.bounds.height) {
|
|
@@ -10874,6 +10834,9 @@ var CrosshairRenderer = class {
|
|
|
10874
10834
|
ctx.textAlign = "start";
|
|
10875
10835
|
}
|
|
10876
10836
|
};
|
|
10837
|
+
function crisp(px, width) {
|
|
10838
|
+
return Math.round(px) + (Math.round(width) % 2 === 1 ? 0.5 : 0);
|
|
10839
|
+
}
|
|
10877
10840
|
function setDash3(ctx, style) {
|
|
10878
10841
|
if (style === "dashed") ctx.setLineDash([6, 4]);
|
|
10879
10842
|
else if (style === "dotted") ctx.setLineDash([2, 3]);
|
|
@@ -17729,23 +17692,21 @@ function stackLayers(entries, candleZ) {
|
|
|
17729
17692
|
return { below, above };
|
|
17730
17693
|
}
|
|
17731
17694
|
|
|
17732
|
-
// src/renderers/native/chrome/
|
|
17733
|
-
var
|
|
17734
|
-
var
|
|
17695
|
+
// src/renderers/native/chrome/vela-logos.ts
|
|
17696
|
+
var VELA_MARK_SVG = '<svg viewBox="0 -142 161.66 148" aria-hidden="true"><g fill="currentColor"><path d="M0 -140L32.91 -140L77.52 -62.73L61.06 -34.24Z"/><path d="M161.66 -140L128.75 -140L84.14 -62.73L100.59 -34.24Z"/><path d="M80.83 -52.04L95.85 -26.02L80.83 0L65.81 -26.02Z"/></g></svg>';
|
|
17697
|
+
var VELA_LETTERS_SVG = '<svg viewBox="161.66 -142 310.57 148" aria-hidden="true"><g fill="currentColor"><path d="M226.3 1.17Q200.81 1.17 186.12 -12.99Q171.42 -27.15 171.42 -51.17Q171.42 -75.2 185.87 -89.6Q200.33 -104 224.25 -104Q247.4 -104 261.7 -90.58Q276.01 -77.15 276.3 -54.59Q276.3 -49.9 275.42 -45.02H200.81V-43.55Q201.2 -32.62 207.75 -26.42Q214.29 -20.21 225.03 -20.21Q233.72 -20.21 239.34 -23.93Q244.95 -27.64 246.81 -34.77H274.64Q272.1 -19.14 259.16 -8.98Q246.22 1.17 226.3 1.17ZM201.5 -63.38H247.88Q246.42 -72.75 240.27 -77.88Q234.12 -83.01 224.45 -83.01Q215.17 -83.01 208.92 -77.73Q202.67 -72.46 201.5 -63.38ZM287.54 0V-140.04H315.76V0ZM429.34 -23.83H432.56V0H418.11Q407.76 0 403.36 -4.44Q398.97 -8.89 399.16 -16.8Q387.83 1.17 365.57 1.17Q348.48 1.17 337.88 -6.74Q327.29 -14.65 327.29 -29Q327.29 -45.41 339.45 -54.3Q351.6 -63.18 374.55 -63.18H394.96V-68.16Q394.96 -75.2 390.08 -79.3Q385.2 -83.4 376.51 -83.4Q368.3 -83.4 363.03 -79.98Q357.76 -76.56 356.88 -70.8H329.73Q331.19 -85.84 344.08 -94.92Q356.97 -104 377.29 -104Q399.06 -104 410.88 -94.43Q422.7 -84.86 422.7 -66.6V-30.18Q422.7 -26.37 424.26 -25.1Q425.82 -23.83 429.34 -23.83ZM394.96 -42.38V-44.43H374.36Q365.47 -44.43 360.59 -40.92Q355.7 -37.4 355.7 -30.96Q355.7 -25.59 359.81 -22.41Q363.91 -19.24 370.94 -19.24Q381.97 -19.24 388.37 -25.54Q394.77 -31.84 394.96 -42.38Z"/><path fill-opacity="0.55" d="M437.44 -124.28H441.96V-113.41H445.34V-124.28H449.87V-127.4H437.44ZM451.43 -113.41H454.7V-123.29L458.97 -113.41H461.62L465.99 -123.29V-113.41H469.21V-127.4H464.53L460.42 -117.68L456.16 -127.4H451.43Z"/></g></svg>';
|
|
17735
17698
|
|
|
17736
17699
|
// src/renderers/native/chrome/AttributionMark.ts
|
|
17737
|
-
var ATTRIBUTION_URL = "https://
|
|
17700
|
+
var ATTRIBUTION_URL = "https://velacharts.dev/?utm_source=vela&utm_medium=attribution";
|
|
17738
17701
|
var STYLE_ID5 = "vela-attribution-styles";
|
|
17739
17702
|
var CSS = `
|
|
17740
17703
|
.vela-attribution {
|
|
17741
17704
|
text-decoration: none;
|
|
17742
17705
|
display: flex;
|
|
17743
17706
|
align-items: center;
|
|
17744
|
-
/*
|
|
17745
|
-
|
|
17746
|
-
|
|
17747
|
-
reads wrong because the symbol's ink hangs low and the wordmark descends. */
|
|
17748
|
-
gap: 4px;
|
|
17707
|
+
/* No gutter: the V is the wordmark's first letter, and the letters' viewBox starts
|
|
17708
|
+
where the V's ends, so the kit's own letter spacing comes with them. */
|
|
17709
|
+
gap: 0;
|
|
17749
17710
|
}
|
|
17750
17711
|
.vela-attribution .vela-attr-symbol {
|
|
17751
17712
|
flex: none;
|
|
@@ -17753,7 +17714,7 @@ var CSS = `
|
|
|
17753
17714
|
line-height: 0;
|
|
17754
17715
|
}
|
|
17755
17716
|
.vela-attribution .vela-attr-symbol svg {
|
|
17756
|
-
height:
|
|
17717
|
+
height: 24px;
|
|
17757
17718
|
width: auto;
|
|
17758
17719
|
display: block;
|
|
17759
17720
|
filter: drop-shadow(0 1px 2px var(--vela-attr-shadow, rgba(0,0,0,0.45)));
|
|
@@ -17764,15 +17725,13 @@ var CSS = `
|
|
|
17764
17725
|
opacity: 0;
|
|
17765
17726
|
display: flex;
|
|
17766
17727
|
align-items: center;
|
|
17767
|
-
/*
|
|
17768
|
-
position: relative;
|
|
17769
|
-
top: 3px;
|
|
17728
|
+
/* Same height and baseline as the V (one viewBox height), so no vertical offset. */
|
|
17770
17729
|
transform: translateX(-8px);
|
|
17771
17730
|
transition: max-width 0.3s ease, opacity 0.25s ease, transform 0.3s ease;
|
|
17772
17731
|
flex: none;
|
|
17773
17732
|
}
|
|
17774
17733
|
.vela-attribution .vela-attr-wordmark svg {
|
|
17775
|
-
height:
|
|
17734
|
+
height: 24px;
|
|
17776
17735
|
width: auto;
|
|
17777
17736
|
display: block;
|
|
17778
17737
|
filter: drop-shadow(0 1px 2px var(--vela-attr-shadow, rgba(0,0,0,0.45)));
|
|
@@ -17787,14 +17746,11 @@ var CSS = `
|
|
|
17787
17746
|
(data-layout): the workspace's single grid-wide mark lives OUTSIDE any renderer
|
|
17788
17747
|
container, so only the shell attribute reaches it. */
|
|
17789
17748
|
[data-vela-layout='mobile'] .vela-attribution .vela-attr-symbol svg,
|
|
17790
|
-
[data-layout='mobile'] .vela-attribution .vela-attr-symbol svg { height:
|
|
17749
|
+
[data-layout='mobile'] .vela-attribution .vela-attr-symbol svg { height: 20px; }
|
|
17791
17750
|
[data-vela-layout='mobile'] .vela-attribution .vela-attr-wordmark,
|
|
17792
|
-
[data-layout='mobile'] .vela-attribution .vela-attr-wordmark {
|
|
17793
|
-
top: 2px;
|
|
17794
|
-
transform: translateX(-6px);
|
|
17795
|
-
}
|
|
17751
|
+
[data-layout='mobile'] .vela-attribution .vela-attr-wordmark { transform: translateX(-6px); }
|
|
17796
17752
|
[data-vela-layout='mobile'] .vela-attribution .vela-attr-wordmark svg,
|
|
17797
|
-
[data-layout='mobile'] .vela-attribution .vela-attr-wordmark svg { height:
|
|
17753
|
+
[data-layout='mobile'] .vela-attribution .vela-attr-wordmark svg { height: 20px; }
|
|
17798
17754
|
[data-vela-layout='mobile'] .vela-attribution:hover .vela-attr-wordmark,
|
|
17799
17755
|
[data-layout='mobile'] .vela-attribution:hover .vela-attr-wordmark { transform: translateX(0); }
|
|
17800
17756
|
`;
|
|
@@ -17851,10 +17807,10 @@ function createAttributionMark(doc, background) {
|
|
|
17851
17807
|
const symbol = doc.createElement("span");
|
|
17852
17808
|
symbol.className = "vela-attr-symbol";
|
|
17853
17809
|
symbol.setAttribute("aria-hidden", "true");
|
|
17854
|
-
symbol.innerHTML =
|
|
17810
|
+
symbol.innerHTML = VELA_MARK_SVG;
|
|
17855
17811
|
const wordmark = doc.createElement("span");
|
|
17856
17812
|
wordmark.className = "vela-attr-wordmark";
|
|
17857
|
-
wordmark.innerHTML =
|
|
17813
|
+
wordmark.innerHTML = VELA_LETTERS_SVG;
|
|
17858
17814
|
a.append(symbol, wordmark);
|
|
17859
17815
|
return a;
|
|
17860
17816
|
}
|
|
@@ -18434,7 +18390,7 @@ var NativeRenderer = class {
|
|
|
18434
18390
|
this.moveIndicatorCbs = /* @__PURE__ */ new Set();
|
|
18435
18391
|
this.priceStyleCbs = /* @__PURE__ */ new Set();
|
|
18436
18392
|
this.name = "native";
|
|
18437
|
-
this.features = ["logScale", "currentPriceLine", "priceLabel", "countdown", "upColor", "downColor", "glow", "animZoom", "animPan", "animScroll", "animAutoscale", "animLiveBar", "intro", "zoomAnchor", "axisDrag", "paneResize", "candleZOrder", "candleVisible", "seriesOrder", "highlights", "sessionZones", "gridlines", "axisLabels", "scaleMode", "invertScale", "paneScales", "autoScale", "timezone", "keyboard", "historyChords", "priceStyle", "priceBaseline", "baselinePrice", "settings", "attribution", "dialogHost", "tradeMarkers", "marks", "indicatorTitles", "indicatorValues"];
|
|
18393
|
+
this.features = ["logScale", "currentPriceLine", "priceLabel", "countdown", "upColor", "downColor", "glow", "animZoom", "animPan", "animScroll", "animAutoscale", "animLiveBar", "intro", "zoomAnchor", "axisDrag", "paneResize", "candleZOrder", "candleVisible", "seriesOrder", "highlights", "sessionZones", "gridlines", "axisLabels", "scaleMode", "invertScale", "paneScales", "autoScale", "timezone", "keyboard", "historyChords", "priceStyle", "priceBaseline", "baselinePrice", "settings", "attribution", "dialogHost", "tradeMarkers", "marks", "indicatorTitles", "indicatorValues", "crosshairOverride"];
|
|
18438
18394
|
/** Track cursor proximity to the scroll button on the plot (bubbles from the button too,
|
|
18439
18395
|
* so moving onto the button doesn't count as leaving). */
|
|
18440
18396
|
this.onScrollProximityMove = (e) => {
|
|
@@ -18613,6 +18569,9 @@ var NativeRenderer = class {
|
|
|
18613
18569
|
this.scene.marks = mergeMarksState(this.scene.marks, value);
|
|
18614
18570
|
this.markPopover?.close();
|
|
18615
18571
|
break;
|
|
18572
|
+
case "crosshairOverride":
|
|
18573
|
+
this.scene.crosshairOverride = sanitizeCrosshairOverride(value);
|
|
18574
|
+
break;
|
|
18616
18575
|
case "keyboard":
|
|
18617
18576
|
this.setKeyboardEnabled(Boolean(value));
|
|
18618
18577
|
return;
|
|
@@ -18726,6 +18685,8 @@ var NativeRenderer = class {
|
|
|
18726
18685
|
return { ...this.scene.tradeMarkers, colors: { ...this.scene.tradeMarkers.colors } };
|
|
18727
18686
|
case "marks":
|
|
18728
18687
|
return { visible: this.scene.marks.visible, groups: { ...this.scene.marks.groups } };
|
|
18688
|
+
case "crosshairOverride":
|
|
18689
|
+
return this.scene.crosshairOverride ? { ...this.scene.crosshairOverride } : null;
|
|
18729
18690
|
case "keyboard":
|
|
18730
18691
|
return this.keyboardEnabled;
|
|
18731
18692
|
case "historyChords":
|
|
@@ -19304,8 +19265,8 @@ var NativeRenderer = class {
|
|
|
19304
19265
|
for (const pane of this.scene.panes.values()) pane.scale = { ...pane.scaleTarget };
|
|
19305
19266
|
this.modelAlpha = 0;
|
|
19306
19267
|
const start = performance.now();
|
|
19307
|
-
const step = (
|
|
19308
|
-
const p = Math.min(1, (
|
|
19268
|
+
const step = (now2) => {
|
|
19269
|
+
const p = Math.min(1, (now2 - start) / duration);
|
|
19309
19270
|
this.scene.bars = p >= 1 ? real : real.map((b, i) => this.revealCandle(b, i, p, n, style));
|
|
19310
19271
|
this.paintData();
|
|
19311
19272
|
if (p < 1) {
|
|
@@ -19321,8 +19282,8 @@ var NativeRenderer = class {
|
|
|
19321
19282
|
fadeInModels() {
|
|
19322
19283
|
const fade = Math.min(INTRO_MODEL_FADE_MS, this.intro.duration || INTRO_MODEL_FADE_MS);
|
|
19323
19284
|
const start = performance.now();
|
|
19324
|
-
const step = (
|
|
19325
|
-
this.modelAlpha = Math.min(1, (
|
|
19285
|
+
const step = (now2) => {
|
|
19286
|
+
this.modelAlpha = Math.min(1, (now2 - start) / fade);
|
|
19326
19287
|
this.paintData();
|
|
19327
19288
|
if (this.modelAlpha < 1) {
|
|
19328
19289
|
this.introRaf = requestAnimationFrame(step);
|
|
@@ -19845,10 +19806,13 @@ var NativeRenderer = class {
|
|
|
19845
19806
|
this.animator.start();
|
|
19846
19807
|
}
|
|
19847
19808
|
} else if (!last || bar.time > last.time) {
|
|
19809
|
+
const vp = this.coords.getViewport();
|
|
19810
|
+
const holdView = n > 0 && vp.rightOffset < 0 && this.scrollTargetRO === null;
|
|
19848
19811
|
this.bars.push(bar);
|
|
19849
19812
|
this.syncLiveEase(bar);
|
|
19850
19813
|
if (this.coords.barInterval > 0) this.coords.appendBar(bar.time);
|
|
19851
19814
|
else this.coords.setBars(this.bars.map((b) => b.time));
|
|
19815
|
+
if (holdView) this.coords.setViewport({ ...vp, rightOffset: vp.rightOffset - 1 });
|
|
19852
19816
|
} else {
|
|
19853
19817
|
return;
|
|
19854
19818
|
}
|
|
@@ -20530,10 +20494,10 @@ var NativeRenderer = class {
|
|
|
20530
20494
|
if (this.markPulseRaf !== null || typeof requestAnimationFrame !== "function") return;
|
|
20531
20495
|
const tick = () => {
|
|
20532
20496
|
this.markPulseRaf = null;
|
|
20533
|
-
const
|
|
20534
|
-
if (this.scene.marksFlash && this.scene.marksFlash.until <=
|
|
20497
|
+
const now2 = frameNow();
|
|
20498
|
+
if (this.scene.marksFlash && this.scene.marksFlash.until <= now2) this.scene.marksFlash = null;
|
|
20535
20499
|
this.scheduler?.invalidate(2 /* Chrome */);
|
|
20536
|
-
const pulsing = this.scene.marksHoverKey !== null &&
|
|
20500
|
+
const pulsing = this.scene.marksHoverKey !== null && now2 - this.scene.marksHoverSince < MARK_PULSE_MS;
|
|
20537
20501
|
if (pulsing || this.scene.marksFlash !== null) this.markPulseRaf = requestAnimationFrame(tick);
|
|
20538
20502
|
};
|
|
20539
20503
|
this.markPulseRaf = requestAnimationFrame(tick);
|
|
@@ -21094,9 +21058,10 @@ var NativeRenderer = class {
|
|
|
21094
21058
|
const ext = this.externalCross;
|
|
21095
21059
|
if (!ext || this.coords.barCount === 0) return null;
|
|
21096
21060
|
const logical = Math.floor(this.coords.timeToLogical(ext.time));
|
|
21097
|
-
if (logical <
|
|
21098
|
-
const x = this.coords.logicalToX(logical);
|
|
21099
|
-
if (!Number.isFinite(x)
|
|
21061
|
+
if (!(logical < this.coords.barCount)) return null;
|
|
21062
|
+
const x = this.coords.logicalToX(Math.max(logical, -1));
|
|
21063
|
+
if (!Number.isFinite(x)) return null;
|
|
21064
|
+
const line = logical >= 0 && x >= 0 && x <= this.coords.width;
|
|
21100
21065
|
let y = null;
|
|
21101
21066
|
if (ext.price != null) {
|
|
21102
21067
|
const pricePane = this.scene.orderedPanes().find((p) => p.kind === "price");
|
|
@@ -21105,7 +21070,7 @@ var NativeRenderer = class {
|
|
|
21105
21070
|
if (!Number.isFinite(y) || y < pricePane.bounds.top || y > pricePane.bounds.top + pricePane.bounds.height) y = null;
|
|
21106
21071
|
}
|
|
21107
21072
|
}
|
|
21108
|
-
return { x, y, time: this.coords.logicalToTime(logical), price: y != null ? ext.price : null };
|
|
21073
|
+
return { x, y, time: this.coords.logicalToTime(Math.max(logical, 0)), price: y != null ? ext.price : null, line };
|
|
21109
21074
|
}
|
|
21110
21075
|
/** Set the sticky magnet mode (called by the drawings toolbar's 3-state button). */
|
|
21111
21076
|
setSnapMode(mode) {
|
|
@@ -22329,6 +22294,7 @@ var CHUNK_BARS = 1e4;
|
|
|
22329
22294
|
var FIRST_PAINT_BARS = 20;
|
|
22330
22295
|
var GAP_FACTOR = 1.5;
|
|
22331
22296
|
var HEAL_COOLDOWN_MS = 5e3;
|
|
22297
|
+
var MIN_TICK_UPDATE_MS = 16;
|
|
22332
22298
|
var EngineOrchestrator = class _EngineOrchestrator {
|
|
22333
22299
|
constructor(container, renderer, feed, engines, config, dataControl) {
|
|
22334
22300
|
this.renderer = renderer;
|
|
@@ -22381,6 +22347,32 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
22381
22347
|
this.lastHealAt = 0;
|
|
22382
22348
|
/** The `visibilitychange` catch-up listener (removed on destroy). */
|
|
22383
22349
|
this.onVisible = null;
|
|
22350
|
+
// ── bar replay: `rawBars` holds the history up to the cursor, the rest waits here ──
|
|
22351
|
+
/** Bars still hidden right of the replay cursor, oldest first; null ≡ replay off. */
|
|
22352
|
+
this.replayQueue = null;
|
|
22353
|
+
this.replayTimer = null;
|
|
22354
|
+
this.replayPlaying = false;
|
|
22355
|
+
this.replayIntervalMs = 1e3;
|
|
22356
|
+
/** The depth (`market.bars`) before a replay deepened the history — restored when it ends. */
|
|
22357
|
+
this.replayDepth = null;
|
|
22358
|
+
/** A start is loading older history before it can cut; `seek` = a replay was running
|
|
22359
|
+
* (and left silently to deepen), so abandoning the load must announce its end. */
|
|
22360
|
+
this.replayLoading = null;
|
|
22361
|
+
/** A replay carried over a timeframe/session switch, between the old series' silent end
|
|
22362
|
+
* and the cut of the new one: still ACTIVE for the outside (`replayState`), resumed
|
|
22363
|
+
* at `revealedEnd` once the new bars land, unless stopped (`cancelled`) meanwhile. */
|
|
22364
|
+
this.replayCarrying = null;
|
|
22365
|
+
/** Intrabar prices for the bars replay reveals — null ≡ whole bars. */
|
|
22366
|
+
this.replayTickSource = null;
|
|
22367
|
+
/** The bar being revealed tick by tick: its stored form (what it settles on), its ticks,
|
|
22368
|
+
* the next tick to apply, the candle built so far, the volume each tick adds when the
|
|
22369
|
+
* ticks carry none, and its pacing (ticks per update, ms between updates — one play
|
|
22370
|
+
* interval per tick — and the clock time tick 0 is due at, so late timers catch up). */
|
|
22371
|
+
this.replayForming = null;
|
|
22372
|
+
/** Tick requests by bar time, shared by prefetch and playback; `ticks` once they landed. */
|
|
22373
|
+
this.replayTickFetches = /* @__PURE__ */ new Map();
|
|
22374
|
+
/** Aborts the tick requests in flight — renewed whenever the replay moves on. */
|
|
22375
|
+
this.replayTickAbort = null;
|
|
22384
22376
|
/** What the last applied bar did: refine the open candle, or open a new one (which
|
|
22385
22377
|
* settles the previous). Read by the `script:run` cause attribution. */
|
|
22386
22378
|
this.barCause = "tick";
|
|
@@ -22604,6 +22596,10 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
22604
22596
|
const paint = (bars, final) => {
|
|
22605
22597
|
if (this.generation !== gen || !final && bars.length === 0) return;
|
|
22606
22598
|
if (!painted && !final && bars.length < Math.min(requested, FIRST_PAINT_BARS)) return;
|
|
22599
|
+
if (painted && this.replayQueue) {
|
|
22600
|
+
this.mergeHistoryIntoReplay(bars);
|
|
22601
|
+
return;
|
|
22602
|
+
}
|
|
22607
22603
|
this.setBarSeries(bars, painted ? { preserveView: true } : void 0);
|
|
22608
22604
|
if (!painted && bars.length > 0) {
|
|
22609
22605
|
painted = true;
|
|
@@ -22746,6 +22742,36 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
22746
22742
|
// exactly like a timeframe change; the cache keys the sessions apart.
|
|
22747
22743
|
next.session !== void 0 && next.session !== m.session || next.data !== void 0;
|
|
22748
22744
|
const depthChanged = next.bars !== void 0 && next.bars !== m.bars;
|
|
22745
|
+
const sameClock = identityChanged && next.data === void 0 && (next.symbol === void 0 || next.symbol === m.symbol) && (next.timeframe !== void 0 || next.session !== void 0);
|
|
22746
|
+
const pending = identityChanged ? this.replayCarrying : null;
|
|
22747
|
+
if (pending) {
|
|
22748
|
+
this.replayCarrying = null;
|
|
22749
|
+
if (!sameClock) {
|
|
22750
|
+
pending.cancelled = true;
|
|
22751
|
+
this.events.emit("replay:end", { reason: "market" });
|
|
22752
|
+
}
|
|
22753
|
+
}
|
|
22754
|
+
const replayCarry = this.replayQueue && sameClock && this.rawBars.length > 0 ? { revealedEnd: this.rawBars[this.rawBars.length - 1].time + this.barIntervalMs(), playing: this.replayPlaying, cancelled: false } : pending && sameClock ? { revealedEnd: pending.revealedEnd, playing: pending.playing, cancelled: false } : null;
|
|
22755
|
+
if (pending) pending.cancelled = true;
|
|
22756
|
+
if (replayCarry) this.replayCarrying = replayCarry;
|
|
22757
|
+
if (replayCarry && this.replayQueue) {
|
|
22758
|
+
this.endReplay(null, { restore: false, resume: false });
|
|
22759
|
+
const depth = this.replayDepth;
|
|
22760
|
+
this.replayDepth = null;
|
|
22761
|
+
if (depth && next.bars === void 0) {
|
|
22762
|
+
if (depth.bars === void 0) delete m.bars;
|
|
22763
|
+
else m.bars = depth.bars;
|
|
22764
|
+
}
|
|
22765
|
+
}
|
|
22766
|
+
if (this.replayQueue && (identityChanged || depthChanged)) this.endReplay("market", { restore: !identityChanged, resume: false });
|
|
22767
|
+
if (this.replayLoading && (identityChanged || depthChanged)) {
|
|
22768
|
+
const depth = this.replayDepth;
|
|
22769
|
+
this.abandonReplayLoad("market");
|
|
22770
|
+
if (depth && next.bars === void 0) {
|
|
22771
|
+
if (depth.bars === void 0) delete m.bars;
|
|
22772
|
+
else m.bars = depth.bars;
|
|
22773
|
+
}
|
|
22774
|
+
}
|
|
22749
22775
|
const depthOnly = depthChanged && !identityChanged && this.rawBars.length > 0 && !m.data?.length && ((next.bars ?? 0) <= this.rawBars.length || typeof this.feed.loadRange === "function");
|
|
22750
22776
|
if (!identityChanged && !depthChanged) {
|
|
22751
22777
|
if (typeof next.visibleRange === "string") this.setVisibleRangePreset(next.visibleRange);
|
|
@@ -22803,15 +22829,20 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
22803
22829
|
const from = bars[Math.max(0, idx - (carriedCount - 1))].time;
|
|
22804
22830
|
this.renderer.setVisibleRange({ from, to: Math.max(carried.right, bars[idx].time + 1) });
|
|
22805
22831
|
}
|
|
22806
|
-
|
|
22832
|
+
const carry = replayCarry && !replayCarry.cancelled ? replayCarry : null;
|
|
22833
|
+
const carryFrom = carry ? carry.revealedEnd - this.barIntervalMs() : 0;
|
|
22834
|
+
const carryNow = carry != null && this.rawBars.length > 0 && carryFrom >= this.rawBars[0].time;
|
|
22835
|
+
const replayBack = carryNow && this.resumeCarriedReplay(this.replayStart({ from: carryFrom }), carry);
|
|
22836
|
+
if (!depthOnly && !replayBack) {
|
|
22807
22837
|
this.restartNativeIndicators();
|
|
22808
22838
|
this.restartChartTypeEngine();
|
|
22809
22839
|
this.reexecuteIndicators();
|
|
22810
22840
|
}
|
|
22811
|
-
this.startLive();
|
|
22841
|
+
if (!replayBack) this.startLive();
|
|
22812
22842
|
if (identityChanged) {
|
|
22813
22843
|
this.events.emit("market:changed", { symbol: m.symbol ?? "TEST", timeframe: m.timeframe ?? "60", prev: prev2 });
|
|
22814
22844
|
}
|
|
22845
|
+
if (carry && !carryNow) this.resumeCarriedReplay(this.replayStart({ from: carryFrom }), carry);
|
|
22815
22846
|
}
|
|
22816
22847
|
/**
|
|
22817
22848
|
* Restart every native indicator over the new market: a `NativeIndicatorContext`
|
|
@@ -22935,6 +22966,397 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
22935
22966
|
historyComplete() {
|
|
22936
22967
|
return this.historyCompletePromise;
|
|
22937
22968
|
}
|
|
22969
|
+
// ── bar replay ──────────────────────────────────────────────
|
|
22970
|
+
/**
|
|
22971
|
+
* Enter replay — or seek, when already replaying — keeping every bar opened at or before
|
|
22972
|
+
* `opts.from` and queueing the rest. A `from` inside the loaded bars cuts at once, even
|
|
22973
|
+
* while older history is still streaming in: what lands later joins the tape's head
|
|
22974
|
+
* (see {@link mergeHistoryIntoReplay}) and leaves the cut alone. An older `from` waits
|
|
22975
|
+
* for that load, then deepens if it still has to. Consumers restart over the cut exactly
|
|
22976
|
+
* as on a market switch — their models describe bars that are now hidden — and the
|
|
22977
|
+
* active chart-type engine is rebuilt without live data.
|
|
22978
|
+
*/
|
|
22979
|
+
async replayStart(opts) {
|
|
22980
|
+
if (this.rawBars.length === 0) {
|
|
22981
|
+
console.warn("[vela] chart.replay.start() ignored \u2014 the chart has no bars yet");
|
|
22982
|
+
return;
|
|
22983
|
+
}
|
|
22984
|
+
let gen = this.generation;
|
|
22985
|
+
if (opts.from < this.rawBars[0].time) {
|
|
22986
|
+
await this.historyCompletePromise;
|
|
22987
|
+
if (this.generation !== gen || this.rawBars.length === 0) return;
|
|
22988
|
+
}
|
|
22989
|
+
const oldest = this.rawBars[0].time;
|
|
22990
|
+
if (opts.from < oldest && this.canHeal() && typeof this.feed.loadRange === "function") {
|
|
22991
|
+
const held = this.rawBars.length + (this.replayQueue?.length ?? 0);
|
|
22992
|
+
const bars = held + Math.ceil((oldest - opts.from) / this.barIntervalMs()) + 1;
|
|
22993
|
+
const seek = this.replayQueue !== null;
|
|
22994
|
+
if (seek) this.endReplay(null, { restore: true, resume: false });
|
|
22995
|
+
this.replayDepth ?? (this.replayDepth = { bars: this.config.market.bars });
|
|
22996
|
+
const deepened = this.setMarket({ bars });
|
|
22997
|
+
this.replayLoading = { seek };
|
|
22998
|
+
await deepened;
|
|
22999
|
+
gen = this.generation;
|
|
23000
|
+
await this.historyCompletePromise;
|
|
23001
|
+
if (!this.replayLoading || this.generation !== gen || this.rawBars.length === 0) return;
|
|
23002
|
+
this.replayLoading = null;
|
|
23003
|
+
}
|
|
23004
|
+
this.settleFormingBar();
|
|
23005
|
+
this.dropTickFetches();
|
|
23006
|
+
const tape = this.replayQueue ? [...this.rawBars, ...this.replayQueue] : this.rawBars;
|
|
23007
|
+
let cut = 0;
|
|
23008
|
+
while (cut < tape.length && tape[cut].time <= opts.from) cut += 1;
|
|
23009
|
+
cut = Math.max(1, cut);
|
|
23010
|
+
if (cut >= tape.length) {
|
|
23011
|
+
console.warn("[vela] chart.replay.start() ignored \u2014 no bar opens after the requested time");
|
|
23012
|
+
return;
|
|
23013
|
+
}
|
|
23014
|
+
const wasPlaying = this.replayPlaying;
|
|
23015
|
+
this.clearReplayTimer();
|
|
23016
|
+
if (!this.replayQueue) this.stopLive();
|
|
23017
|
+
this.replayQueue = tape.slice(cut);
|
|
23018
|
+
this.setBarSeries(tape.slice(0, cut));
|
|
23019
|
+
this.restartBarConsumers({ blank: true });
|
|
23020
|
+
if (wasPlaying) this.events.emit("replay:pause", void 0);
|
|
23021
|
+
this.events.emit("replay:start", { cursorTime: tape[cut - 1].time, remaining: this.replayQueue.length });
|
|
23022
|
+
this.prefetchTicks();
|
|
23023
|
+
}
|
|
23024
|
+
/** Reveal the next queued bar through the live-bar path — or complete the bar forming
|
|
23025
|
+
* tick by tick; the last one ends the replay. */
|
|
23026
|
+
replayStep() {
|
|
23027
|
+
if (this.replayForming) {
|
|
23028
|
+
this.finishFormingBar();
|
|
23029
|
+
return true;
|
|
23030
|
+
}
|
|
23031
|
+
const queue = this.replayQueue;
|
|
23032
|
+
const bar = queue?.shift();
|
|
23033
|
+
if (!queue || !bar) return false;
|
|
23034
|
+
this.replayTickFetches.delete(bar.time);
|
|
23035
|
+
this.applyBar(bar);
|
|
23036
|
+
this.completeReveal(bar);
|
|
23037
|
+
this.prefetchTicks();
|
|
23038
|
+
return true;
|
|
23039
|
+
}
|
|
23040
|
+
/**
|
|
23041
|
+
* Follow up a replay carried onto a new series of the same symbol (another timeframe or
|
|
23042
|
+
* session). The cut asked for keeps the bars that CLOSED by the end of what was revealed —
|
|
23043
|
+
* every finer bar of the last revealed one, never a coarser bar still open at that point
|
|
23044
|
+
* (no look-ahead). Once it settles: playback resumes if it was on; if the replay did not
|
|
23045
|
+
* come back (nothing left after that point, or a later switch took over), it ends as a
|
|
23046
|
+
* market switch would. Returns whether the replay is back already — a start with no
|
|
23047
|
+
* history to wait for cuts synchronously (an async function runs up to its first await).
|
|
23048
|
+
*/
|
|
23049
|
+
resumeCarriedReplay(start, carry) {
|
|
23050
|
+
if (this.replayQueue && this.replayCarrying === carry) this.replayCarrying = null;
|
|
23051
|
+
void start.then(() => {
|
|
23052
|
+
if (this.replayCarrying === carry) this.replayCarrying = null;
|
|
23053
|
+
if (carry.cancelled) return;
|
|
23054
|
+
if (!this.replayQueue) this.events.emit("replay:end", { reason: "market" });
|
|
23055
|
+
else if (carry.playing) this.replayPlay();
|
|
23056
|
+
});
|
|
23057
|
+
return this.replayQueue !== null;
|
|
23058
|
+
}
|
|
23059
|
+
replayStepUpdate() {
|
|
23060
|
+
if (!this.replayQueue) return false;
|
|
23061
|
+
if (this.replayForming) {
|
|
23062
|
+
this.applyReplayTicks(1);
|
|
23063
|
+
return true;
|
|
23064
|
+
}
|
|
23065
|
+
const bar = this.replayQueue[0];
|
|
23066
|
+
if (!bar) return false;
|
|
23067
|
+
const entry = this.replayTickSource ? this.requestTicks(bar) : null;
|
|
23068
|
+
if (entry && !entry.ticks) {
|
|
23069
|
+
const epoch = this.replayTickAbort;
|
|
23070
|
+
void entry.done.then(() => {
|
|
23071
|
+
if (!this.replayPlaying && !this.replayForming && this.replayTickAbort === epoch && this.replayQueue?.[0] === bar) this.replayStepUpdate();
|
|
23072
|
+
});
|
|
23073
|
+
return true;
|
|
23074
|
+
}
|
|
23075
|
+
if (!entry?.ticks?.length) return this.replayStep();
|
|
23076
|
+
this.openFormingBar(entry.ticks, 1);
|
|
23077
|
+
return true;
|
|
23078
|
+
}
|
|
23079
|
+
replaySetTicks(source) {
|
|
23080
|
+
this.finishFormingBar();
|
|
23081
|
+
this.dropTickFetches();
|
|
23082
|
+
this.replayTickSource = source;
|
|
23083
|
+
this.prefetchTicks();
|
|
23084
|
+
}
|
|
23085
|
+
replayPlay(intervalMs2) {
|
|
23086
|
+
if (!this.replayQueue) return;
|
|
23087
|
+
if (intervalMs2 !== void 0) {
|
|
23088
|
+
if (!(Number.isFinite(intervalMs2) && intervalMs2 > 0)) {
|
|
23089
|
+
console.warn(`[vela] chart.replay.play(${intervalMs2}) ignored \u2014 the interval must be a positive number of ms`);
|
|
23090
|
+
return;
|
|
23091
|
+
}
|
|
23092
|
+
this.replayIntervalMs = intervalMs2;
|
|
23093
|
+
}
|
|
23094
|
+
this.clearReplayTimer();
|
|
23095
|
+
this.replayPlaying = true;
|
|
23096
|
+
if (this.replayForming) this.paceFormingBar(this.replayForming);
|
|
23097
|
+
this.scheduleReplayStep(this.replayForming?.delayMs);
|
|
23098
|
+
this.events.emit("replay:play", { intervalMs: this.replayIntervalMs });
|
|
23099
|
+
}
|
|
23100
|
+
replayPause() {
|
|
23101
|
+
if (!this.replayPlaying) return;
|
|
23102
|
+
this.clearReplayTimer();
|
|
23103
|
+
this.events.emit("replay:pause", void 0);
|
|
23104
|
+
}
|
|
23105
|
+
replayStop() {
|
|
23106
|
+
const carrying = this.replayCarrying;
|
|
23107
|
+
if (carrying) {
|
|
23108
|
+
carrying.cancelled = true;
|
|
23109
|
+
this.replayCarrying = null;
|
|
23110
|
+
this.events.emit("replay:end", { reason: "stopped" });
|
|
23111
|
+
}
|
|
23112
|
+
if (this.replayLoading) {
|
|
23113
|
+
const depth = this.replayDepth;
|
|
23114
|
+
this.abandonReplayLoad("stopped");
|
|
23115
|
+
void this.setMarket({ bars: depth?.bars ?? 500 });
|
|
23116
|
+
return;
|
|
23117
|
+
}
|
|
23118
|
+
this.endReplay("stopped", { restore: true, resume: true });
|
|
23119
|
+
}
|
|
23120
|
+
abandonReplayLoad(reason) {
|
|
23121
|
+
const loading = this.replayLoading;
|
|
23122
|
+
if (!loading) return;
|
|
23123
|
+
this.replayLoading = null;
|
|
23124
|
+
this.replayDepth = null;
|
|
23125
|
+
if (loading.seek) this.events.emit("replay:end", { reason });
|
|
23126
|
+
}
|
|
23127
|
+
replayState() {
|
|
23128
|
+
const active = this.replayQueue !== null;
|
|
23129
|
+
return {
|
|
23130
|
+
active: active || this.replayCarrying !== null,
|
|
23131
|
+
// a replay carried over a switch stays on while the new bars load
|
|
23132
|
+
playing: this.replayCarrying?.playing ?? this.replayPlaying,
|
|
23133
|
+
cursorTime: active ? this.rawBars[this.rawBars.length - 1]?.time ?? null : null,
|
|
23134
|
+
remaining: this.replayQueue?.length ?? 0,
|
|
23135
|
+
nextTime: this.replayForming?.bar.time ?? this.replayQueue?.[0]?.time ?? null,
|
|
23136
|
+
intervalMs: this.replayIntervalMs
|
|
23137
|
+
};
|
|
23138
|
+
}
|
|
23139
|
+
replayBounds() {
|
|
23140
|
+
const first = this.rawBars[0];
|
|
23141
|
+
const last = this.replayQueue?.[this.replayQueue.length - 1] ?? this.rawBars[this.rawBars.length - 1];
|
|
23142
|
+
return first && last ? { first: first.time, last: last.time } : null;
|
|
23143
|
+
}
|
|
23144
|
+
scheduleReplayStep(delayMs = this.replayIntervalMs) {
|
|
23145
|
+
this.replayTimer = setTimeout(() => {
|
|
23146
|
+
this.replayTimer = null;
|
|
23147
|
+
this.replayAdvance();
|
|
23148
|
+
}, delayMs);
|
|
23149
|
+
}
|
|
23150
|
+
/** One beat of timed playback: the forming bar's next ticks, else the next bar — whole,
|
|
23151
|
+
* or opened from its ticks (waiting for them when they have not landed yet). */
|
|
23152
|
+
replayAdvance() {
|
|
23153
|
+
if (!this.replayPlaying || !this.replayQueue) return;
|
|
23154
|
+
const forming = this.replayForming;
|
|
23155
|
+
if (forming) {
|
|
23156
|
+
const due = Math.floor((now() - forming.origin) / this.replayIntervalMs) + 1;
|
|
23157
|
+
this.applyReplayTicks(Math.max(forming.batch, due - forming.next));
|
|
23158
|
+
if (this.replayPlaying) this.scheduleReplayStep(forming.delayMs);
|
|
23159
|
+
return;
|
|
23160
|
+
}
|
|
23161
|
+
const bar = this.replayQueue[0];
|
|
23162
|
+
if (!bar) return;
|
|
23163
|
+
const entry = this.replayTickSource ? this.requestTicks(bar) : null;
|
|
23164
|
+
if (entry && !entry.ticks) {
|
|
23165
|
+
const epoch = this.replayTickAbort;
|
|
23166
|
+
void entry.done.then(() => {
|
|
23167
|
+
if (this.replayPlaying && this.replayTimer == null && this.replayTickAbort === epoch && this.replayQueue?.[0] === bar) this.replayAdvance();
|
|
23168
|
+
});
|
|
23169
|
+
return;
|
|
23170
|
+
}
|
|
23171
|
+
if (!entry?.ticks?.length) {
|
|
23172
|
+
if (this.replayStep() && this.replayPlaying) this.scheduleReplayStep();
|
|
23173
|
+
return;
|
|
23174
|
+
}
|
|
23175
|
+
this.openFormingBar(entry.ticks);
|
|
23176
|
+
if (this.replayPlaying) this.scheduleReplayStep(this.replayForming?.delayMs);
|
|
23177
|
+
}
|
|
23178
|
+
/** Take the next queued bar off the tape and open it at its first tick (the first
|
|
23179
|
+
* `count` ticks — default: one timed update's worth). */
|
|
23180
|
+
openFormingBar(ticks, count) {
|
|
23181
|
+
const bar = this.replayQueue.shift();
|
|
23182
|
+
const volumeEach = ticks.every((t) => t.volume === void 0) ? (bar.volume ?? 0) / ticks.length : null;
|
|
23183
|
+
const forming = { bar, ticks, next: 0, candle: null, volumeEach, batch: 1, delayMs: this.replayIntervalMs, origin: 0 };
|
|
23184
|
+
this.paceFormingBar(forming);
|
|
23185
|
+
this.replayForming = forming;
|
|
23186
|
+
this.applyReplayTicks(count ?? forming.batch);
|
|
23187
|
+
this.prefetchTicks();
|
|
23188
|
+
}
|
|
23189
|
+
/** One tick per interval, batched when the interval is shorter than a frame; the clock
|
|
23190
|
+
* is anchored on the ticks already applied (a resume or a pace change carries on from
|
|
23191
|
+
* there, never jumps). */
|
|
23192
|
+
paceFormingBar(f) {
|
|
23193
|
+
f.batch = Math.max(1, Math.ceil(MIN_TICK_UPDATE_MS / this.replayIntervalMs));
|
|
23194
|
+
f.delayMs = this.replayIntervalMs * f.batch;
|
|
23195
|
+
f.origin = now() - Math.max(0, f.next - 1) * this.replayIntervalMs;
|
|
23196
|
+
}
|
|
23197
|
+
/** Fold the next `count` ticks into the forming candle; the last one settles the bar. */
|
|
23198
|
+
applyReplayTicks(count) {
|
|
23199
|
+
const f = this.replayForming;
|
|
23200
|
+
if (!f) return;
|
|
23201
|
+
let c = f.candle;
|
|
23202
|
+
const stop = Math.min(f.ticks.length, f.next + count);
|
|
23203
|
+
for (; f.next < stop; f.next += 1) {
|
|
23204
|
+
const t = f.ticks[f.next];
|
|
23205
|
+
const v = f.volumeEach ?? t.volume ?? 0;
|
|
23206
|
+
const hi = Math.max(t.high ?? t.price, t.price);
|
|
23207
|
+
const lo = Math.min(t.low ?? t.price, t.price);
|
|
23208
|
+
if (c) {
|
|
23209
|
+
c = { ...c, high: Math.max(c.high, hi), low: Math.min(c.low, lo), close: t.price, volume: (c.volume ?? 0) + v };
|
|
23210
|
+
} else {
|
|
23211
|
+
const open = t.open ?? t.price;
|
|
23212
|
+
c = { time: f.bar.time, open, high: Math.max(open, hi), low: Math.min(open, lo), close: t.price, volume: v };
|
|
23213
|
+
}
|
|
23214
|
+
}
|
|
23215
|
+
f.candle = c;
|
|
23216
|
+
if (f.next >= f.ticks.length) {
|
|
23217
|
+
this.finishFormingBar();
|
|
23218
|
+
return;
|
|
23219
|
+
}
|
|
23220
|
+
this.applyBar(c);
|
|
23221
|
+
this.events.emit("replay:tick", { cursorTime: f.bar.time, index: f.next - 1, count: f.ticks.length });
|
|
23222
|
+
}
|
|
23223
|
+
/** Settle the forming bar on its stored values and announce it revealed. */
|
|
23224
|
+
finishFormingBar() {
|
|
23225
|
+
const f = this.replayForming;
|
|
23226
|
+
if (!f) return;
|
|
23227
|
+
this.replayForming = null;
|
|
23228
|
+
this.replayTickFetches.delete(f.bar.time);
|
|
23229
|
+
this.applyBar(f.bar);
|
|
23230
|
+
this.events.emit("replay:tick", { cursorTime: f.bar.time, index: f.ticks.length - 1, count: f.ticks.length });
|
|
23231
|
+
this.completeReveal(f.bar);
|
|
23232
|
+
}
|
|
23233
|
+
/** Put the forming bar back in its stored form, silently — a seek or an end rebuilds
|
|
23234
|
+
* from the tape, which must hold the stored bar. */
|
|
23235
|
+
settleFormingBar() {
|
|
23236
|
+
const f = this.replayForming;
|
|
23237
|
+
if (!f) return;
|
|
23238
|
+
this.replayForming = null;
|
|
23239
|
+
this.applyBar(f.bar);
|
|
23240
|
+
}
|
|
23241
|
+
/**
|
|
23242
|
+
* A history answer landing mid-replay (a progressive snapshot — the whole answer so far):
|
|
23243
|
+
* the tape keeps its cut and its hidden queue; bars older than the tape join its head,
|
|
23244
|
+
* bars newer than it join the queue's end.
|
|
23245
|
+
*/
|
|
23246
|
+
mergeHistoryIntoReplay(bars) {
|
|
23247
|
+
const queue = this.replayQueue;
|
|
23248
|
+
const head = this.rawBars[0];
|
|
23249
|
+
if (!queue || !head) return;
|
|
23250
|
+
const tail = queue[queue.length - 1] ?? this.rawBars[this.rawBars.length - 1];
|
|
23251
|
+
let older = 0;
|
|
23252
|
+
while (older < bars.length && bars[older].time < head.time) older += 1;
|
|
23253
|
+
let newer = bars.length;
|
|
23254
|
+
while (newer > older && bars[newer - 1].time > tail.time) newer -= 1;
|
|
23255
|
+
if (newer < bars.length) queue.push(...bars.slice(newer));
|
|
23256
|
+
if (older > 0) {
|
|
23257
|
+
this.setBarSeries([...bars.slice(0, older), ...this.rawBars], { preserveView: true });
|
|
23258
|
+
this.notifySessionsBars("backfill");
|
|
23259
|
+
}
|
|
23260
|
+
}
|
|
23261
|
+
/** A bar is fully on screen: announce it, and end the replay if it was the last. */
|
|
23262
|
+
completeReveal(bar) {
|
|
23263
|
+
const queue = this.replayQueue;
|
|
23264
|
+
if (!queue) return;
|
|
23265
|
+
this.events.emit("replay:step", { cursorTime: bar.time, remaining: queue.length });
|
|
23266
|
+
if (queue.length === 0) this.endReplay("finished", { restore: true, resume: true });
|
|
23267
|
+
}
|
|
23268
|
+
/** Ask for the ticks of the next hidden bar ahead of time. */
|
|
23269
|
+
prefetchTicks() {
|
|
23270
|
+
const next = this.replayQueue?.[0];
|
|
23271
|
+
if (next && this.replayTickSource) this.requestTicks(next);
|
|
23272
|
+
}
|
|
23273
|
+
/** The (shared) tick request of a queued bar — the source runs once per bar. */
|
|
23274
|
+
requestTicks(bar) {
|
|
23275
|
+
const known = this.replayTickFetches.get(bar.time);
|
|
23276
|
+
if (known) return known;
|
|
23277
|
+
const source = this.replayTickSource;
|
|
23278
|
+
this.replayTickAbort ?? (this.replayTickAbort = new AbortController());
|
|
23279
|
+
const signal = this.replayTickAbort.signal;
|
|
23280
|
+
const queue = this.replayQueue ?? [];
|
|
23281
|
+
const end = queue[queue.indexOf(bar) + 1]?.time ?? bar.time + this.barIntervalMs();
|
|
23282
|
+
const entry = { ticks: null, done: Promise.resolve() };
|
|
23283
|
+
const land = (ticks) => {
|
|
23284
|
+
entry.ticks = usableTicks(ticks);
|
|
23285
|
+
};
|
|
23286
|
+
const fail = (e) => {
|
|
23287
|
+
if (!signal.aborted) console.warn(`[vela] replay tick source failed \u2014 revealing the bar whole (${e instanceof Error ? e.message : String(e)})`);
|
|
23288
|
+
land([]);
|
|
23289
|
+
};
|
|
23290
|
+
try {
|
|
23291
|
+
const out = source(bar, { end, signal });
|
|
23292
|
+
if (Array.isArray(out)) land(out);
|
|
23293
|
+
else entry.done = Promise.resolve(out).then(land, fail);
|
|
23294
|
+
} catch (e) {
|
|
23295
|
+
fail(e);
|
|
23296
|
+
}
|
|
23297
|
+
this.replayTickFetches.set(bar.time, entry);
|
|
23298
|
+
return entry;
|
|
23299
|
+
}
|
|
23300
|
+
/** Abort the tick requests in flight and forget the landed ones. */
|
|
23301
|
+
dropTickFetches() {
|
|
23302
|
+
this.replayTickAbort?.abort();
|
|
23303
|
+
this.replayTickAbort = null;
|
|
23304
|
+
this.replayTickFetches.clear();
|
|
23305
|
+
}
|
|
23306
|
+
clearReplayTimer() {
|
|
23307
|
+
if (this.replayTimer != null) clearTimeout(this.replayTimer);
|
|
23308
|
+
this.replayTimer = null;
|
|
23309
|
+
this.replayPlaying = false;
|
|
23310
|
+
}
|
|
23311
|
+
/**
|
|
23312
|
+
* Leave replay. `restore` puts the queued bars back and restarts the consumers over the
|
|
23313
|
+
* full history (a market switch skips it: its load replaces everything anyway); `resume`
|
|
23314
|
+
* restarts live updates and heals the bars that closed while replaying. A null reason
|
|
23315
|
+
* leaves silently — a seek that re-enters right after.
|
|
23316
|
+
*/
|
|
23317
|
+
endReplay(reason, opts) {
|
|
23318
|
+
const queue = this.replayQueue;
|
|
23319
|
+
if (!queue) return;
|
|
23320
|
+
this.clearReplayTimer();
|
|
23321
|
+
if (opts.restore) this.settleFormingBar();
|
|
23322
|
+
else this.replayForming = null;
|
|
23323
|
+
this.dropTickFetches();
|
|
23324
|
+
this.replayQueue = null;
|
|
23325
|
+
const depth = reason ? this.replayDepth : null;
|
|
23326
|
+
if (reason) this.replayDepth = null;
|
|
23327
|
+
if (depth) {
|
|
23328
|
+
if (depth.bars === void 0) delete this.config.market.bars;
|
|
23329
|
+
else this.config.market.bars = depth.bars;
|
|
23330
|
+
}
|
|
23331
|
+
if (opts.restore) {
|
|
23332
|
+
const keep = depth ? depth.bars ?? 500 : Infinity;
|
|
23333
|
+
if (queue.length > 0 || this.rawBars.length > keep) {
|
|
23334
|
+
const full = [...this.rawBars, ...queue];
|
|
23335
|
+
this.setBarSeries(full.length > keep ? full.slice(full.length - keep) : full);
|
|
23336
|
+
this.restartBarConsumers({ blank: false });
|
|
23337
|
+
} else if (this.config.live) {
|
|
23338
|
+
this.restartNativeIndicators();
|
|
23339
|
+
this.restartChartTypeEngine();
|
|
23340
|
+
}
|
|
23341
|
+
}
|
|
23342
|
+
if (opts.resume && this.config.live) {
|
|
23343
|
+
this.startLive();
|
|
23344
|
+
const last = this.rawBars[this.rawBars.length - 1];
|
|
23345
|
+
if (last && this.canHeal()) void this.healGap(last.time);
|
|
23346
|
+
}
|
|
23347
|
+
if (reason) this.events.emit("replay:end", { reason });
|
|
23348
|
+
}
|
|
23349
|
+
/**
|
|
23350
|
+
* The bars were replaced by a cut or a restore of the SAME market: every consumer re-runs.
|
|
23351
|
+
* A cut also blanks the mounted models first — they still paint the hidden bars' values,
|
|
23352
|
+
* which must not show until the re-runs land.
|
|
23353
|
+
*/
|
|
23354
|
+
restartBarConsumers(opts) {
|
|
23355
|
+
if (opts.blank) this.blankIndicatorVisuals();
|
|
23356
|
+
this.restartNativeIndicators();
|
|
23357
|
+
this.restartChartTypeEngine();
|
|
23358
|
+
this.reexecuteIndicators();
|
|
23359
|
+
}
|
|
22938
23360
|
/** The primary symbol as the data layer resolves it, or null if no symbol is set. */
|
|
22939
23361
|
qualifiedSymbol() {
|
|
22940
23362
|
const market = this.config.market;
|
|
@@ -23006,7 +23428,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
23006
23428
|
engine.start({
|
|
23007
23429
|
symbol: this.config.market.symbol ?? "TEST",
|
|
23008
23430
|
timeframe: this.config.market.timeframe ?? "60",
|
|
23009
|
-
live: this.config.live ?? false,
|
|
23431
|
+
live: (this.config.live ?? false) && this.replayQueue === null,
|
|
23010
23432
|
session: this.config.market.session,
|
|
23011
23433
|
bars: () => this.bars,
|
|
23012
23434
|
data: this.dataControl,
|
|
@@ -23132,6 +23554,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
23132
23554
|
record.session.notifyBars(reason);
|
|
23133
23555
|
} else if (record.native) record.native.instance.onBars();
|
|
23134
23556
|
}
|
|
23557
|
+
if (this.activeEngineStyle) this.typeEngines.get(this.activeEngineStyle)?.onBars?.();
|
|
23135
23558
|
}
|
|
23136
23559
|
/** Gap healing needs a ranged feed and a provider-backed series (offline `data` has no source). */
|
|
23137
23560
|
canHeal() {
|
|
@@ -23154,10 +23577,11 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
23154
23577
|
try {
|
|
23155
23578
|
const bars = await this.feed.loadRange(this.config.market, { from: fromMs });
|
|
23156
23579
|
if (this.generation !== gen) return;
|
|
23580
|
+
if (this.replayQueue) return;
|
|
23157
23581
|
for (const b of bars) this.applyBar(b, false);
|
|
23158
23582
|
} catch {
|
|
23159
23583
|
} finally {
|
|
23160
|
-
if (this.generation === gen) {
|
|
23584
|
+
if (this.generation === gen && !this.replayQueue) {
|
|
23161
23585
|
this.healing = false;
|
|
23162
23586
|
const pending = this.healBuffer;
|
|
23163
23587
|
this.healBuffer = [];
|
|
@@ -23462,6 +23886,13 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
23462
23886
|
destroy() {
|
|
23463
23887
|
this.bumpGeneration();
|
|
23464
23888
|
this.resolveHistoryComplete();
|
|
23889
|
+
this.clearReplayTimer();
|
|
23890
|
+
this.replayQueue = null;
|
|
23891
|
+
this.replayForming = null;
|
|
23892
|
+
this.replayCarrying = null;
|
|
23893
|
+
this.dropTickFetches();
|
|
23894
|
+
this.replayDepth = null;
|
|
23895
|
+
this.replayLoading = null;
|
|
23465
23896
|
this.stopLive();
|
|
23466
23897
|
if (this.viewportTimer != null) clearTimeout(this.viewportTimer);
|
|
23467
23898
|
this.viewportUnsub?.();
|
|
@@ -23560,6 +23991,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
23560
23991
|
this.emitScriptRun(id, cause, first);
|
|
23561
23992
|
},
|
|
23562
23993
|
onAlert: (a) => {
|
|
23994
|
+
if (this.replayQueue || this.replayCarrying) return;
|
|
23563
23995
|
const indicator = record.options?.title ?? record.prepared?.meta.title ?? record.title;
|
|
23564
23996
|
this.events.emit("alert", { ...a, indicator });
|
|
23565
23997
|
handle.emit("alert", { id: a.id, message: a.message, title: a.title, time: a.time });
|
|
@@ -23586,7 +24018,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
23586
24018
|
id,
|
|
23587
24019
|
symbol: this.config.market.symbol ?? "TEST",
|
|
23588
24020
|
timeframe: this.config.market.timeframe ?? "60",
|
|
23589
|
-
live: this.config.live,
|
|
24021
|
+
live: this.config.live && this.replayQueue === null,
|
|
23590
24022
|
session: this.config.market.session,
|
|
23591
24023
|
bars: () => this.bars,
|
|
23592
24024
|
data: this.dataControl,
|
|
@@ -23989,17 +24421,17 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
23989
24421
|
}
|
|
23990
24422
|
emitContextChanged(id) {
|
|
23991
24423
|
if (!this.registry.get(id)?.session?.getContext) return;
|
|
23992
|
-
const
|
|
23993
|
-
if (
|
|
23994
|
-
this.contextEmitAt.set(id,
|
|
24424
|
+
const now2 = Date.now();
|
|
24425
|
+
if (now2 - (this.contextEmitAt.get(id) ?? 0) < 1e3) return;
|
|
24426
|
+
this.contextEmitAt.set(id, now2);
|
|
23995
24427
|
this.events.emit("context:changed", { id });
|
|
23996
24428
|
}
|
|
23997
24429
|
emitScriptRun(id, cause, first) {
|
|
23998
24430
|
const record = this.registry.get(id);
|
|
23999
24431
|
if (!record || record.native || !this.events.hasListeners("script:run")) return;
|
|
24000
|
-
const
|
|
24001
|
-
if (cause === "tick" &&
|
|
24002
|
-
this.runEmitAt.set(id,
|
|
24432
|
+
const now2 = Date.now();
|
|
24433
|
+
if (cause === "tick" && now2 - (this.runEmitAt.get(id) ?? 0) < RUN_EMIT_THROTTLE_MS) return;
|
|
24434
|
+
this.runEmitAt.set(id, now2);
|
|
24003
24435
|
void Promise.resolve().then(() => this.buildScriptRun(record, cause, first)).then((run) => {
|
|
24004
24436
|
if (run && this.registry.get(id)) this.events.emit("script:run", run);
|
|
24005
24437
|
});
|
|
@@ -24052,6 +24484,16 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
24052
24484
|
handle?.emit("error", { error });
|
|
24053
24485
|
}
|
|
24054
24486
|
};
|
|
24487
|
+
function now() {
|
|
24488
|
+
return typeof performance !== "undefined" ? performance.now() : Date.now();
|
|
24489
|
+
}
|
|
24490
|
+
function usableTicks(value) {
|
|
24491
|
+
if (!Array.isArray(value)) return [];
|
|
24492
|
+
const optional = (n) => n === void 0 || Number.isFinite(n);
|
|
24493
|
+
return value.filter(
|
|
24494
|
+
(t) => t != null && Number.isFinite(t.price) && optional(t.volume) && optional(t.high) && optional(t.low) && optional(t.open)
|
|
24495
|
+
);
|
|
24496
|
+
}
|
|
24055
24497
|
function yieldToPaint() {
|
|
24056
24498
|
if (typeof requestAnimationFrame === "function") {
|
|
24057
24499
|
return new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(() => resolve())));
|
|
@@ -28127,6 +28569,7 @@ var Vela = class {
|
|
|
28127
28569
|
this.panesControl = new PanesControl(this.orchestrator);
|
|
28128
28570
|
this.drawingsControl = new DrawingsControl(this.orchestrator.drawings);
|
|
28129
28571
|
this.marksControl = new MarksControl(this.orchestrator.marks);
|
|
28572
|
+
this.replayControl = new ReplayControl(this.orchestrator);
|
|
28130
28573
|
}
|
|
28131
28574
|
/**
|
|
28132
28575
|
* Register a scripting engine so `addIndicator({ language })` can run that
|
|
@@ -28366,6 +28809,16 @@ var Vela = class {
|
|
|
28366
28809
|
get marks() {
|
|
28367
28810
|
return this.marksControl;
|
|
28368
28811
|
}
|
|
28812
|
+
/**
|
|
28813
|
+
* The chart's bar-replay control surface: rewind to a past bar and reveal the
|
|
28814
|
+
* following ones by hand or on a timer, from the history already loaded —
|
|
28815
|
+
* `await chart.replay.start({ from })`, `chart.replay.play(2000)`, `chart.replay.step()`,
|
|
28816
|
+
* `chart.replay.stop()`. Revealed bars behave like live bars for indicators; live
|
|
28817
|
+
* updates pause meanwhile. Follow it with the `replay:*` events.
|
|
28818
|
+
*/
|
|
28819
|
+
get replay() {
|
|
28820
|
+
return this.replayControl;
|
|
28821
|
+
}
|
|
28369
28822
|
on(event, handler) {
|
|
28370
28823
|
return this.orchestrator.events.on(event, handler);
|
|
28371
28824
|
}
|
|
@@ -28424,4 +28877,4 @@ function resolveElement(container) {
|
|
|
28424
28877
|
return element;
|
|
28425
28878
|
}
|
|
28426
28879
|
|
|
28427
|
-
export { BUILTIN_PRICE_STYLES, BarStore, CachingDataFeed, DARK_THEME, DataControl, DrawingStore, DrawingToolbar, DrawingsControl, LEGEND_AT_TOP_ATTR, LIGHT_THEME, MarksControl, MultiProviderFeed, NativeRenderer, RendererControl, SecondClock, TIMEZONES, TypedEventBus, Vela, applyAttributionMarkTheme, buildToolbar, createAttributionMark, createCustomMark, defaultMemberOf, defaultToolbar, groupKeyOf, groupMembers, isExchangeTimezone, isGroupRow, normalizeSession, normalizeTimezone, parseSymbol, priceStyleIds, registerBuiltinChartTypes, resolveTheme, resolveTimezone, sharedBarStore, timeframeToMs, timezoneMenuRows, tzButtonLabel, tzMenuLabel, tzOffset };
|
|
28880
|
+
export { BUILTIN_PRICE_STYLES, BarStore, CachingDataFeed, DARK_THEME, DataControl, DrawingStore, DrawingToolbar, DrawingsControl, LEGEND_AT_TOP_ATTR, LIGHT_THEME, MarksControl, MultiProviderFeed, NativeRenderer, RendererControl, ReplayControl, SecondClock, TIMEZONES, TypedEventBus, Vela, applyAttributionMarkTheme, buildToolbar, createAttributionMark, createCustomMark, defaultMemberOf, defaultToolbar, groupKeyOf, groupMembers, isExchangeTimezone, isGroupRow, normalizeSession, normalizeTimezone, parseSymbol, priceStyleIds, registerBuiltinChartTypes, resolveTheme, resolveTimezone, sharedBarStore, timeframeToMs, timezoneMenuRows, tzButtonLabel, tzMenuLabel, tzOffset };
|