hyperprop-charting-library 0.1.131 → 0.1.132
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/dist/hyperprop-charting-library.cjs +221 -1
- package/dist/hyperprop-charting-library.d.ts +57 -1
- package/dist/hyperprop-charting-library.js +220 -1
- package/dist/index.cjs +221 -1
- package/dist/index.d.cts +57 -1
- package/dist/index.d.ts +57 -1
- package/dist/index.js +220 -1
- package/docs/API.md +64 -0
- package/package.json +1 -1
|
@@ -22,6 +22,7 @@ var index_exports = {};
|
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
FIB_DEFAULT_PALETTE: () => FIB_DEFAULT_PALETTE,
|
|
24
24
|
POSITION_DEFAULT_COLORS: () => POSITION_DEFAULT_COLORS,
|
|
25
|
+
compileScriptIndicator: () => compileScriptIndicator,
|
|
25
26
|
createChart: () => createChart
|
|
26
27
|
});
|
|
27
28
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -1494,6 +1495,195 @@ var drawSeparateMultiSeries = (ctx, renderContext, seriesList, options = {}) =>
|
|
|
1494
1495
|
}
|
|
1495
1496
|
return paneInfo;
|
|
1496
1497
|
};
|
|
1498
|
+
var scriptRollingExtreme = (values, length, mode) => {
|
|
1499
|
+
const fallback = mode === "max" ? Number.NEGATIVE_INFINITY : Number.POSITIVE_INFINITY;
|
|
1500
|
+
const filled = new Array(values.length);
|
|
1501
|
+
for (let i = 0; i < values.length; i += 1) {
|
|
1502
|
+
const value = values[i];
|
|
1503
|
+
filled[i] = value != null && Number.isFinite(value) ? value : fallback;
|
|
1504
|
+
}
|
|
1505
|
+
const raw = rollingExtremeSeries(filled, Math.max(1, Math.floor(length)), mode);
|
|
1506
|
+
return raw.map((value) => value != null && Number.isFinite(value) ? value : null);
|
|
1507
|
+
};
|
|
1508
|
+
var scriptStdevSeries = (values, length) => {
|
|
1509
|
+
const window2 = Math.max(1, Math.floor(length));
|
|
1510
|
+
const result = new Array(values.length).fill(null);
|
|
1511
|
+
let sum = 0;
|
|
1512
|
+
let sumSq = 0;
|
|
1513
|
+
let valid = 0;
|
|
1514
|
+
for (let i = 0; i < values.length; i += 1) {
|
|
1515
|
+
const value = values[i];
|
|
1516
|
+
if (value == null || !Number.isFinite(value)) {
|
|
1517
|
+
sum = 0;
|
|
1518
|
+
sumSq = 0;
|
|
1519
|
+
valid = 0;
|
|
1520
|
+
continue;
|
|
1521
|
+
}
|
|
1522
|
+
sum += value;
|
|
1523
|
+
sumSq += value * value;
|
|
1524
|
+
valid += 1;
|
|
1525
|
+
if (valid > window2) {
|
|
1526
|
+
const old = values[i - window2];
|
|
1527
|
+
sum -= old;
|
|
1528
|
+
sumSq -= old * old;
|
|
1529
|
+
valid -= 1;
|
|
1530
|
+
}
|
|
1531
|
+
if (valid === window2) {
|
|
1532
|
+
const mean = sum / window2;
|
|
1533
|
+
result[i] = Math.sqrt(Math.max(0, sumSq / window2 - mean * mean));
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
return result;
|
|
1537
|
+
};
|
|
1538
|
+
var scriptAtrSeries = (bars, length) => {
|
|
1539
|
+
const tr = new Array(bars.length).fill(null);
|
|
1540
|
+
for (let i = 0; i < bars.length; i += 1) {
|
|
1541
|
+
const bar = bars[i];
|
|
1542
|
+
if (i === 0) {
|
|
1543
|
+
tr[i] = bar.h - bar.l;
|
|
1544
|
+
continue;
|
|
1545
|
+
}
|
|
1546
|
+
const prevClose = bars[i - 1].c;
|
|
1547
|
+
tr[i] = Math.max(bar.h - bar.l, Math.abs(bar.h - prevClose), Math.abs(bar.l - prevClose));
|
|
1548
|
+
}
|
|
1549
|
+
return rmaFromValues(tr, Math.max(1, Math.floor(length)));
|
|
1550
|
+
};
|
|
1551
|
+
var SCRIPT_HELPERS = Object.freeze({
|
|
1552
|
+
sma: (values, length) => smaFromValues(values, Math.max(1, Math.floor(length))),
|
|
1553
|
+
ema: (values, length) => emaFromValues(values, Math.max(1, Math.floor(length))),
|
|
1554
|
+
rma: (values, length) => rmaFromValues(values, Math.max(1, Math.floor(length))),
|
|
1555
|
+
highest: (values, length) => scriptRollingExtreme(values, length, "max"),
|
|
1556
|
+
lowest: (values, length) => scriptRollingExtreme(values, length, "min"),
|
|
1557
|
+
change: (values, length = 1) => values.map((value, index) => {
|
|
1558
|
+
const prev = values[index - Math.max(1, Math.floor(length))];
|
|
1559
|
+
return value != null && prev != null ? value - prev : null;
|
|
1560
|
+
}),
|
|
1561
|
+
stdev: scriptStdevSeries,
|
|
1562
|
+
atr: scriptAtrSeries
|
|
1563
|
+
});
|
|
1564
|
+
var SCRIPT_PLOT_PALETTE = ["#2962ff", "#ff6d00", "#26a69a", "#ab47bc", "#ef5350", "#fdd835"];
|
|
1565
|
+
var hashScriptSource = (source) => {
|
|
1566
|
+
let hash = 5381;
|
|
1567
|
+
for (let i = 0; i < source.length; i += 1) {
|
|
1568
|
+
hash = (hash << 5) + hash + source.charCodeAt(i) | 0;
|
|
1569
|
+
}
|
|
1570
|
+
return (hash >>> 0).toString(36);
|
|
1571
|
+
};
|
|
1572
|
+
var compileScriptCompute = (source) => {
|
|
1573
|
+
const factory = new Function(
|
|
1574
|
+
`"use strict";
|
|
1575
|
+
${source}
|
|
1576
|
+
;if (typeof compute !== "function") { throw new Error("Script must define function compute(bars, inputs, hp)"); }
|
|
1577
|
+
return compute;`
|
|
1578
|
+
);
|
|
1579
|
+
return factory();
|
|
1580
|
+
};
|
|
1581
|
+
var drawScriptError = (ctx, renderContext, name, message) => {
|
|
1582
|
+
ctx.save();
|
|
1583
|
+
ctx.font = "11px -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif";
|
|
1584
|
+
ctx.textAlign = "left";
|
|
1585
|
+
ctx.textBaseline = "top";
|
|
1586
|
+
ctx.fillStyle = "#ef5350";
|
|
1587
|
+
const text = `${name}: ${message}`.slice(0, 160);
|
|
1588
|
+
ctx.fillText(text, renderContext.chartLeft + 10, renderContext.chartTop + 24);
|
|
1589
|
+
ctx.restore();
|
|
1590
|
+
};
|
|
1591
|
+
var compileScriptIndicator = (definition) => {
|
|
1592
|
+
const pane = definition.pane ?? "separate";
|
|
1593
|
+
const defaultInputs = { showValueLine: true };
|
|
1594
|
+
for (const input of definition.inputs ?? []) {
|
|
1595
|
+
defaultInputs[input.key] = input.default;
|
|
1596
|
+
}
|
|
1597
|
+
let compiled = null;
|
|
1598
|
+
let compileError = null;
|
|
1599
|
+
try {
|
|
1600
|
+
compiled = compileScriptCompute(definition.source);
|
|
1601
|
+
} catch (error) {
|
|
1602
|
+
compileError = error instanceof Error ? error.message : String(error);
|
|
1603
|
+
}
|
|
1604
|
+
const sourceKey = hashScriptSource(definition.source);
|
|
1605
|
+
const runCompute = (data, inputs) => {
|
|
1606
|
+
if (!compiled) {
|
|
1607
|
+
return { error: compileError ?? "Script failed to compile" };
|
|
1608
|
+
}
|
|
1609
|
+
try {
|
|
1610
|
+
const result = compiled(data, inputs, SCRIPT_HELPERS);
|
|
1611
|
+
if (!result || !Array.isArray(result.plots)) {
|
|
1612
|
+
return { error: "compute() must return { plots: [...] }" };
|
|
1613
|
+
}
|
|
1614
|
+
for (const plot of result.plots) {
|
|
1615
|
+
if (!plot || !Array.isArray(plot.values)) {
|
|
1616
|
+
return { error: "Every plot needs a `values` array" };
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
return { result };
|
|
1620
|
+
} catch (error) {
|
|
1621
|
+
return { error: error instanceof Error ? error.message : String(error) };
|
|
1622
|
+
}
|
|
1623
|
+
};
|
|
1624
|
+
const cachedCompute = (data, inputs) => withCachedComputation(
|
|
1625
|
+
`script|${definition.id}|${sourceKey}|${JSON.stringify(inputs)}`,
|
|
1626
|
+
data,
|
|
1627
|
+
() => runCompute(data, inputs)
|
|
1628
|
+
);
|
|
1629
|
+
const plugin = {
|
|
1630
|
+
id: definition.id,
|
|
1631
|
+
name: definition.name,
|
|
1632
|
+
pane,
|
|
1633
|
+
defaultInputs,
|
|
1634
|
+
draw: (ctx, renderContext, inputs) => {
|
|
1635
|
+
const outcome = cachedCompute(renderContext.data, inputs);
|
|
1636
|
+
if (!outcome.result) {
|
|
1637
|
+
drawScriptError(ctx, renderContext, definition.name, outcome.error ?? "Unknown script error");
|
|
1638
|
+
return pane === "separate" ? { title: `${definition.name} (script error)` } : void 0;
|
|
1639
|
+
}
|
|
1640
|
+
const { plots, range, guides, decimals } = outcome.result;
|
|
1641
|
+
if (pane === "separate") {
|
|
1642
|
+
const seriesList = plots.map((plot, index) => ({
|
|
1643
|
+
values: plot.values,
|
|
1644
|
+
color: plot.color ?? SCRIPT_PLOT_PALETTE[index % SCRIPT_PLOT_PALETTE.length],
|
|
1645
|
+
...plot.width !== void 0 ? { width: plot.width } : {},
|
|
1646
|
+
...plot.title ? { label: plot.title } : {},
|
|
1647
|
+
...plot.style === "histogram" ? { histogram: true } : {},
|
|
1648
|
+
...plot.negativeColor ? { negativeColor: plot.negativeColor } : {}
|
|
1649
|
+
}));
|
|
1650
|
+
return drawSeparateMultiSeries(ctx, renderContext, seriesList, {
|
|
1651
|
+
title: definition.name,
|
|
1652
|
+
...range?.min !== void 0 ? { minOverride: range.min } : {},
|
|
1653
|
+
...range?.max !== void 0 ? { maxOverride: range.max } : {},
|
|
1654
|
+
...guides && guides.length > 0 ? { guideLines: guides } : {},
|
|
1655
|
+
...decimals !== void 0 ? { decimals } : {},
|
|
1656
|
+
valueLines: inputs.showValueLine !== false
|
|
1657
|
+
});
|
|
1658
|
+
}
|
|
1659
|
+
for (let index = 0; index < plots.length; index += 1) {
|
|
1660
|
+
const plot = plots[index];
|
|
1661
|
+
if (plot.style === "histogram") continue;
|
|
1662
|
+
drawOverlaySeries(
|
|
1663
|
+
ctx,
|
|
1664
|
+
renderContext,
|
|
1665
|
+
plot.values,
|
|
1666
|
+
plot.color ?? SCRIPT_PLOT_PALETTE[index % SCRIPT_PLOT_PALETTE.length],
|
|
1667
|
+
plot.width ?? 2
|
|
1668
|
+
);
|
|
1669
|
+
}
|
|
1670
|
+
return void 0;
|
|
1671
|
+
},
|
|
1672
|
+
...pane === "overlay" ? {
|
|
1673
|
+
getAutoscaleRange: (data, startIndex, endIndex, inputs, skipIndex) => {
|
|
1674
|
+
const outcome = cachedCompute(data, inputs);
|
|
1675
|
+
if (!outcome.result) return null;
|
|
1676
|
+
const lineSeries = outcome.result.plots.filter((plot) => plot.style !== "histogram").map((plot) => plot.values);
|
|
1677
|
+
if (lineSeries.length === 0) return null;
|
|
1678
|
+
return rangeOfSeries(lineSeries, startIndex, endIndex, skipIndex);
|
|
1679
|
+
}
|
|
1680
|
+
} : {}
|
|
1681
|
+
};
|
|
1682
|
+
if (definition.paneHeightRatio !== void 0) {
|
|
1683
|
+
plugin.paneHeightRatio = definition.paneHeightRatio;
|
|
1684
|
+
}
|
|
1685
|
+
return plugin;
|
|
1686
|
+
};
|
|
1497
1687
|
var computeMacd = (data, fast, slow, signalLength) => {
|
|
1498
1688
|
const fastEma = computeEmaSeries(data, fast, "close");
|
|
1499
1689
|
const slowEma = computeEmaSeries(data, slow, "close");
|
|
@@ -2545,7 +2735,7 @@ function createChart(element, options = {}) {
|
|
|
2545
2735
|
return;
|
|
2546
2736
|
}
|
|
2547
2737
|
const tickerOpts = mergedOptions.tickerLine ?? DEFAULT_OPTIONS.tickerLine;
|
|
2548
|
-
const speed = clamp(tickerOpts.smoothingSpeed ??
|
|
2738
|
+
const speed = clamp(tickerOpts.smoothingSpeed ?? 16, 1, 60);
|
|
2549
2739
|
const dt = 1 / 60;
|
|
2550
2740
|
const lerp = 1 - Math.exp(-speed * dt);
|
|
2551
2741
|
smoothedTickerPrice += priceDiff * lerp;
|
|
@@ -2575,6 +2765,29 @@ function createChart(element, options = {}) {
|
|
|
2575
2765
|
smoothingRafId = requestAnimationFrame(tickerSmoothingLoop);
|
|
2576
2766
|
}
|
|
2577
2767
|
};
|
|
2768
|
+
let countdownTimerId = null;
|
|
2769
|
+
const countdownRepaintNeeded = () => {
|
|
2770
|
+
if (data.length === 0) {
|
|
2771
|
+
return false;
|
|
2772
|
+
}
|
|
2773
|
+
const ticker = mergedOptions.tickerLine ?? DEFAULT_OPTIONS.tickerLine;
|
|
2774
|
+
const labels = mergedOptions.labels ?? DEFAULT_OPTIONS.labels;
|
|
2775
|
+
return Boolean(ticker.showCountdownInLabel) || Boolean(labels.visible && labels.showCountdownToBarClose);
|
|
2776
|
+
};
|
|
2777
|
+
const armCountdownHeartbeat = () => {
|
|
2778
|
+
if (countdownTimerId !== null || !countdownRepaintNeeded()) {
|
|
2779
|
+
return;
|
|
2780
|
+
}
|
|
2781
|
+
const delay = 1e3 - Date.now() % 1e3 + 15;
|
|
2782
|
+
countdownTimerId = setTimeout(() => {
|
|
2783
|
+
countdownTimerId = null;
|
|
2784
|
+
if (!countdownRepaintNeeded()) {
|
|
2785
|
+
return;
|
|
2786
|
+
}
|
|
2787
|
+
scheduleDraw({ updateAutoScale: false });
|
|
2788
|
+
armCountdownHeartbeat();
|
|
2789
|
+
}, delay);
|
|
2790
|
+
};
|
|
2578
2791
|
const canvas = document.createElement("canvas");
|
|
2579
2792
|
const ctx = canvas.getContext("2d");
|
|
2580
2793
|
if (!ctx) {
|
|
@@ -3500,6 +3713,7 @@ function createChart(element, options = {}) {
|
|
|
3500
3713
|
let pendingDrawUpdateAutoScale = false;
|
|
3501
3714
|
let pendingDrawOverlayOnly = true;
|
|
3502
3715
|
const scheduleDraw = (options2 = {}) => {
|
|
3716
|
+
armCountdownHeartbeat();
|
|
3503
3717
|
const overlayOnly = options2.overlayOnly ?? false;
|
|
3504
3718
|
pendingDrawOverlayOnly = pendingDrawOverlayOnly && overlayOnly;
|
|
3505
3719
|
pendingDrawUpdateAutoScale = pendingDrawUpdateAutoScale || !overlayOnly && (options2.updateAutoScale ?? true);
|
|
@@ -7757,6 +7971,10 @@ function createChart(element, options = {}) {
|
|
|
7757
7971
|
cancelAnimationFrame(smoothingRafId);
|
|
7758
7972
|
smoothingRafId = null;
|
|
7759
7973
|
}
|
|
7974
|
+
if (countdownTimerId !== null) {
|
|
7975
|
+
clearTimeout(countdownTimerId);
|
|
7976
|
+
countdownTimerId = null;
|
|
7977
|
+
}
|
|
7760
7978
|
if (drawRafId !== null) {
|
|
7761
7979
|
cancelAnimationFrame(drawRafId);
|
|
7762
7980
|
drawRafId = null;
|
|
@@ -7778,6 +7996,7 @@ function createChart(element, options = {}) {
|
|
|
7778
7996
|
element.innerHTML = "";
|
|
7779
7997
|
};
|
|
7780
7998
|
draw();
|
|
7999
|
+
armCountdownHeartbeat();
|
|
7781
8000
|
return {
|
|
7782
8001
|
updateOptions,
|
|
7783
8002
|
setChartType,
|
|
@@ -7848,5 +8067,6 @@ function createChart(element, options = {}) {
|
|
|
7848
8067
|
0 && (module.exports = {
|
|
7849
8068
|
FIB_DEFAULT_PALETTE,
|
|
7850
8069
|
POSITION_DEFAULT_COLORS,
|
|
8070
|
+
compileScriptIndicator,
|
|
7851
8071
|
createChart
|
|
7852
8072
|
});
|
|
@@ -501,6 +501,62 @@ interface LabelsOptions {
|
|
|
501
501
|
labelHeight?: number;
|
|
502
502
|
labelPaddingX?: number;
|
|
503
503
|
}
|
|
504
|
+
interface ScriptIndicatorInputDef {
|
|
505
|
+
key: string;
|
|
506
|
+
label?: string;
|
|
507
|
+
type?: "number" | "color" | "boolean" | "select" | "string";
|
|
508
|
+
default: unknown;
|
|
509
|
+
min?: number;
|
|
510
|
+
max?: number;
|
|
511
|
+
step?: number;
|
|
512
|
+
options?: Array<{
|
|
513
|
+
value: string;
|
|
514
|
+
label: string;
|
|
515
|
+
}>;
|
|
516
|
+
}
|
|
517
|
+
interface ScriptIndicatorDefinition {
|
|
518
|
+
/** Unique indicator id, e.g. "script:my-rsi". */
|
|
519
|
+
id: string;
|
|
520
|
+
name: string;
|
|
521
|
+
/** Defaults to "separate". */
|
|
522
|
+
pane?: IndicatorPane;
|
|
523
|
+
paneHeightRatio?: number;
|
|
524
|
+
inputs?: ScriptIndicatorInputDef[];
|
|
525
|
+
/**
|
|
526
|
+
* JavaScript source that must define `function compute(bars, inputs, hp)`.
|
|
527
|
+
* `bars` is an array of `{ time, o, h, l, c, v }`, `inputs` the resolved
|
|
528
|
+
* input values, and `hp` a bundle of TA helpers (sma, ema, rma, highest,
|
|
529
|
+
* lowest, change, stdev, atr). It must return `{ plots: [...] }`.
|
|
530
|
+
*/
|
|
531
|
+
source: string;
|
|
532
|
+
}
|
|
533
|
+
interface ScriptPlotSpec {
|
|
534
|
+
title?: string;
|
|
535
|
+
values: Array<number | null>;
|
|
536
|
+
color?: string;
|
|
537
|
+
width?: number;
|
|
538
|
+
style?: "line" | "histogram";
|
|
539
|
+
negativeColor?: string;
|
|
540
|
+
}
|
|
541
|
+
interface ScriptComputeResult {
|
|
542
|
+
plots: ScriptPlotSpec[];
|
|
543
|
+
range?: {
|
|
544
|
+
min?: number;
|
|
545
|
+
max?: number;
|
|
546
|
+
};
|
|
547
|
+
guides?: number[];
|
|
548
|
+
decimals?: number;
|
|
549
|
+
}
|
|
550
|
+
/**
|
|
551
|
+
* Compile a user-authored script into a regular indicator plugin. The plugin
|
|
552
|
+
* can then be registered with `chart.registerIndicator(...)` and behaves like
|
|
553
|
+
* a built-in: separate panes get the shared grid, legends, value lines,
|
|
554
|
+
* drag-resize and hover controls; overlays autoscale with price.
|
|
555
|
+
*
|
|
556
|
+
* Compile/runtime errors never throw during rendering; they render as an
|
|
557
|
+
* inline error message in the pane instead.
|
|
558
|
+
*/
|
|
559
|
+
declare const compileScriptIndicator: (definition: ScriptIndicatorDefinition) => IndicatorPlugin;
|
|
504
560
|
interface ChartInstance {
|
|
505
561
|
updateOptions: (options: ChartOptions) => void;
|
|
506
562
|
/** Switch the main series rendering style (candles, bars, line, ...). */
|
|
@@ -597,4 +653,4 @@ interface ViewportState {
|
|
|
597
653
|
}
|
|
598
654
|
declare function createChart(element: HTMLElement, options?: ChartOptions): ChartInstance;
|
|
599
655
|
|
|
600
|
-
export { type AxisOptions, type BuiltInIndicatorInfo, type ChartClickEvent, type ChartInstance, type ChartOptions, type ChartType, type CrosshairMoveEvent, type CrosshairOptions, type CrosshairPriceActionEvent, type DashPatternOptions, type DrawingDefaults, type DrawingHoverEvent, type DrawingObjectOptions, type DrawingPoint, type DrawingSelectEvent, type DrawingToolType, FIB_DEFAULT_PALETTE, type GridOptions, type IndicatorInstanceOptions, type IndicatorPane, type IndicatorPaneActionEvent, type IndicatorPaneAxisOptions, type IndicatorPaneGuideLine, type IndicatorPaneHeightChangeEvent, type IndicatorPaneRenderInfo, type IndicatorPaneValue, type IndicatorPaneValueLabel, type IndicatorPlugin, type IndicatorRenderContext, type LabelsOptions, type OhlcDataPoint, type OrderActionButton, type OrderActionEvent, type OrderLineOptions, POSITION_DEFAULT_COLORS, type PriceLineOptions, type TickerLineOptions, type TradeMarkerOptions, type ViewportState, type WatermarkOptions, createChart };
|
|
656
|
+
export { type AxisOptions, type BuiltInIndicatorInfo, type ChartClickEvent, type ChartInstance, type ChartOptions, type ChartType, type CrosshairMoveEvent, type CrosshairOptions, type CrosshairPriceActionEvent, type DashPatternOptions, type DrawingDefaults, type DrawingHoverEvent, type DrawingObjectOptions, type DrawingPoint, type DrawingSelectEvent, type DrawingToolType, FIB_DEFAULT_PALETTE, type GridOptions, type IndicatorInstanceOptions, type IndicatorPane, type IndicatorPaneActionEvent, type IndicatorPaneAxisOptions, type IndicatorPaneGuideLine, type IndicatorPaneHeightChangeEvent, type IndicatorPaneRenderInfo, type IndicatorPaneValue, type IndicatorPaneValueLabel, type IndicatorPlugin, type IndicatorRenderContext, type LabelsOptions, type OhlcDataPoint, type OrderActionButton, type OrderActionEvent, type OrderLineOptions, POSITION_DEFAULT_COLORS, type PriceLineOptions, type ScriptComputeResult, type ScriptIndicatorDefinition, type ScriptIndicatorInputDef, type ScriptPlotSpec, type TickerLineOptions, type TradeMarkerOptions, type ViewportState, type WatermarkOptions, compileScriptIndicator, createChart };
|
|
@@ -1468,6 +1468,195 @@ var drawSeparateMultiSeries = (ctx, renderContext, seriesList, options = {}) =>
|
|
|
1468
1468
|
}
|
|
1469
1469
|
return paneInfo;
|
|
1470
1470
|
};
|
|
1471
|
+
var scriptRollingExtreme = (values, length, mode) => {
|
|
1472
|
+
const fallback = mode === "max" ? Number.NEGATIVE_INFINITY : Number.POSITIVE_INFINITY;
|
|
1473
|
+
const filled = new Array(values.length);
|
|
1474
|
+
for (let i = 0; i < values.length; i += 1) {
|
|
1475
|
+
const value = values[i];
|
|
1476
|
+
filled[i] = value != null && Number.isFinite(value) ? value : fallback;
|
|
1477
|
+
}
|
|
1478
|
+
const raw = rollingExtremeSeries(filled, Math.max(1, Math.floor(length)), mode);
|
|
1479
|
+
return raw.map((value) => value != null && Number.isFinite(value) ? value : null);
|
|
1480
|
+
};
|
|
1481
|
+
var scriptStdevSeries = (values, length) => {
|
|
1482
|
+
const window2 = Math.max(1, Math.floor(length));
|
|
1483
|
+
const result = new Array(values.length).fill(null);
|
|
1484
|
+
let sum = 0;
|
|
1485
|
+
let sumSq = 0;
|
|
1486
|
+
let valid = 0;
|
|
1487
|
+
for (let i = 0; i < values.length; i += 1) {
|
|
1488
|
+
const value = values[i];
|
|
1489
|
+
if (value == null || !Number.isFinite(value)) {
|
|
1490
|
+
sum = 0;
|
|
1491
|
+
sumSq = 0;
|
|
1492
|
+
valid = 0;
|
|
1493
|
+
continue;
|
|
1494
|
+
}
|
|
1495
|
+
sum += value;
|
|
1496
|
+
sumSq += value * value;
|
|
1497
|
+
valid += 1;
|
|
1498
|
+
if (valid > window2) {
|
|
1499
|
+
const old = values[i - window2];
|
|
1500
|
+
sum -= old;
|
|
1501
|
+
sumSq -= old * old;
|
|
1502
|
+
valid -= 1;
|
|
1503
|
+
}
|
|
1504
|
+
if (valid === window2) {
|
|
1505
|
+
const mean = sum / window2;
|
|
1506
|
+
result[i] = Math.sqrt(Math.max(0, sumSq / window2 - mean * mean));
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
return result;
|
|
1510
|
+
};
|
|
1511
|
+
var scriptAtrSeries = (bars, length) => {
|
|
1512
|
+
const tr = new Array(bars.length).fill(null);
|
|
1513
|
+
for (let i = 0; i < bars.length; i += 1) {
|
|
1514
|
+
const bar = bars[i];
|
|
1515
|
+
if (i === 0) {
|
|
1516
|
+
tr[i] = bar.h - bar.l;
|
|
1517
|
+
continue;
|
|
1518
|
+
}
|
|
1519
|
+
const prevClose = bars[i - 1].c;
|
|
1520
|
+
tr[i] = Math.max(bar.h - bar.l, Math.abs(bar.h - prevClose), Math.abs(bar.l - prevClose));
|
|
1521
|
+
}
|
|
1522
|
+
return rmaFromValues(tr, Math.max(1, Math.floor(length)));
|
|
1523
|
+
};
|
|
1524
|
+
var SCRIPT_HELPERS = Object.freeze({
|
|
1525
|
+
sma: (values, length) => smaFromValues(values, Math.max(1, Math.floor(length))),
|
|
1526
|
+
ema: (values, length) => emaFromValues(values, Math.max(1, Math.floor(length))),
|
|
1527
|
+
rma: (values, length) => rmaFromValues(values, Math.max(1, Math.floor(length))),
|
|
1528
|
+
highest: (values, length) => scriptRollingExtreme(values, length, "max"),
|
|
1529
|
+
lowest: (values, length) => scriptRollingExtreme(values, length, "min"),
|
|
1530
|
+
change: (values, length = 1) => values.map((value, index) => {
|
|
1531
|
+
const prev = values[index - Math.max(1, Math.floor(length))];
|
|
1532
|
+
return value != null && prev != null ? value - prev : null;
|
|
1533
|
+
}),
|
|
1534
|
+
stdev: scriptStdevSeries,
|
|
1535
|
+
atr: scriptAtrSeries
|
|
1536
|
+
});
|
|
1537
|
+
var SCRIPT_PLOT_PALETTE = ["#2962ff", "#ff6d00", "#26a69a", "#ab47bc", "#ef5350", "#fdd835"];
|
|
1538
|
+
var hashScriptSource = (source) => {
|
|
1539
|
+
let hash = 5381;
|
|
1540
|
+
for (let i = 0; i < source.length; i += 1) {
|
|
1541
|
+
hash = (hash << 5) + hash + source.charCodeAt(i) | 0;
|
|
1542
|
+
}
|
|
1543
|
+
return (hash >>> 0).toString(36);
|
|
1544
|
+
};
|
|
1545
|
+
var compileScriptCompute = (source) => {
|
|
1546
|
+
const factory = new Function(
|
|
1547
|
+
`"use strict";
|
|
1548
|
+
${source}
|
|
1549
|
+
;if (typeof compute !== "function") { throw new Error("Script must define function compute(bars, inputs, hp)"); }
|
|
1550
|
+
return compute;`
|
|
1551
|
+
);
|
|
1552
|
+
return factory();
|
|
1553
|
+
};
|
|
1554
|
+
var drawScriptError = (ctx, renderContext, name, message) => {
|
|
1555
|
+
ctx.save();
|
|
1556
|
+
ctx.font = "11px -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif";
|
|
1557
|
+
ctx.textAlign = "left";
|
|
1558
|
+
ctx.textBaseline = "top";
|
|
1559
|
+
ctx.fillStyle = "#ef5350";
|
|
1560
|
+
const text = `${name}: ${message}`.slice(0, 160);
|
|
1561
|
+
ctx.fillText(text, renderContext.chartLeft + 10, renderContext.chartTop + 24);
|
|
1562
|
+
ctx.restore();
|
|
1563
|
+
};
|
|
1564
|
+
var compileScriptIndicator = (definition) => {
|
|
1565
|
+
const pane = definition.pane ?? "separate";
|
|
1566
|
+
const defaultInputs = { showValueLine: true };
|
|
1567
|
+
for (const input of definition.inputs ?? []) {
|
|
1568
|
+
defaultInputs[input.key] = input.default;
|
|
1569
|
+
}
|
|
1570
|
+
let compiled = null;
|
|
1571
|
+
let compileError = null;
|
|
1572
|
+
try {
|
|
1573
|
+
compiled = compileScriptCompute(definition.source);
|
|
1574
|
+
} catch (error) {
|
|
1575
|
+
compileError = error instanceof Error ? error.message : String(error);
|
|
1576
|
+
}
|
|
1577
|
+
const sourceKey = hashScriptSource(definition.source);
|
|
1578
|
+
const runCompute = (data, inputs) => {
|
|
1579
|
+
if (!compiled) {
|
|
1580
|
+
return { error: compileError ?? "Script failed to compile" };
|
|
1581
|
+
}
|
|
1582
|
+
try {
|
|
1583
|
+
const result = compiled(data, inputs, SCRIPT_HELPERS);
|
|
1584
|
+
if (!result || !Array.isArray(result.plots)) {
|
|
1585
|
+
return { error: "compute() must return { plots: [...] }" };
|
|
1586
|
+
}
|
|
1587
|
+
for (const plot of result.plots) {
|
|
1588
|
+
if (!plot || !Array.isArray(plot.values)) {
|
|
1589
|
+
return { error: "Every plot needs a `values` array" };
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
return { result };
|
|
1593
|
+
} catch (error) {
|
|
1594
|
+
return { error: error instanceof Error ? error.message : String(error) };
|
|
1595
|
+
}
|
|
1596
|
+
};
|
|
1597
|
+
const cachedCompute = (data, inputs) => withCachedComputation(
|
|
1598
|
+
`script|${definition.id}|${sourceKey}|${JSON.stringify(inputs)}`,
|
|
1599
|
+
data,
|
|
1600
|
+
() => runCompute(data, inputs)
|
|
1601
|
+
);
|
|
1602
|
+
const plugin = {
|
|
1603
|
+
id: definition.id,
|
|
1604
|
+
name: definition.name,
|
|
1605
|
+
pane,
|
|
1606
|
+
defaultInputs,
|
|
1607
|
+
draw: (ctx, renderContext, inputs) => {
|
|
1608
|
+
const outcome = cachedCompute(renderContext.data, inputs);
|
|
1609
|
+
if (!outcome.result) {
|
|
1610
|
+
drawScriptError(ctx, renderContext, definition.name, outcome.error ?? "Unknown script error");
|
|
1611
|
+
return pane === "separate" ? { title: `${definition.name} (script error)` } : void 0;
|
|
1612
|
+
}
|
|
1613
|
+
const { plots, range, guides, decimals } = outcome.result;
|
|
1614
|
+
if (pane === "separate") {
|
|
1615
|
+
const seriesList = plots.map((plot, index) => ({
|
|
1616
|
+
values: plot.values,
|
|
1617
|
+
color: plot.color ?? SCRIPT_PLOT_PALETTE[index % SCRIPT_PLOT_PALETTE.length],
|
|
1618
|
+
...plot.width !== void 0 ? { width: plot.width } : {},
|
|
1619
|
+
...plot.title ? { label: plot.title } : {},
|
|
1620
|
+
...plot.style === "histogram" ? { histogram: true } : {},
|
|
1621
|
+
...plot.negativeColor ? { negativeColor: plot.negativeColor } : {}
|
|
1622
|
+
}));
|
|
1623
|
+
return drawSeparateMultiSeries(ctx, renderContext, seriesList, {
|
|
1624
|
+
title: definition.name,
|
|
1625
|
+
...range?.min !== void 0 ? { minOverride: range.min } : {},
|
|
1626
|
+
...range?.max !== void 0 ? { maxOverride: range.max } : {},
|
|
1627
|
+
...guides && guides.length > 0 ? { guideLines: guides } : {},
|
|
1628
|
+
...decimals !== void 0 ? { decimals } : {},
|
|
1629
|
+
valueLines: inputs.showValueLine !== false
|
|
1630
|
+
});
|
|
1631
|
+
}
|
|
1632
|
+
for (let index = 0; index < plots.length; index += 1) {
|
|
1633
|
+
const plot = plots[index];
|
|
1634
|
+
if (plot.style === "histogram") continue;
|
|
1635
|
+
drawOverlaySeries(
|
|
1636
|
+
ctx,
|
|
1637
|
+
renderContext,
|
|
1638
|
+
plot.values,
|
|
1639
|
+
plot.color ?? SCRIPT_PLOT_PALETTE[index % SCRIPT_PLOT_PALETTE.length],
|
|
1640
|
+
plot.width ?? 2
|
|
1641
|
+
);
|
|
1642
|
+
}
|
|
1643
|
+
return void 0;
|
|
1644
|
+
},
|
|
1645
|
+
...pane === "overlay" ? {
|
|
1646
|
+
getAutoscaleRange: (data, startIndex, endIndex, inputs, skipIndex) => {
|
|
1647
|
+
const outcome = cachedCompute(data, inputs);
|
|
1648
|
+
if (!outcome.result) return null;
|
|
1649
|
+
const lineSeries = outcome.result.plots.filter((plot) => plot.style !== "histogram").map((plot) => plot.values);
|
|
1650
|
+
if (lineSeries.length === 0) return null;
|
|
1651
|
+
return rangeOfSeries(lineSeries, startIndex, endIndex, skipIndex);
|
|
1652
|
+
}
|
|
1653
|
+
} : {}
|
|
1654
|
+
};
|
|
1655
|
+
if (definition.paneHeightRatio !== void 0) {
|
|
1656
|
+
plugin.paneHeightRatio = definition.paneHeightRatio;
|
|
1657
|
+
}
|
|
1658
|
+
return plugin;
|
|
1659
|
+
};
|
|
1471
1660
|
var computeMacd = (data, fast, slow, signalLength) => {
|
|
1472
1661
|
const fastEma = computeEmaSeries(data, fast, "close");
|
|
1473
1662
|
const slowEma = computeEmaSeries(data, slow, "close");
|
|
@@ -2519,7 +2708,7 @@ function createChart(element, options = {}) {
|
|
|
2519
2708
|
return;
|
|
2520
2709
|
}
|
|
2521
2710
|
const tickerOpts = mergedOptions.tickerLine ?? DEFAULT_OPTIONS.tickerLine;
|
|
2522
|
-
const speed = clamp(tickerOpts.smoothingSpeed ??
|
|
2711
|
+
const speed = clamp(tickerOpts.smoothingSpeed ?? 16, 1, 60);
|
|
2523
2712
|
const dt = 1 / 60;
|
|
2524
2713
|
const lerp = 1 - Math.exp(-speed * dt);
|
|
2525
2714
|
smoothedTickerPrice += priceDiff * lerp;
|
|
@@ -2549,6 +2738,29 @@ function createChart(element, options = {}) {
|
|
|
2549
2738
|
smoothingRafId = requestAnimationFrame(tickerSmoothingLoop);
|
|
2550
2739
|
}
|
|
2551
2740
|
};
|
|
2741
|
+
let countdownTimerId = null;
|
|
2742
|
+
const countdownRepaintNeeded = () => {
|
|
2743
|
+
if (data.length === 0) {
|
|
2744
|
+
return false;
|
|
2745
|
+
}
|
|
2746
|
+
const ticker = mergedOptions.tickerLine ?? DEFAULT_OPTIONS.tickerLine;
|
|
2747
|
+
const labels = mergedOptions.labels ?? DEFAULT_OPTIONS.labels;
|
|
2748
|
+
return Boolean(ticker.showCountdownInLabel) || Boolean(labels.visible && labels.showCountdownToBarClose);
|
|
2749
|
+
};
|
|
2750
|
+
const armCountdownHeartbeat = () => {
|
|
2751
|
+
if (countdownTimerId !== null || !countdownRepaintNeeded()) {
|
|
2752
|
+
return;
|
|
2753
|
+
}
|
|
2754
|
+
const delay = 1e3 - Date.now() % 1e3 + 15;
|
|
2755
|
+
countdownTimerId = setTimeout(() => {
|
|
2756
|
+
countdownTimerId = null;
|
|
2757
|
+
if (!countdownRepaintNeeded()) {
|
|
2758
|
+
return;
|
|
2759
|
+
}
|
|
2760
|
+
scheduleDraw({ updateAutoScale: false });
|
|
2761
|
+
armCountdownHeartbeat();
|
|
2762
|
+
}, delay);
|
|
2763
|
+
};
|
|
2552
2764
|
const canvas = document.createElement("canvas");
|
|
2553
2765
|
const ctx = canvas.getContext("2d");
|
|
2554
2766
|
if (!ctx) {
|
|
@@ -3474,6 +3686,7 @@ function createChart(element, options = {}) {
|
|
|
3474
3686
|
let pendingDrawUpdateAutoScale = false;
|
|
3475
3687
|
let pendingDrawOverlayOnly = true;
|
|
3476
3688
|
const scheduleDraw = (options2 = {}) => {
|
|
3689
|
+
armCountdownHeartbeat();
|
|
3477
3690
|
const overlayOnly = options2.overlayOnly ?? false;
|
|
3478
3691
|
pendingDrawOverlayOnly = pendingDrawOverlayOnly && overlayOnly;
|
|
3479
3692
|
pendingDrawUpdateAutoScale = pendingDrawUpdateAutoScale || !overlayOnly && (options2.updateAutoScale ?? true);
|
|
@@ -7731,6 +7944,10 @@ function createChart(element, options = {}) {
|
|
|
7731
7944
|
cancelAnimationFrame(smoothingRafId);
|
|
7732
7945
|
smoothingRafId = null;
|
|
7733
7946
|
}
|
|
7947
|
+
if (countdownTimerId !== null) {
|
|
7948
|
+
clearTimeout(countdownTimerId);
|
|
7949
|
+
countdownTimerId = null;
|
|
7950
|
+
}
|
|
7734
7951
|
if (drawRafId !== null) {
|
|
7735
7952
|
cancelAnimationFrame(drawRafId);
|
|
7736
7953
|
drawRafId = null;
|
|
@@ -7752,6 +7969,7 @@ function createChart(element, options = {}) {
|
|
|
7752
7969
|
element.innerHTML = "";
|
|
7753
7970
|
};
|
|
7754
7971
|
draw();
|
|
7972
|
+
armCountdownHeartbeat();
|
|
7755
7973
|
return {
|
|
7756
7974
|
updateOptions,
|
|
7757
7975
|
setChartType,
|
|
@@ -7821,5 +8039,6 @@ function createChart(element, options = {}) {
|
|
|
7821
8039
|
export {
|
|
7822
8040
|
FIB_DEFAULT_PALETTE,
|
|
7823
8041
|
POSITION_DEFAULT_COLORS,
|
|
8042
|
+
compileScriptIndicator,
|
|
7824
8043
|
createChart
|
|
7825
8044
|
};
|
package/dist/index.cjs
CHANGED
|
@@ -22,6 +22,7 @@ var index_exports = {};
|
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
FIB_DEFAULT_PALETTE: () => FIB_DEFAULT_PALETTE,
|
|
24
24
|
POSITION_DEFAULT_COLORS: () => POSITION_DEFAULT_COLORS,
|
|
25
|
+
compileScriptIndicator: () => compileScriptIndicator,
|
|
25
26
|
createChart: () => createChart
|
|
26
27
|
});
|
|
27
28
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -1494,6 +1495,195 @@ var drawSeparateMultiSeries = (ctx, renderContext, seriesList, options = {}) =>
|
|
|
1494
1495
|
}
|
|
1495
1496
|
return paneInfo;
|
|
1496
1497
|
};
|
|
1498
|
+
var scriptRollingExtreme = (values, length, mode) => {
|
|
1499
|
+
const fallback = mode === "max" ? Number.NEGATIVE_INFINITY : Number.POSITIVE_INFINITY;
|
|
1500
|
+
const filled = new Array(values.length);
|
|
1501
|
+
for (let i = 0; i < values.length; i += 1) {
|
|
1502
|
+
const value = values[i];
|
|
1503
|
+
filled[i] = value != null && Number.isFinite(value) ? value : fallback;
|
|
1504
|
+
}
|
|
1505
|
+
const raw = rollingExtremeSeries(filled, Math.max(1, Math.floor(length)), mode);
|
|
1506
|
+
return raw.map((value) => value != null && Number.isFinite(value) ? value : null);
|
|
1507
|
+
};
|
|
1508
|
+
var scriptStdevSeries = (values, length) => {
|
|
1509
|
+
const window2 = Math.max(1, Math.floor(length));
|
|
1510
|
+
const result = new Array(values.length).fill(null);
|
|
1511
|
+
let sum = 0;
|
|
1512
|
+
let sumSq = 0;
|
|
1513
|
+
let valid = 0;
|
|
1514
|
+
for (let i = 0; i < values.length; i += 1) {
|
|
1515
|
+
const value = values[i];
|
|
1516
|
+
if (value == null || !Number.isFinite(value)) {
|
|
1517
|
+
sum = 0;
|
|
1518
|
+
sumSq = 0;
|
|
1519
|
+
valid = 0;
|
|
1520
|
+
continue;
|
|
1521
|
+
}
|
|
1522
|
+
sum += value;
|
|
1523
|
+
sumSq += value * value;
|
|
1524
|
+
valid += 1;
|
|
1525
|
+
if (valid > window2) {
|
|
1526
|
+
const old = values[i - window2];
|
|
1527
|
+
sum -= old;
|
|
1528
|
+
sumSq -= old * old;
|
|
1529
|
+
valid -= 1;
|
|
1530
|
+
}
|
|
1531
|
+
if (valid === window2) {
|
|
1532
|
+
const mean = sum / window2;
|
|
1533
|
+
result[i] = Math.sqrt(Math.max(0, sumSq / window2 - mean * mean));
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
return result;
|
|
1537
|
+
};
|
|
1538
|
+
var scriptAtrSeries = (bars, length) => {
|
|
1539
|
+
const tr = new Array(bars.length).fill(null);
|
|
1540
|
+
for (let i = 0; i < bars.length; i += 1) {
|
|
1541
|
+
const bar = bars[i];
|
|
1542
|
+
if (i === 0) {
|
|
1543
|
+
tr[i] = bar.h - bar.l;
|
|
1544
|
+
continue;
|
|
1545
|
+
}
|
|
1546
|
+
const prevClose = bars[i - 1].c;
|
|
1547
|
+
tr[i] = Math.max(bar.h - bar.l, Math.abs(bar.h - prevClose), Math.abs(bar.l - prevClose));
|
|
1548
|
+
}
|
|
1549
|
+
return rmaFromValues(tr, Math.max(1, Math.floor(length)));
|
|
1550
|
+
};
|
|
1551
|
+
var SCRIPT_HELPERS = Object.freeze({
|
|
1552
|
+
sma: (values, length) => smaFromValues(values, Math.max(1, Math.floor(length))),
|
|
1553
|
+
ema: (values, length) => emaFromValues(values, Math.max(1, Math.floor(length))),
|
|
1554
|
+
rma: (values, length) => rmaFromValues(values, Math.max(1, Math.floor(length))),
|
|
1555
|
+
highest: (values, length) => scriptRollingExtreme(values, length, "max"),
|
|
1556
|
+
lowest: (values, length) => scriptRollingExtreme(values, length, "min"),
|
|
1557
|
+
change: (values, length = 1) => values.map((value, index) => {
|
|
1558
|
+
const prev = values[index - Math.max(1, Math.floor(length))];
|
|
1559
|
+
return value != null && prev != null ? value - prev : null;
|
|
1560
|
+
}),
|
|
1561
|
+
stdev: scriptStdevSeries,
|
|
1562
|
+
atr: scriptAtrSeries
|
|
1563
|
+
});
|
|
1564
|
+
var SCRIPT_PLOT_PALETTE = ["#2962ff", "#ff6d00", "#26a69a", "#ab47bc", "#ef5350", "#fdd835"];
|
|
1565
|
+
var hashScriptSource = (source) => {
|
|
1566
|
+
let hash = 5381;
|
|
1567
|
+
for (let i = 0; i < source.length; i += 1) {
|
|
1568
|
+
hash = (hash << 5) + hash + source.charCodeAt(i) | 0;
|
|
1569
|
+
}
|
|
1570
|
+
return (hash >>> 0).toString(36);
|
|
1571
|
+
};
|
|
1572
|
+
var compileScriptCompute = (source) => {
|
|
1573
|
+
const factory = new Function(
|
|
1574
|
+
`"use strict";
|
|
1575
|
+
${source}
|
|
1576
|
+
;if (typeof compute !== "function") { throw new Error("Script must define function compute(bars, inputs, hp)"); }
|
|
1577
|
+
return compute;`
|
|
1578
|
+
);
|
|
1579
|
+
return factory();
|
|
1580
|
+
};
|
|
1581
|
+
var drawScriptError = (ctx, renderContext, name, message) => {
|
|
1582
|
+
ctx.save();
|
|
1583
|
+
ctx.font = "11px -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif";
|
|
1584
|
+
ctx.textAlign = "left";
|
|
1585
|
+
ctx.textBaseline = "top";
|
|
1586
|
+
ctx.fillStyle = "#ef5350";
|
|
1587
|
+
const text = `${name}: ${message}`.slice(0, 160);
|
|
1588
|
+
ctx.fillText(text, renderContext.chartLeft + 10, renderContext.chartTop + 24);
|
|
1589
|
+
ctx.restore();
|
|
1590
|
+
};
|
|
1591
|
+
var compileScriptIndicator = (definition) => {
|
|
1592
|
+
const pane = definition.pane ?? "separate";
|
|
1593
|
+
const defaultInputs = { showValueLine: true };
|
|
1594
|
+
for (const input of definition.inputs ?? []) {
|
|
1595
|
+
defaultInputs[input.key] = input.default;
|
|
1596
|
+
}
|
|
1597
|
+
let compiled = null;
|
|
1598
|
+
let compileError = null;
|
|
1599
|
+
try {
|
|
1600
|
+
compiled = compileScriptCompute(definition.source);
|
|
1601
|
+
} catch (error) {
|
|
1602
|
+
compileError = error instanceof Error ? error.message : String(error);
|
|
1603
|
+
}
|
|
1604
|
+
const sourceKey = hashScriptSource(definition.source);
|
|
1605
|
+
const runCompute = (data, inputs) => {
|
|
1606
|
+
if (!compiled) {
|
|
1607
|
+
return { error: compileError ?? "Script failed to compile" };
|
|
1608
|
+
}
|
|
1609
|
+
try {
|
|
1610
|
+
const result = compiled(data, inputs, SCRIPT_HELPERS);
|
|
1611
|
+
if (!result || !Array.isArray(result.plots)) {
|
|
1612
|
+
return { error: "compute() must return { plots: [...] }" };
|
|
1613
|
+
}
|
|
1614
|
+
for (const plot of result.plots) {
|
|
1615
|
+
if (!plot || !Array.isArray(plot.values)) {
|
|
1616
|
+
return { error: "Every plot needs a `values` array" };
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
return { result };
|
|
1620
|
+
} catch (error) {
|
|
1621
|
+
return { error: error instanceof Error ? error.message : String(error) };
|
|
1622
|
+
}
|
|
1623
|
+
};
|
|
1624
|
+
const cachedCompute = (data, inputs) => withCachedComputation(
|
|
1625
|
+
`script|${definition.id}|${sourceKey}|${JSON.stringify(inputs)}`,
|
|
1626
|
+
data,
|
|
1627
|
+
() => runCompute(data, inputs)
|
|
1628
|
+
);
|
|
1629
|
+
const plugin = {
|
|
1630
|
+
id: definition.id,
|
|
1631
|
+
name: definition.name,
|
|
1632
|
+
pane,
|
|
1633
|
+
defaultInputs,
|
|
1634
|
+
draw: (ctx, renderContext, inputs) => {
|
|
1635
|
+
const outcome = cachedCompute(renderContext.data, inputs);
|
|
1636
|
+
if (!outcome.result) {
|
|
1637
|
+
drawScriptError(ctx, renderContext, definition.name, outcome.error ?? "Unknown script error");
|
|
1638
|
+
return pane === "separate" ? { title: `${definition.name} (script error)` } : void 0;
|
|
1639
|
+
}
|
|
1640
|
+
const { plots, range, guides, decimals } = outcome.result;
|
|
1641
|
+
if (pane === "separate") {
|
|
1642
|
+
const seriesList = plots.map((plot, index) => ({
|
|
1643
|
+
values: plot.values,
|
|
1644
|
+
color: plot.color ?? SCRIPT_PLOT_PALETTE[index % SCRIPT_PLOT_PALETTE.length],
|
|
1645
|
+
...plot.width !== void 0 ? { width: plot.width } : {},
|
|
1646
|
+
...plot.title ? { label: plot.title } : {},
|
|
1647
|
+
...plot.style === "histogram" ? { histogram: true } : {},
|
|
1648
|
+
...plot.negativeColor ? { negativeColor: plot.negativeColor } : {}
|
|
1649
|
+
}));
|
|
1650
|
+
return drawSeparateMultiSeries(ctx, renderContext, seriesList, {
|
|
1651
|
+
title: definition.name,
|
|
1652
|
+
...range?.min !== void 0 ? { minOverride: range.min } : {},
|
|
1653
|
+
...range?.max !== void 0 ? { maxOverride: range.max } : {},
|
|
1654
|
+
...guides && guides.length > 0 ? { guideLines: guides } : {},
|
|
1655
|
+
...decimals !== void 0 ? { decimals } : {},
|
|
1656
|
+
valueLines: inputs.showValueLine !== false
|
|
1657
|
+
});
|
|
1658
|
+
}
|
|
1659
|
+
for (let index = 0; index < plots.length; index += 1) {
|
|
1660
|
+
const plot = plots[index];
|
|
1661
|
+
if (plot.style === "histogram") continue;
|
|
1662
|
+
drawOverlaySeries(
|
|
1663
|
+
ctx,
|
|
1664
|
+
renderContext,
|
|
1665
|
+
plot.values,
|
|
1666
|
+
plot.color ?? SCRIPT_PLOT_PALETTE[index % SCRIPT_PLOT_PALETTE.length],
|
|
1667
|
+
plot.width ?? 2
|
|
1668
|
+
);
|
|
1669
|
+
}
|
|
1670
|
+
return void 0;
|
|
1671
|
+
},
|
|
1672
|
+
...pane === "overlay" ? {
|
|
1673
|
+
getAutoscaleRange: (data, startIndex, endIndex, inputs, skipIndex) => {
|
|
1674
|
+
const outcome = cachedCompute(data, inputs);
|
|
1675
|
+
if (!outcome.result) return null;
|
|
1676
|
+
const lineSeries = outcome.result.plots.filter((plot) => plot.style !== "histogram").map((plot) => plot.values);
|
|
1677
|
+
if (lineSeries.length === 0) return null;
|
|
1678
|
+
return rangeOfSeries(lineSeries, startIndex, endIndex, skipIndex);
|
|
1679
|
+
}
|
|
1680
|
+
} : {}
|
|
1681
|
+
};
|
|
1682
|
+
if (definition.paneHeightRatio !== void 0) {
|
|
1683
|
+
plugin.paneHeightRatio = definition.paneHeightRatio;
|
|
1684
|
+
}
|
|
1685
|
+
return plugin;
|
|
1686
|
+
};
|
|
1497
1687
|
var computeMacd = (data, fast, slow, signalLength) => {
|
|
1498
1688
|
const fastEma = computeEmaSeries(data, fast, "close");
|
|
1499
1689
|
const slowEma = computeEmaSeries(data, slow, "close");
|
|
@@ -2545,7 +2735,7 @@ function createChart(element, options = {}) {
|
|
|
2545
2735
|
return;
|
|
2546
2736
|
}
|
|
2547
2737
|
const tickerOpts = mergedOptions.tickerLine ?? DEFAULT_OPTIONS.tickerLine;
|
|
2548
|
-
const speed = clamp(tickerOpts.smoothingSpeed ??
|
|
2738
|
+
const speed = clamp(tickerOpts.smoothingSpeed ?? 16, 1, 60);
|
|
2549
2739
|
const dt = 1 / 60;
|
|
2550
2740
|
const lerp = 1 - Math.exp(-speed * dt);
|
|
2551
2741
|
smoothedTickerPrice += priceDiff * lerp;
|
|
@@ -2575,6 +2765,29 @@ function createChart(element, options = {}) {
|
|
|
2575
2765
|
smoothingRafId = requestAnimationFrame(tickerSmoothingLoop);
|
|
2576
2766
|
}
|
|
2577
2767
|
};
|
|
2768
|
+
let countdownTimerId = null;
|
|
2769
|
+
const countdownRepaintNeeded = () => {
|
|
2770
|
+
if (data.length === 0) {
|
|
2771
|
+
return false;
|
|
2772
|
+
}
|
|
2773
|
+
const ticker = mergedOptions.tickerLine ?? DEFAULT_OPTIONS.tickerLine;
|
|
2774
|
+
const labels = mergedOptions.labels ?? DEFAULT_OPTIONS.labels;
|
|
2775
|
+
return Boolean(ticker.showCountdownInLabel) || Boolean(labels.visible && labels.showCountdownToBarClose);
|
|
2776
|
+
};
|
|
2777
|
+
const armCountdownHeartbeat = () => {
|
|
2778
|
+
if (countdownTimerId !== null || !countdownRepaintNeeded()) {
|
|
2779
|
+
return;
|
|
2780
|
+
}
|
|
2781
|
+
const delay = 1e3 - Date.now() % 1e3 + 15;
|
|
2782
|
+
countdownTimerId = setTimeout(() => {
|
|
2783
|
+
countdownTimerId = null;
|
|
2784
|
+
if (!countdownRepaintNeeded()) {
|
|
2785
|
+
return;
|
|
2786
|
+
}
|
|
2787
|
+
scheduleDraw({ updateAutoScale: false });
|
|
2788
|
+
armCountdownHeartbeat();
|
|
2789
|
+
}, delay);
|
|
2790
|
+
};
|
|
2578
2791
|
const canvas = document.createElement("canvas");
|
|
2579
2792
|
const ctx = canvas.getContext("2d");
|
|
2580
2793
|
if (!ctx) {
|
|
@@ -3500,6 +3713,7 @@ function createChart(element, options = {}) {
|
|
|
3500
3713
|
let pendingDrawUpdateAutoScale = false;
|
|
3501
3714
|
let pendingDrawOverlayOnly = true;
|
|
3502
3715
|
const scheduleDraw = (options2 = {}) => {
|
|
3716
|
+
armCountdownHeartbeat();
|
|
3503
3717
|
const overlayOnly = options2.overlayOnly ?? false;
|
|
3504
3718
|
pendingDrawOverlayOnly = pendingDrawOverlayOnly && overlayOnly;
|
|
3505
3719
|
pendingDrawUpdateAutoScale = pendingDrawUpdateAutoScale || !overlayOnly && (options2.updateAutoScale ?? true);
|
|
@@ -7757,6 +7971,10 @@ function createChart(element, options = {}) {
|
|
|
7757
7971
|
cancelAnimationFrame(smoothingRafId);
|
|
7758
7972
|
smoothingRafId = null;
|
|
7759
7973
|
}
|
|
7974
|
+
if (countdownTimerId !== null) {
|
|
7975
|
+
clearTimeout(countdownTimerId);
|
|
7976
|
+
countdownTimerId = null;
|
|
7977
|
+
}
|
|
7760
7978
|
if (drawRafId !== null) {
|
|
7761
7979
|
cancelAnimationFrame(drawRafId);
|
|
7762
7980
|
drawRafId = null;
|
|
@@ -7778,6 +7996,7 @@ function createChart(element, options = {}) {
|
|
|
7778
7996
|
element.innerHTML = "";
|
|
7779
7997
|
};
|
|
7780
7998
|
draw();
|
|
7999
|
+
armCountdownHeartbeat();
|
|
7781
8000
|
return {
|
|
7782
8001
|
updateOptions,
|
|
7783
8002
|
setChartType,
|
|
@@ -7848,5 +8067,6 @@ function createChart(element, options = {}) {
|
|
|
7848
8067
|
0 && (module.exports = {
|
|
7849
8068
|
FIB_DEFAULT_PALETTE,
|
|
7850
8069
|
POSITION_DEFAULT_COLORS,
|
|
8070
|
+
compileScriptIndicator,
|
|
7851
8071
|
createChart
|
|
7852
8072
|
});
|
package/dist/index.d.cts
CHANGED
|
@@ -501,6 +501,62 @@ interface LabelsOptions {
|
|
|
501
501
|
labelHeight?: number;
|
|
502
502
|
labelPaddingX?: number;
|
|
503
503
|
}
|
|
504
|
+
interface ScriptIndicatorInputDef {
|
|
505
|
+
key: string;
|
|
506
|
+
label?: string;
|
|
507
|
+
type?: "number" | "color" | "boolean" | "select" | "string";
|
|
508
|
+
default: unknown;
|
|
509
|
+
min?: number;
|
|
510
|
+
max?: number;
|
|
511
|
+
step?: number;
|
|
512
|
+
options?: Array<{
|
|
513
|
+
value: string;
|
|
514
|
+
label: string;
|
|
515
|
+
}>;
|
|
516
|
+
}
|
|
517
|
+
interface ScriptIndicatorDefinition {
|
|
518
|
+
/** Unique indicator id, e.g. "script:my-rsi". */
|
|
519
|
+
id: string;
|
|
520
|
+
name: string;
|
|
521
|
+
/** Defaults to "separate". */
|
|
522
|
+
pane?: IndicatorPane;
|
|
523
|
+
paneHeightRatio?: number;
|
|
524
|
+
inputs?: ScriptIndicatorInputDef[];
|
|
525
|
+
/**
|
|
526
|
+
* JavaScript source that must define `function compute(bars, inputs, hp)`.
|
|
527
|
+
* `bars` is an array of `{ time, o, h, l, c, v }`, `inputs` the resolved
|
|
528
|
+
* input values, and `hp` a bundle of TA helpers (sma, ema, rma, highest,
|
|
529
|
+
* lowest, change, stdev, atr). It must return `{ plots: [...] }`.
|
|
530
|
+
*/
|
|
531
|
+
source: string;
|
|
532
|
+
}
|
|
533
|
+
interface ScriptPlotSpec {
|
|
534
|
+
title?: string;
|
|
535
|
+
values: Array<number | null>;
|
|
536
|
+
color?: string;
|
|
537
|
+
width?: number;
|
|
538
|
+
style?: "line" | "histogram";
|
|
539
|
+
negativeColor?: string;
|
|
540
|
+
}
|
|
541
|
+
interface ScriptComputeResult {
|
|
542
|
+
plots: ScriptPlotSpec[];
|
|
543
|
+
range?: {
|
|
544
|
+
min?: number;
|
|
545
|
+
max?: number;
|
|
546
|
+
};
|
|
547
|
+
guides?: number[];
|
|
548
|
+
decimals?: number;
|
|
549
|
+
}
|
|
550
|
+
/**
|
|
551
|
+
* Compile a user-authored script into a regular indicator plugin. The plugin
|
|
552
|
+
* can then be registered with `chart.registerIndicator(...)` and behaves like
|
|
553
|
+
* a built-in: separate panes get the shared grid, legends, value lines,
|
|
554
|
+
* drag-resize and hover controls; overlays autoscale with price.
|
|
555
|
+
*
|
|
556
|
+
* Compile/runtime errors never throw during rendering; they render as an
|
|
557
|
+
* inline error message in the pane instead.
|
|
558
|
+
*/
|
|
559
|
+
declare const compileScriptIndicator: (definition: ScriptIndicatorDefinition) => IndicatorPlugin;
|
|
504
560
|
interface ChartInstance {
|
|
505
561
|
updateOptions: (options: ChartOptions) => void;
|
|
506
562
|
/** Switch the main series rendering style (candles, bars, line, ...). */
|
|
@@ -597,4 +653,4 @@ interface ViewportState {
|
|
|
597
653
|
}
|
|
598
654
|
declare function createChart(element: HTMLElement, options?: ChartOptions): ChartInstance;
|
|
599
655
|
|
|
600
|
-
export { type AxisOptions, type BuiltInIndicatorInfo, type ChartClickEvent, type ChartInstance, type ChartOptions, type ChartType, type CrosshairMoveEvent, type CrosshairOptions, type CrosshairPriceActionEvent, type DashPatternOptions, type DrawingDefaults, type DrawingHoverEvent, type DrawingObjectOptions, type DrawingPoint, type DrawingSelectEvent, type DrawingToolType, FIB_DEFAULT_PALETTE, type GridOptions, type IndicatorInstanceOptions, type IndicatorPane, type IndicatorPaneActionEvent, type IndicatorPaneAxisOptions, type IndicatorPaneGuideLine, type IndicatorPaneHeightChangeEvent, type IndicatorPaneRenderInfo, type IndicatorPaneValue, type IndicatorPaneValueLabel, type IndicatorPlugin, type IndicatorRenderContext, type LabelsOptions, type OhlcDataPoint, type OrderActionButton, type OrderActionEvent, type OrderLineOptions, POSITION_DEFAULT_COLORS, type PriceLineOptions, type TickerLineOptions, type TradeMarkerOptions, type ViewportState, type WatermarkOptions, createChart };
|
|
656
|
+
export { type AxisOptions, type BuiltInIndicatorInfo, type ChartClickEvent, type ChartInstance, type ChartOptions, type ChartType, type CrosshairMoveEvent, type CrosshairOptions, type CrosshairPriceActionEvent, type DashPatternOptions, type DrawingDefaults, type DrawingHoverEvent, type DrawingObjectOptions, type DrawingPoint, type DrawingSelectEvent, type DrawingToolType, FIB_DEFAULT_PALETTE, type GridOptions, type IndicatorInstanceOptions, type IndicatorPane, type IndicatorPaneActionEvent, type IndicatorPaneAxisOptions, type IndicatorPaneGuideLine, type IndicatorPaneHeightChangeEvent, type IndicatorPaneRenderInfo, type IndicatorPaneValue, type IndicatorPaneValueLabel, type IndicatorPlugin, type IndicatorRenderContext, type LabelsOptions, type OhlcDataPoint, type OrderActionButton, type OrderActionEvent, type OrderLineOptions, POSITION_DEFAULT_COLORS, type PriceLineOptions, type ScriptComputeResult, type ScriptIndicatorDefinition, type ScriptIndicatorInputDef, type ScriptPlotSpec, type TickerLineOptions, type TradeMarkerOptions, type ViewportState, type WatermarkOptions, compileScriptIndicator, createChart };
|
package/dist/index.d.ts
CHANGED
|
@@ -501,6 +501,62 @@ interface LabelsOptions {
|
|
|
501
501
|
labelHeight?: number;
|
|
502
502
|
labelPaddingX?: number;
|
|
503
503
|
}
|
|
504
|
+
interface ScriptIndicatorInputDef {
|
|
505
|
+
key: string;
|
|
506
|
+
label?: string;
|
|
507
|
+
type?: "number" | "color" | "boolean" | "select" | "string";
|
|
508
|
+
default: unknown;
|
|
509
|
+
min?: number;
|
|
510
|
+
max?: number;
|
|
511
|
+
step?: number;
|
|
512
|
+
options?: Array<{
|
|
513
|
+
value: string;
|
|
514
|
+
label: string;
|
|
515
|
+
}>;
|
|
516
|
+
}
|
|
517
|
+
interface ScriptIndicatorDefinition {
|
|
518
|
+
/** Unique indicator id, e.g. "script:my-rsi". */
|
|
519
|
+
id: string;
|
|
520
|
+
name: string;
|
|
521
|
+
/** Defaults to "separate". */
|
|
522
|
+
pane?: IndicatorPane;
|
|
523
|
+
paneHeightRatio?: number;
|
|
524
|
+
inputs?: ScriptIndicatorInputDef[];
|
|
525
|
+
/**
|
|
526
|
+
* JavaScript source that must define `function compute(bars, inputs, hp)`.
|
|
527
|
+
* `bars` is an array of `{ time, o, h, l, c, v }`, `inputs` the resolved
|
|
528
|
+
* input values, and `hp` a bundle of TA helpers (sma, ema, rma, highest,
|
|
529
|
+
* lowest, change, stdev, atr). It must return `{ plots: [...] }`.
|
|
530
|
+
*/
|
|
531
|
+
source: string;
|
|
532
|
+
}
|
|
533
|
+
interface ScriptPlotSpec {
|
|
534
|
+
title?: string;
|
|
535
|
+
values: Array<number | null>;
|
|
536
|
+
color?: string;
|
|
537
|
+
width?: number;
|
|
538
|
+
style?: "line" | "histogram";
|
|
539
|
+
negativeColor?: string;
|
|
540
|
+
}
|
|
541
|
+
interface ScriptComputeResult {
|
|
542
|
+
plots: ScriptPlotSpec[];
|
|
543
|
+
range?: {
|
|
544
|
+
min?: number;
|
|
545
|
+
max?: number;
|
|
546
|
+
};
|
|
547
|
+
guides?: number[];
|
|
548
|
+
decimals?: number;
|
|
549
|
+
}
|
|
550
|
+
/**
|
|
551
|
+
* Compile a user-authored script into a regular indicator plugin. The plugin
|
|
552
|
+
* can then be registered with `chart.registerIndicator(...)` and behaves like
|
|
553
|
+
* a built-in: separate panes get the shared grid, legends, value lines,
|
|
554
|
+
* drag-resize and hover controls; overlays autoscale with price.
|
|
555
|
+
*
|
|
556
|
+
* Compile/runtime errors never throw during rendering; they render as an
|
|
557
|
+
* inline error message in the pane instead.
|
|
558
|
+
*/
|
|
559
|
+
declare const compileScriptIndicator: (definition: ScriptIndicatorDefinition) => IndicatorPlugin;
|
|
504
560
|
interface ChartInstance {
|
|
505
561
|
updateOptions: (options: ChartOptions) => void;
|
|
506
562
|
/** Switch the main series rendering style (candles, bars, line, ...). */
|
|
@@ -597,4 +653,4 @@ interface ViewportState {
|
|
|
597
653
|
}
|
|
598
654
|
declare function createChart(element: HTMLElement, options?: ChartOptions): ChartInstance;
|
|
599
655
|
|
|
600
|
-
export { type AxisOptions, type BuiltInIndicatorInfo, type ChartClickEvent, type ChartInstance, type ChartOptions, type ChartType, type CrosshairMoveEvent, type CrosshairOptions, type CrosshairPriceActionEvent, type DashPatternOptions, type DrawingDefaults, type DrawingHoverEvent, type DrawingObjectOptions, type DrawingPoint, type DrawingSelectEvent, type DrawingToolType, FIB_DEFAULT_PALETTE, type GridOptions, type IndicatorInstanceOptions, type IndicatorPane, type IndicatorPaneActionEvent, type IndicatorPaneAxisOptions, type IndicatorPaneGuideLine, type IndicatorPaneHeightChangeEvent, type IndicatorPaneRenderInfo, type IndicatorPaneValue, type IndicatorPaneValueLabel, type IndicatorPlugin, type IndicatorRenderContext, type LabelsOptions, type OhlcDataPoint, type OrderActionButton, type OrderActionEvent, type OrderLineOptions, POSITION_DEFAULT_COLORS, type PriceLineOptions, type TickerLineOptions, type TradeMarkerOptions, type ViewportState, type WatermarkOptions, createChart };
|
|
656
|
+
export { type AxisOptions, type BuiltInIndicatorInfo, type ChartClickEvent, type ChartInstance, type ChartOptions, type ChartType, type CrosshairMoveEvent, type CrosshairOptions, type CrosshairPriceActionEvent, type DashPatternOptions, type DrawingDefaults, type DrawingHoverEvent, type DrawingObjectOptions, type DrawingPoint, type DrawingSelectEvent, type DrawingToolType, FIB_DEFAULT_PALETTE, type GridOptions, type IndicatorInstanceOptions, type IndicatorPane, type IndicatorPaneActionEvent, type IndicatorPaneAxisOptions, type IndicatorPaneGuideLine, type IndicatorPaneHeightChangeEvent, type IndicatorPaneRenderInfo, type IndicatorPaneValue, type IndicatorPaneValueLabel, type IndicatorPlugin, type IndicatorRenderContext, type LabelsOptions, type OhlcDataPoint, type OrderActionButton, type OrderActionEvent, type OrderLineOptions, POSITION_DEFAULT_COLORS, type PriceLineOptions, type ScriptComputeResult, type ScriptIndicatorDefinition, type ScriptIndicatorInputDef, type ScriptPlotSpec, type TickerLineOptions, type TradeMarkerOptions, type ViewportState, type WatermarkOptions, compileScriptIndicator, createChart };
|
package/dist/index.js
CHANGED
|
@@ -1468,6 +1468,195 @@ var drawSeparateMultiSeries = (ctx, renderContext, seriesList, options = {}) =>
|
|
|
1468
1468
|
}
|
|
1469
1469
|
return paneInfo;
|
|
1470
1470
|
};
|
|
1471
|
+
var scriptRollingExtreme = (values, length, mode) => {
|
|
1472
|
+
const fallback = mode === "max" ? Number.NEGATIVE_INFINITY : Number.POSITIVE_INFINITY;
|
|
1473
|
+
const filled = new Array(values.length);
|
|
1474
|
+
for (let i = 0; i < values.length; i += 1) {
|
|
1475
|
+
const value = values[i];
|
|
1476
|
+
filled[i] = value != null && Number.isFinite(value) ? value : fallback;
|
|
1477
|
+
}
|
|
1478
|
+
const raw = rollingExtremeSeries(filled, Math.max(1, Math.floor(length)), mode);
|
|
1479
|
+
return raw.map((value) => value != null && Number.isFinite(value) ? value : null);
|
|
1480
|
+
};
|
|
1481
|
+
var scriptStdevSeries = (values, length) => {
|
|
1482
|
+
const window2 = Math.max(1, Math.floor(length));
|
|
1483
|
+
const result = new Array(values.length).fill(null);
|
|
1484
|
+
let sum = 0;
|
|
1485
|
+
let sumSq = 0;
|
|
1486
|
+
let valid = 0;
|
|
1487
|
+
for (let i = 0; i < values.length; i += 1) {
|
|
1488
|
+
const value = values[i];
|
|
1489
|
+
if (value == null || !Number.isFinite(value)) {
|
|
1490
|
+
sum = 0;
|
|
1491
|
+
sumSq = 0;
|
|
1492
|
+
valid = 0;
|
|
1493
|
+
continue;
|
|
1494
|
+
}
|
|
1495
|
+
sum += value;
|
|
1496
|
+
sumSq += value * value;
|
|
1497
|
+
valid += 1;
|
|
1498
|
+
if (valid > window2) {
|
|
1499
|
+
const old = values[i - window2];
|
|
1500
|
+
sum -= old;
|
|
1501
|
+
sumSq -= old * old;
|
|
1502
|
+
valid -= 1;
|
|
1503
|
+
}
|
|
1504
|
+
if (valid === window2) {
|
|
1505
|
+
const mean = sum / window2;
|
|
1506
|
+
result[i] = Math.sqrt(Math.max(0, sumSq / window2 - mean * mean));
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
return result;
|
|
1510
|
+
};
|
|
1511
|
+
var scriptAtrSeries = (bars, length) => {
|
|
1512
|
+
const tr = new Array(bars.length).fill(null);
|
|
1513
|
+
for (let i = 0; i < bars.length; i += 1) {
|
|
1514
|
+
const bar = bars[i];
|
|
1515
|
+
if (i === 0) {
|
|
1516
|
+
tr[i] = bar.h - bar.l;
|
|
1517
|
+
continue;
|
|
1518
|
+
}
|
|
1519
|
+
const prevClose = bars[i - 1].c;
|
|
1520
|
+
tr[i] = Math.max(bar.h - bar.l, Math.abs(bar.h - prevClose), Math.abs(bar.l - prevClose));
|
|
1521
|
+
}
|
|
1522
|
+
return rmaFromValues(tr, Math.max(1, Math.floor(length)));
|
|
1523
|
+
};
|
|
1524
|
+
var SCRIPT_HELPERS = Object.freeze({
|
|
1525
|
+
sma: (values, length) => smaFromValues(values, Math.max(1, Math.floor(length))),
|
|
1526
|
+
ema: (values, length) => emaFromValues(values, Math.max(1, Math.floor(length))),
|
|
1527
|
+
rma: (values, length) => rmaFromValues(values, Math.max(1, Math.floor(length))),
|
|
1528
|
+
highest: (values, length) => scriptRollingExtreme(values, length, "max"),
|
|
1529
|
+
lowest: (values, length) => scriptRollingExtreme(values, length, "min"),
|
|
1530
|
+
change: (values, length = 1) => values.map((value, index) => {
|
|
1531
|
+
const prev = values[index - Math.max(1, Math.floor(length))];
|
|
1532
|
+
return value != null && prev != null ? value - prev : null;
|
|
1533
|
+
}),
|
|
1534
|
+
stdev: scriptStdevSeries,
|
|
1535
|
+
atr: scriptAtrSeries
|
|
1536
|
+
});
|
|
1537
|
+
var SCRIPT_PLOT_PALETTE = ["#2962ff", "#ff6d00", "#26a69a", "#ab47bc", "#ef5350", "#fdd835"];
|
|
1538
|
+
var hashScriptSource = (source) => {
|
|
1539
|
+
let hash = 5381;
|
|
1540
|
+
for (let i = 0; i < source.length; i += 1) {
|
|
1541
|
+
hash = (hash << 5) + hash + source.charCodeAt(i) | 0;
|
|
1542
|
+
}
|
|
1543
|
+
return (hash >>> 0).toString(36);
|
|
1544
|
+
};
|
|
1545
|
+
var compileScriptCompute = (source) => {
|
|
1546
|
+
const factory = new Function(
|
|
1547
|
+
`"use strict";
|
|
1548
|
+
${source}
|
|
1549
|
+
;if (typeof compute !== "function") { throw new Error("Script must define function compute(bars, inputs, hp)"); }
|
|
1550
|
+
return compute;`
|
|
1551
|
+
);
|
|
1552
|
+
return factory();
|
|
1553
|
+
};
|
|
1554
|
+
var drawScriptError = (ctx, renderContext, name, message) => {
|
|
1555
|
+
ctx.save();
|
|
1556
|
+
ctx.font = "11px -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif";
|
|
1557
|
+
ctx.textAlign = "left";
|
|
1558
|
+
ctx.textBaseline = "top";
|
|
1559
|
+
ctx.fillStyle = "#ef5350";
|
|
1560
|
+
const text = `${name}: ${message}`.slice(0, 160);
|
|
1561
|
+
ctx.fillText(text, renderContext.chartLeft + 10, renderContext.chartTop + 24);
|
|
1562
|
+
ctx.restore();
|
|
1563
|
+
};
|
|
1564
|
+
var compileScriptIndicator = (definition) => {
|
|
1565
|
+
const pane = definition.pane ?? "separate";
|
|
1566
|
+
const defaultInputs = { showValueLine: true };
|
|
1567
|
+
for (const input of definition.inputs ?? []) {
|
|
1568
|
+
defaultInputs[input.key] = input.default;
|
|
1569
|
+
}
|
|
1570
|
+
let compiled = null;
|
|
1571
|
+
let compileError = null;
|
|
1572
|
+
try {
|
|
1573
|
+
compiled = compileScriptCompute(definition.source);
|
|
1574
|
+
} catch (error) {
|
|
1575
|
+
compileError = error instanceof Error ? error.message : String(error);
|
|
1576
|
+
}
|
|
1577
|
+
const sourceKey = hashScriptSource(definition.source);
|
|
1578
|
+
const runCompute = (data, inputs) => {
|
|
1579
|
+
if (!compiled) {
|
|
1580
|
+
return { error: compileError ?? "Script failed to compile" };
|
|
1581
|
+
}
|
|
1582
|
+
try {
|
|
1583
|
+
const result = compiled(data, inputs, SCRIPT_HELPERS);
|
|
1584
|
+
if (!result || !Array.isArray(result.plots)) {
|
|
1585
|
+
return { error: "compute() must return { plots: [...] }" };
|
|
1586
|
+
}
|
|
1587
|
+
for (const plot of result.plots) {
|
|
1588
|
+
if (!plot || !Array.isArray(plot.values)) {
|
|
1589
|
+
return { error: "Every plot needs a `values` array" };
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
return { result };
|
|
1593
|
+
} catch (error) {
|
|
1594
|
+
return { error: error instanceof Error ? error.message : String(error) };
|
|
1595
|
+
}
|
|
1596
|
+
};
|
|
1597
|
+
const cachedCompute = (data, inputs) => withCachedComputation(
|
|
1598
|
+
`script|${definition.id}|${sourceKey}|${JSON.stringify(inputs)}`,
|
|
1599
|
+
data,
|
|
1600
|
+
() => runCompute(data, inputs)
|
|
1601
|
+
);
|
|
1602
|
+
const plugin = {
|
|
1603
|
+
id: definition.id,
|
|
1604
|
+
name: definition.name,
|
|
1605
|
+
pane,
|
|
1606
|
+
defaultInputs,
|
|
1607
|
+
draw: (ctx, renderContext, inputs) => {
|
|
1608
|
+
const outcome = cachedCompute(renderContext.data, inputs);
|
|
1609
|
+
if (!outcome.result) {
|
|
1610
|
+
drawScriptError(ctx, renderContext, definition.name, outcome.error ?? "Unknown script error");
|
|
1611
|
+
return pane === "separate" ? { title: `${definition.name} (script error)` } : void 0;
|
|
1612
|
+
}
|
|
1613
|
+
const { plots, range, guides, decimals } = outcome.result;
|
|
1614
|
+
if (pane === "separate") {
|
|
1615
|
+
const seriesList = plots.map((plot, index) => ({
|
|
1616
|
+
values: plot.values,
|
|
1617
|
+
color: plot.color ?? SCRIPT_PLOT_PALETTE[index % SCRIPT_PLOT_PALETTE.length],
|
|
1618
|
+
...plot.width !== void 0 ? { width: plot.width } : {},
|
|
1619
|
+
...plot.title ? { label: plot.title } : {},
|
|
1620
|
+
...plot.style === "histogram" ? { histogram: true } : {},
|
|
1621
|
+
...plot.negativeColor ? { negativeColor: plot.negativeColor } : {}
|
|
1622
|
+
}));
|
|
1623
|
+
return drawSeparateMultiSeries(ctx, renderContext, seriesList, {
|
|
1624
|
+
title: definition.name,
|
|
1625
|
+
...range?.min !== void 0 ? { minOverride: range.min } : {},
|
|
1626
|
+
...range?.max !== void 0 ? { maxOverride: range.max } : {},
|
|
1627
|
+
...guides && guides.length > 0 ? { guideLines: guides } : {},
|
|
1628
|
+
...decimals !== void 0 ? { decimals } : {},
|
|
1629
|
+
valueLines: inputs.showValueLine !== false
|
|
1630
|
+
});
|
|
1631
|
+
}
|
|
1632
|
+
for (let index = 0; index < plots.length; index += 1) {
|
|
1633
|
+
const plot = plots[index];
|
|
1634
|
+
if (plot.style === "histogram") continue;
|
|
1635
|
+
drawOverlaySeries(
|
|
1636
|
+
ctx,
|
|
1637
|
+
renderContext,
|
|
1638
|
+
plot.values,
|
|
1639
|
+
plot.color ?? SCRIPT_PLOT_PALETTE[index % SCRIPT_PLOT_PALETTE.length],
|
|
1640
|
+
plot.width ?? 2
|
|
1641
|
+
);
|
|
1642
|
+
}
|
|
1643
|
+
return void 0;
|
|
1644
|
+
},
|
|
1645
|
+
...pane === "overlay" ? {
|
|
1646
|
+
getAutoscaleRange: (data, startIndex, endIndex, inputs, skipIndex) => {
|
|
1647
|
+
const outcome = cachedCompute(data, inputs);
|
|
1648
|
+
if (!outcome.result) return null;
|
|
1649
|
+
const lineSeries = outcome.result.plots.filter((plot) => plot.style !== "histogram").map((plot) => plot.values);
|
|
1650
|
+
if (lineSeries.length === 0) return null;
|
|
1651
|
+
return rangeOfSeries(lineSeries, startIndex, endIndex, skipIndex);
|
|
1652
|
+
}
|
|
1653
|
+
} : {}
|
|
1654
|
+
};
|
|
1655
|
+
if (definition.paneHeightRatio !== void 0) {
|
|
1656
|
+
plugin.paneHeightRatio = definition.paneHeightRatio;
|
|
1657
|
+
}
|
|
1658
|
+
return plugin;
|
|
1659
|
+
};
|
|
1471
1660
|
var computeMacd = (data, fast, slow, signalLength) => {
|
|
1472
1661
|
const fastEma = computeEmaSeries(data, fast, "close");
|
|
1473
1662
|
const slowEma = computeEmaSeries(data, slow, "close");
|
|
@@ -2519,7 +2708,7 @@ function createChart(element, options = {}) {
|
|
|
2519
2708
|
return;
|
|
2520
2709
|
}
|
|
2521
2710
|
const tickerOpts = mergedOptions.tickerLine ?? DEFAULT_OPTIONS.tickerLine;
|
|
2522
|
-
const speed = clamp(tickerOpts.smoothingSpeed ??
|
|
2711
|
+
const speed = clamp(tickerOpts.smoothingSpeed ?? 16, 1, 60);
|
|
2523
2712
|
const dt = 1 / 60;
|
|
2524
2713
|
const lerp = 1 - Math.exp(-speed * dt);
|
|
2525
2714
|
smoothedTickerPrice += priceDiff * lerp;
|
|
@@ -2549,6 +2738,29 @@ function createChart(element, options = {}) {
|
|
|
2549
2738
|
smoothingRafId = requestAnimationFrame(tickerSmoothingLoop);
|
|
2550
2739
|
}
|
|
2551
2740
|
};
|
|
2741
|
+
let countdownTimerId = null;
|
|
2742
|
+
const countdownRepaintNeeded = () => {
|
|
2743
|
+
if (data.length === 0) {
|
|
2744
|
+
return false;
|
|
2745
|
+
}
|
|
2746
|
+
const ticker = mergedOptions.tickerLine ?? DEFAULT_OPTIONS.tickerLine;
|
|
2747
|
+
const labels = mergedOptions.labels ?? DEFAULT_OPTIONS.labels;
|
|
2748
|
+
return Boolean(ticker.showCountdownInLabel) || Boolean(labels.visible && labels.showCountdownToBarClose);
|
|
2749
|
+
};
|
|
2750
|
+
const armCountdownHeartbeat = () => {
|
|
2751
|
+
if (countdownTimerId !== null || !countdownRepaintNeeded()) {
|
|
2752
|
+
return;
|
|
2753
|
+
}
|
|
2754
|
+
const delay = 1e3 - Date.now() % 1e3 + 15;
|
|
2755
|
+
countdownTimerId = setTimeout(() => {
|
|
2756
|
+
countdownTimerId = null;
|
|
2757
|
+
if (!countdownRepaintNeeded()) {
|
|
2758
|
+
return;
|
|
2759
|
+
}
|
|
2760
|
+
scheduleDraw({ updateAutoScale: false });
|
|
2761
|
+
armCountdownHeartbeat();
|
|
2762
|
+
}, delay);
|
|
2763
|
+
};
|
|
2552
2764
|
const canvas = document.createElement("canvas");
|
|
2553
2765
|
const ctx = canvas.getContext("2d");
|
|
2554
2766
|
if (!ctx) {
|
|
@@ -3474,6 +3686,7 @@ function createChart(element, options = {}) {
|
|
|
3474
3686
|
let pendingDrawUpdateAutoScale = false;
|
|
3475
3687
|
let pendingDrawOverlayOnly = true;
|
|
3476
3688
|
const scheduleDraw = (options2 = {}) => {
|
|
3689
|
+
armCountdownHeartbeat();
|
|
3477
3690
|
const overlayOnly = options2.overlayOnly ?? false;
|
|
3478
3691
|
pendingDrawOverlayOnly = pendingDrawOverlayOnly && overlayOnly;
|
|
3479
3692
|
pendingDrawUpdateAutoScale = pendingDrawUpdateAutoScale || !overlayOnly && (options2.updateAutoScale ?? true);
|
|
@@ -7731,6 +7944,10 @@ function createChart(element, options = {}) {
|
|
|
7731
7944
|
cancelAnimationFrame(smoothingRafId);
|
|
7732
7945
|
smoothingRafId = null;
|
|
7733
7946
|
}
|
|
7947
|
+
if (countdownTimerId !== null) {
|
|
7948
|
+
clearTimeout(countdownTimerId);
|
|
7949
|
+
countdownTimerId = null;
|
|
7950
|
+
}
|
|
7734
7951
|
if (drawRafId !== null) {
|
|
7735
7952
|
cancelAnimationFrame(drawRafId);
|
|
7736
7953
|
drawRafId = null;
|
|
@@ -7752,6 +7969,7 @@ function createChart(element, options = {}) {
|
|
|
7752
7969
|
element.innerHTML = "";
|
|
7753
7970
|
};
|
|
7754
7971
|
draw();
|
|
7972
|
+
armCountdownHeartbeat();
|
|
7755
7973
|
return {
|
|
7756
7974
|
updateOptions,
|
|
7757
7975
|
setChartType,
|
|
@@ -7821,5 +8039,6 @@ function createChart(element, options = {}) {
|
|
|
7821
8039
|
export {
|
|
7822
8040
|
FIB_DEFAULT_PALETTE,
|
|
7823
8041
|
POSITION_DEFAULT_COLORS,
|
|
8042
|
+
compileScriptIndicator,
|
|
7824
8043
|
createChart
|
|
7825
8044
|
};
|
package/docs/API.md
CHANGED
|
@@ -543,3 +543,67 @@ Use `getDrawings()` / `setDrawings()` for persistence.
|
|
|
543
543
|
- `setIndicators(indicators: IndicatorInstanceOptions[]): void`
|
|
544
544
|
- `resize(width?: number, height?: number): void`
|
|
545
545
|
- `destroy(): void`
|
|
546
|
+
|
|
547
|
+
---
|
|
548
|
+
|
|
549
|
+
## Script Indicators (user-authored)
|
|
550
|
+
|
|
551
|
+
`compileScriptIndicator(definition: ScriptIndicatorDefinition): IndicatorPlugin` — compiles a
|
|
552
|
+
user-authored "Hyperprop Script" into a regular indicator plugin. Register the result with
|
|
553
|
+
`chart.registerIndicator(...)` and add it with `chart.addIndicator(definition.id)`. Separate-pane
|
|
554
|
+
scripts automatically get the shared grid, legend, live values, value lines, drag-resize and the
|
|
555
|
+
hover controls (eye / gear / source / delete). Overlay scripts participate in price autoscale.
|
|
556
|
+
|
|
557
|
+
```ts
|
|
558
|
+
import { createChart, compileScriptIndicator } from "hyperprop-charting-library";
|
|
559
|
+
|
|
560
|
+
const plugin = compileScriptIndicator({
|
|
561
|
+
id: "script:my-rsi",
|
|
562
|
+
name: "My RSI",
|
|
563
|
+
pane: "separate", // or "overlay"
|
|
564
|
+
inputs: [
|
|
565
|
+
{ key: "length", label: "Length", type: "number", default: 14 },
|
|
566
|
+
{ key: "color", label: "Color", type: "color", default: "#ab47bc" }
|
|
567
|
+
],
|
|
568
|
+
source: `
|
|
569
|
+
function compute(bars, inputs, hp) {
|
|
570
|
+
const closes = bars.map(b => b.c);
|
|
571
|
+
const gains = closes.map((c, i) => i === 0 ? null : Math.max(0, c - closes[i - 1]));
|
|
572
|
+
const losses = closes.map((c, i) => i === 0 ? null : Math.max(0, closes[i - 1] - c));
|
|
573
|
+
const avgGain = hp.rma(gains, inputs.length);
|
|
574
|
+
const avgLoss = hp.rma(losses, inputs.length);
|
|
575
|
+
const rsi = avgGain.map((g, i) => {
|
|
576
|
+
const l = avgLoss[i];
|
|
577
|
+
if (g == null || l == null) return null;
|
|
578
|
+
return l === 0 ? 100 : 100 - 100 / (1 + g / l);
|
|
579
|
+
});
|
|
580
|
+
return {
|
|
581
|
+
plots: [{ title: "RSI", values: rsi, color: inputs.color }],
|
|
582
|
+
range: { min: 0, max: 100 },
|
|
583
|
+
guides: [30, 70]
|
|
584
|
+
};
|
|
585
|
+
}`
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
chart.registerIndicator(plugin);
|
|
589
|
+
chart.addIndicator("script:my-rsi");
|
|
590
|
+
```
|
|
591
|
+
|
|
592
|
+
Script contract:
|
|
593
|
+
|
|
594
|
+
- The source must define `function compute(bars, inputs, hp)`.
|
|
595
|
+
- `bars`: `Array<{ time: Date, o, h, l, c, v? }>` — the full series.
|
|
596
|
+
- `inputs`: resolved input values (defaults merged with per-instance overrides). `showValueLine`
|
|
597
|
+
is injected automatically and honored by separate panes.
|
|
598
|
+
- `hp`: TA helpers, all `(values: Array<number|null>, length) => Array<number|null>` unless noted:
|
|
599
|
+
`sma`, `ema`, `rma`, `highest`, `lowest`, `stdev`, `change(values, length = 1)`,
|
|
600
|
+
`atr(bars, length)`.
|
|
601
|
+
- Return `{ plots, range?, guides?, decimals? }`:
|
|
602
|
+
- `plots`: `Array<{ title?, values, color?, width?, style?: "line" | "histogram", negativeColor? }>`
|
|
603
|
+
- `range`: `{ min?, max? }` fixes the pane scale (e.g. `0..100` for oscillators).
|
|
604
|
+
- `guides`: dashed horizontal guide levels (e.g. `[30, 70]`).
|
|
605
|
+
- `decimals`: axis/legend decimal places.
|
|
606
|
+
|
|
607
|
+
Errors never break the chart: compile or runtime errors render as an inline red message in the
|
|
608
|
+
pane. Scripts are plain JavaScript executed in the page context — treat them like any other code
|
|
609
|
+
you'd paste into your app (only run scripts you trust).
|