hyperprop-charting-library 0.1.127 → 0.1.129
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 +1297 -16
- package/dist/hyperprop-charting-library.d.ts +26 -1
- package/dist/hyperprop-charting-library.js +1297 -16
- package/dist/index.cjs +1297 -16
- package/dist/index.d.cts +26 -1
- package/dist/index.d.ts +26 -1
- package/dist/index.js +1297 -16
- package/docs/API.md +26 -0
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -215,6 +215,11 @@ var DEFAULT_OPTIONS = {
|
|
|
215
215
|
tickSize: 0,
|
|
216
216
|
candleColorMode: "openClose",
|
|
217
217
|
candleColorEpsilon: -1,
|
|
218
|
+
chartType: "candles",
|
|
219
|
+
lineColor: "#2962ff",
|
|
220
|
+
lineWidth: 2,
|
|
221
|
+
areaFillOpacity: 0.12,
|
|
222
|
+
baselinePrice: null,
|
|
218
223
|
// 0 = deterministic scale (lightweight-charts behavior): the y-range is a
|
|
219
224
|
// pure function of the visible data, so it can never drift or "breathe".
|
|
220
225
|
// Values > 0 re-enable eased contraction for hosts that prefer it.
|
|
@@ -623,6 +628,79 @@ var withCachedSeries = (key, data, compute) => {
|
|
|
623
628
|
builtInSeriesCache.set(key, { fingerprint, values });
|
|
624
629
|
return values;
|
|
625
630
|
};
|
|
631
|
+
var builtInComputationCache = /* @__PURE__ */ new Map();
|
|
632
|
+
var withCachedComputation = (key, data, compute) => {
|
|
633
|
+
const fingerprint = getSeriesFingerprint(data);
|
|
634
|
+
const existing = builtInComputationCache.get(key);
|
|
635
|
+
if (existing && existing.fingerprint === fingerprint) {
|
|
636
|
+
return existing.value;
|
|
637
|
+
}
|
|
638
|
+
const value = compute();
|
|
639
|
+
builtInComputationCache.set(key, { fingerprint, value });
|
|
640
|
+
return value;
|
|
641
|
+
};
|
|
642
|
+
var rollingExtremeSeries = (values, length, mode) => {
|
|
643
|
+
const count = values.length;
|
|
644
|
+
const result = new Array(count).fill(null);
|
|
645
|
+
const deque = [];
|
|
646
|
+
const better = (a, b) => mode === "max" ? a >= b : a <= b;
|
|
647
|
+
for (let i = 0; i < count; i += 1) {
|
|
648
|
+
const value = values[i];
|
|
649
|
+
while (deque.length > 0 && better(value, values[deque[deque.length - 1]])) {
|
|
650
|
+
deque.pop();
|
|
651
|
+
}
|
|
652
|
+
deque.push(i);
|
|
653
|
+
if (deque[0] <= i - length) {
|
|
654
|
+
deque.shift();
|
|
655
|
+
}
|
|
656
|
+
if (i >= length - 1) {
|
|
657
|
+
result[i] = values[deque[0]];
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
return result;
|
|
661
|
+
};
|
|
662
|
+
var smaFromValues = (values, length) => {
|
|
663
|
+
const result = new Array(values.length).fill(null);
|
|
664
|
+
let sum = 0;
|
|
665
|
+
let valid = 0;
|
|
666
|
+
for (let i = 0; i < values.length; i += 1) {
|
|
667
|
+
const value = values[i];
|
|
668
|
+
if (value != null && Number.isFinite(value)) {
|
|
669
|
+
sum += value;
|
|
670
|
+
valid += 1;
|
|
671
|
+
} else {
|
|
672
|
+
sum = 0;
|
|
673
|
+
valid = 0;
|
|
674
|
+
continue;
|
|
675
|
+
}
|
|
676
|
+
if (valid > length) {
|
|
677
|
+
sum -= values[i - length];
|
|
678
|
+
valid = length;
|
|
679
|
+
}
|
|
680
|
+
if (valid === length) {
|
|
681
|
+
result[i] = sum / length;
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
return result;
|
|
685
|
+
};
|
|
686
|
+
var emaFromValues = (values, length) => {
|
|
687
|
+
const result = new Array(values.length).fill(null);
|
|
688
|
+
const alpha = 2 / (length + 1);
|
|
689
|
+
let prev = null;
|
|
690
|
+
let seen = 0;
|
|
691
|
+
for (let i = 0; i < values.length; i += 1) {
|
|
692
|
+
const value = values[i];
|
|
693
|
+
if (value == null || !Number.isFinite(value)) {
|
|
694
|
+
continue;
|
|
695
|
+
}
|
|
696
|
+
prev = prev === null ? value : alpha * value + (1 - alpha) * prev;
|
|
697
|
+
seen += 1;
|
|
698
|
+
if (seen >= length) {
|
|
699
|
+
result[i] = prev;
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
return result;
|
|
703
|
+
};
|
|
626
704
|
var drawOverlaySeries = (ctx, renderContext, values, color, width) => {
|
|
627
705
|
if (!renderContext.yFromPrice) return;
|
|
628
706
|
const yFromPrice = renderContext.yFromPrice;
|
|
@@ -772,6 +850,19 @@ var drawSeparateSeries = (ctx, renderContext, values, color, width, minOverride,
|
|
|
772
850
|
break;
|
|
773
851
|
}
|
|
774
852
|
}
|
|
853
|
+
if (options.valueLine && latestValue !== null && latestValue >= minValue && latestValue <= maxValue) {
|
|
854
|
+
ctx.save();
|
|
855
|
+
ctx.lineWidth = 1;
|
|
856
|
+
ctx.setLineDash([3, 3]);
|
|
857
|
+
ctx.globalAlpha = 0.65;
|
|
858
|
+
ctx.strokeStyle = color;
|
|
859
|
+
const y = yFromValue(latestValue);
|
|
860
|
+
ctx.beginPath();
|
|
861
|
+
ctx.moveTo(renderContext.chartLeft, y);
|
|
862
|
+
ctx.lineTo(renderContext.chartRight, y);
|
|
863
|
+
ctx.stroke();
|
|
864
|
+
ctx.restore();
|
|
865
|
+
}
|
|
775
866
|
const decimals = options.decimals ?? 2;
|
|
776
867
|
const formatValue = (value) => value.toFixed(decimals);
|
|
777
868
|
const axisTicks = options.axisTicks ?? guideLines;
|
|
@@ -1099,7 +1190,7 @@ var BUILTIN_STDDEV_INDICATOR = {
|
|
|
1099
1190
|
name: "StdDev",
|
|
1100
1191
|
pane: "separate",
|
|
1101
1192
|
paneHeightRatio: 0.16,
|
|
1102
|
-
defaultInputs: { length: 20, source: "close", color: "#f97316", width: 2 },
|
|
1193
|
+
defaultInputs: { length: 20, source: "close", color: "#f97316", width: 2, showValueLine: false },
|
|
1103
1194
|
draw: (ctx, renderContext, inputs) => {
|
|
1104
1195
|
const length = clampIndicatorLength(inputs.length, 20);
|
|
1105
1196
|
const values = withCachedSeries(
|
|
@@ -1109,7 +1200,8 @@ var BUILTIN_STDDEV_INDICATOR = {
|
|
|
1109
1200
|
);
|
|
1110
1201
|
return drawSeparateSeries(ctx, renderContext, values, inputs.color ?? "#f97316", Number(inputs.width) || 2, void 0, void 0, void 0, {
|
|
1111
1202
|
title: `StdDev ${length}`,
|
|
1112
|
-
decimals: 2
|
|
1203
|
+
decimals: 2,
|
|
1204
|
+
valueLine: inputs.showValueLine === true
|
|
1113
1205
|
});
|
|
1114
1206
|
}
|
|
1115
1207
|
};
|
|
@@ -1118,13 +1210,14 @@ var BUILTIN_ATR_INDICATOR = {
|
|
|
1118
1210
|
name: "ATR",
|
|
1119
1211
|
pane: "separate",
|
|
1120
1212
|
paneHeightRatio: 0.16,
|
|
1121
|
-
defaultInputs: { length: 14, color: "#eab308", width: 2 },
|
|
1213
|
+
defaultInputs: { length: 14, color: "#eab308", width: 2, showValueLine: false },
|
|
1122
1214
|
draw: (ctx, renderContext, inputs) => {
|
|
1123
1215
|
const length = clampIndicatorLength(inputs.length, 14);
|
|
1124
1216
|
const values = withCachedSeries(`atr|${length}`, renderContext.data, () => computeAtrSeries(renderContext.data, length));
|
|
1125
1217
|
return drawSeparateSeries(ctx, renderContext, values, inputs.color ?? "#eab308", Number(inputs.width) || 2, void 0, void 0, void 0, {
|
|
1126
1218
|
title: `ATR ${length}`,
|
|
1127
|
-
decimals: 2
|
|
1219
|
+
decimals: 2,
|
|
1220
|
+
valueLine: inputs.showValueLine === true
|
|
1128
1221
|
});
|
|
1129
1222
|
}
|
|
1130
1223
|
};
|
|
@@ -1140,7 +1233,8 @@ var BUILTIN_RSI_INDICATOR = {
|
|
|
1140
1233
|
showLegend: true,
|
|
1141
1234
|
showValueLabel: true,
|
|
1142
1235
|
showGuideLines: true,
|
|
1143
|
-
showScaleLabels: true
|
|
1236
|
+
showScaleLabels: true,
|
|
1237
|
+
showValueLine: false
|
|
1144
1238
|
},
|
|
1145
1239
|
draw: (ctx, renderContext, inputs) => {
|
|
1146
1240
|
const length = clampIndicatorLength(inputs.length, 14);
|
|
@@ -1163,11 +1257,1006 @@ var BUILTIN_RSI_INDICATOR = {
|
|
|
1163
1257
|
valueLabelColor: "#9E9E9E",
|
|
1164
1258
|
valueLabelBackgroundColor: "#9E9E9E",
|
|
1165
1259
|
valueLabelTextColor: "#0f172a",
|
|
1166
|
-
showLegend: inputs.showLegend !== false
|
|
1260
|
+
showLegend: inputs.showLegend !== false,
|
|
1261
|
+
valueLine: inputs.showValueLine === true
|
|
1262
|
+
}
|
|
1263
|
+
);
|
|
1264
|
+
}
|
|
1265
|
+
};
|
|
1266
|
+
var rmaFromValues = (values, length) => {
|
|
1267
|
+
const result = new Array(values.length).fill(null);
|
|
1268
|
+
let prev = null;
|
|
1269
|
+
let seedSum = 0;
|
|
1270
|
+
let seedCount = 0;
|
|
1271
|
+
for (let i = 0; i < values.length; i += 1) {
|
|
1272
|
+
const value = values[i];
|
|
1273
|
+
if (value == null || !Number.isFinite(value)) continue;
|
|
1274
|
+
if (prev === null) {
|
|
1275
|
+
seedSum += value;
|
|
1276
|
+
seedCount += 1;
|
|
1277
|
+
if (seedCount === length) {
|
|
1278
|
+
prev = seedSum / length;
|
|
1279
|
+
result[i] = prev;
|
|
1280
|
+
}
|
|
1281
|
+
continue;
|
|
1282
|
+
}
|
|
1283
|
+
prev = (prev * (length - 1) + value) / length;
|
|
1284
|
+
result[i] = prev;
|
|
1285
|
+
}
|
|
1286
|
+
return result;
|
|
1287
|
+
};
|
|
1288
|
+
var formatCompactNumber = (value) => {
|
|
1289
|
+
const abs = Math.abs(value);
|
|
1290
|
+
if (abs >= 1e9) return `${(value / 1e9).toFixed(2)}B`;
|
|
1291
|
+
if (abs >= 1e6) return `${(value / 1e6).toFixed(2)}M`;
|
|
1292
|
+
if (abs >= 1e3) return `${(value / 1e3).toFixed(1)}K`;
|
|
1293
|
+
return value.toFixed(0);
|
|
1294
|
+
};
|
|
1295
|
+
var drawSeparateMultiSeries = (ctx, renderContext, seriesList, options = {}) => {
|
|
1296
|
+
let min = Number.POSITIVE_INFINITY;
|
|
1297
|
+
let max = Number.NEGATIVE_INFINITY;
|
|
1298
|
+
for (const spec of seriesList) {
|
|
1299
|
+
for (let index = renderContext.startIndex; index <= renderContext.endIndex; index += 1) {
|
|
1300
|
+
const value = spec.values[index];
|
|
1301
|
+
if (value == null || !Number.isFinite(value)) continue;
|
|
1302
|
+
if (value < min) min = value;
|
|
1303
|
+
if (value > max) max = value;
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
if (min > max) return void 0;
|
|
1307
|
+
if (options.includeZero || seriesList.some((spec) => spec.histogram)) {
|
|
1308
|
+
min = Math.min(min, 0);
|
|
1309
|
+
max = Math.max(max, 0);
|
|
1310
|
+
}
|
|
1311
|
+
const minValue = options.minOverride ?? min;
|
|
1312
|
+
const maxValue = options.maxOverride ?? max;
|
|
1313
|
+
const range = maxValue - minValue || 1;
|
|
1314
|
+
const yFromValue = (value) => {
|
|
1315
|
+
const ratio = (value - minValue) / range;
|
|
1316
|
+
return renderContext.chartBottom - ratio * renderContext.chartHeight;
|
|
1317
|
+
};
|
|
1318
|
+
if (options.guideLines && options.guideLines.length > 0) {
|
|
1319
|
+
ctx.save();
|
|
1320
|
+
ctx.strokeStyle = "rgba(148,163,184,0.35)";
|
|
1321
|
+
ctx.lineWidth = 1;
|
|
1322
|
+
ctx.setLineDash([4, 4]);
|
|
1323
|
+
for (const guide of options.guideLines) {
|
|
1324
|
+
const y = yFromValue(guide);
|
|
1325
|
+
ctx.beginPath();
|
|
1326
|
+
ctx.moveTo(renderContext.chartLeft, y);
|
|
1327
|
+
ctx.lineTo(renderContext.chartRight, y);
|
|
1328
|
+
ctx.stroke();
|
|
1329
|
+
}
|
|
1330
|
+
ctx.restore();
|
|
1331
|
+
}
|
|
1332
|
+
for (const spec of seriesList) {
|
|
1333
|
+
if (!spec.histogram) continue;
|
|
1334
|
+
const zeroY = yFromValue(0);
|
|
1335
|
+
const barWidth = Math.max(
|
|
1336
|
+
1,
|
|
1337
|
+
Math.min(Math.max(1, renderContext.candleSpacing - 1), Math.floor(renderContext.candleSpacing * 0.7))
|
|
1338
|
+
);
|
|
1339
|
+
const positive = new Path2D();
|
|
1340
|
+
const negative = new Path2D();
|
|
1341
|
+
for (let index = renderContext.startIndex; index <= renderContext.endIndex; index += 1) {
|
|
1342
|
+
const value = spec.values[index];
|
|
1343
|
+
if (value == null || !Number.isFinite(value)) continue;
|
|
1344
|
+
const x = Math.round(renderContext.xFromIndex(index) - barWidth / 2);
|
|
1345
|
+
const y = yFromValue(value);
|
|
1346
|
+
const top = Math.min(y, zeroY);
|
|
1347
|
+
const height = Math.max(1, Math.abs(y - zeroY));
|
|
1348
|
+
(value >= 0 ? positive : negative).rect(x, top, barWidth, height);
|
|
1349
|
+
}
|
|
1350
|
+
ctx.save();
|
|
1351
|
+
ctx.globalAlpha = 0.85;
|
|
1352
|
+
ctx.fillStyle = spec.color;
|
|
1353
|
+
ctx.fill(positive);
|
|
1354
|
+
ctx.fillStyle = spec.negativeColor ?? spec.color;
|
|
1355
|
+
ctx.fill(negative);
|
|
1356
|
+
ctx.restore();
|
|
1357
|
+
}
|
|
1358
|
+
for (const spec of seriesList) {
|
|
1359
|
+
if (spec.histogram) continue;
|
|
1360
|
+
ctx.save();
|
|
1361
|
+
ctx.strokeStyle = spec.color;
|
|
1362
|
+
ctx.lineWidth = Math.max(1, spec.width ?? 2);
|
|
1363
|
+
ctx.setLineDash([]);
|
|
1364
|
+
let drawing = false;
|
|
1365
|
+
for (let index = renderContext.startIndex; index <= renderContext.endIndex; index += 1) {
|
|
1366
|
+
const value = spec.values[index];
|
|
1367
|
+
if (!Number.isFinite(value ?? Number.NaN)) {
|
|
1368
|
+
drawing = false;
|
|
1369
|
+
continue;
|
|
1370
|
+
}
|
|
1371
|
+
const x = renderContext.xFromIndex(index);
|
|
1372
|
+
const y = yFromValue(value);
|
|
1373
|
+
if (!drawing) {
|
|
1374
|
+
ctx.beginPath();
|
|
1375
|
+
ctx.moveTo(x, y);
|
|
1376
|
+
drawing = true;
|
|
1377
|
+
} else {
|
|
1378
|
+
ctx.lineTo(x, y);
|
|
1379
|
+
}
|
|
1380
|
+
const nextValue = spec.values[index + 1];
|
|
1381
|
+
if (!Number.isFinite(nextValue ?? Number.NaN) || index === renderContext.endIndex) {
|
|
1382
|
+
ctx.stroke();
|
|
1383
|
+
drawing = false;
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
ctx.restore();
|
|
1387
|
+
}
|
|
1388
|
+
const decimals = options.decimals ?? 2;
|
|
1389
|
+
const formatValue = options.format ?? ((value) => value.toFixed(decimals));
|
|
1390
|
+
const latestOf = (values) => {
|
|
1391
|
+
for (let index = values.length - 1; index >= 0; index -= 1) {
|
|
1392
|
+
const value = values[index];
|
|
1393
|
+
if (Number.isFinite(value ?? Number.NaN)) return value;
|
|
1394
|
+
}
|
|
1395
|
+
return null;
|
|
1396
|
+
};
|
|
1397
|
+
if (options.valueLines) {
|
|
1398
|
+
ctx.save();
|
|
1399
|
+
ctx.lineWidth = 1;
|
|
1400
|
+
ctx.setLineDash([3, 3]);
|
|
1401
|
+
ctx.globalAlpha = 0.65;
|
|
1402
|
+
for (const spec of seriesList) {
|
|
1403
|
+
if (spec.histogram) continue;
|
|
1404
|
+
const latest = latestOf(spec.values);
|
|
1405
|
+
if (latest === null || latest < minValue || latest > maxValue) continue;
|
|
1406
|
+
const y = yFromValue(latest);
|
|
1407
|
+
ctx.strokeStyle = spec.color;
|
|
1408
|
+
ctx.beginPath();
|
|
1409
|
+
ctx.moveTo(renderContext.chartLeft, y);
|
|
1410
|
+
ctx.lineTo(renderContext.chartRight, y);
|
|
1411
|
+
ctx.stroke();
|
|
1412
|
+
}
|
|
1413
|
+
ctx.restore();
|
|
1414
|
+
}
|
|
1415
|
+
const paneInfo = {
|
|
1416
|
+
...options.title ? { title: options.title } : {},
|
|
1417
|
+
axis: {
|
|
1418
|
+
min: minValue,
|
|
1419
|
+
max: maxValue,
|
|
1420
|
+
...options.axisTicks ? { ticks: options.axisTicks } : options.guideLines ? { ticks: options.guideLines } : {},
|
|
1421
|
+
decimals,
|
|
1422
|
+
format: formatValue
|
|
1423
|
+
}
|
|
1424
|
+
};
|
|
1425
|
+
if (options.guideLines) {
|
|
1426
|
+
paneInfo.guideLines = options.guideLines.map((value) => ({
|
|
1427
|
+
value,
|
|
1428
|
+
label: formatValue(value),
|
|
1429
|
+
style: "dotted"
|
|
1430
|
+
}));
|
|
1431
|
+
}
|
|
1432
|
+
const legendValues = [];
|
|
1433
|
+
for (const spec of seriesList) {
|
|
1434
|
+
const latest = latestOf(spec.values);
|
|
1435
|
+
if (latest === null) continue;
|
|
1436
|
+
legendValues.push({
|
|
1437
|
+
...spec.label ? { label: spec.label } : {},
|
|
1438
|
+
value: latest,
|
|
1439
|
+
text: formatValue(latest),
|
|
1440
|
+
color: spec.color
|
|
1441
|
+
});
|
|
1442
|
+
}
|
|
1443
|
+
if (legendValues.length > 0) {
|
|
1444
|
+
paneInfo.legendValues = legendValues;
|
|
1445
|
+
}
|
|
1446
|
+
const labelSpec = seriesList[options.valueLabelSeriesIndex ?? 0];
|
|
1447
|
+
const labelLatest = labelSpec ? latestOf(labelSpec.values) : null;
|
|
1448
|
+
if (labelSpec && labelLatest !== null) {
|
|
1449
|
+
paneInfo.valueLabels = [
|
|
1450
|
+
{
|
|
1451
|
+
value: labelLatest,
|
|
1452
|
+
text: formatValue(labelLatest),
|
|
1453
|
+
color: labelSpec.color,
|
|
1454
|
+
backgroundColor: labelSpec.color,
|
|
1455
|
+
textColor: "#0f172a"
|
|
1456
|
+
}
|
|
1457
|
+
];
|
|
1458
|
+
}
|
|
1459
|
+
return paneInfo;
|
|
1460
|
+
};
|
|
1461
|
+
var computeMacd = (data, fast, slow, signalLength) => {
|
|
1462
|
+
const fastEma = computeEmaSeries(data, fast, "close");
|
|
1463
|
+
const slowEma = computeEmaSeries(data, slow, "close");
|
|
1464
|
+
const macd = fastEma.map((value, idx) => {
|
|
1465
|
+
const slowValue = slowEma[idx];
|
|
1466
|
+
return value == null || slowValue == null ? null : value - slowValue;
|
|
1467
|
+
});
|
|
1468
|
+
const signal = emaFromValues(macd, signalLength);
|
|
1469
|
+
const hist = macd.map((value, idx) => {
|
|
1470
|
+
const signalValue = signal[idx];
|
|
1471
|
+
return value == null || signalValue == null ? null : value - signalValue;
|
|
1472
|
+
});
|
|
1473
|
+
return { macd, signal, hist };
|
|
1474
|
+
};
|
|
1475
|
+
var computeStochastic = (data, kLength, kSmoothing, dLength) => {
|
|
1476
|
+
const highestHigh = rollingExtremeSeries(data.map((point) => point.h), kLength, "max");
|
|
1477
|
+
const lowestLow = rollingExtremeSeries(data.map((point) => point.l), kLength, "min");
|
|
1478
|
+
const rawK = data.map((point, idx) => {
|
|
1479
|
+
const hh = highestHigh[idx];
|
|
1480
|
+
const ll = lowestLow[idx];
|
|
1481
|
+
if (hh == null || ll == null) return null;
|
|
1482
|
+
const range = hh - ll;
|
|
1483
|
+
return range === 0 ? 50 : (point.c - ll) / range * 100;
|
|
1484
|
+
});
|
|
1485
|
+
const k = kSmoothing > 1 ? smaFromValues(rawK, kSmoothing) : rawK;
|
|
1486
|
+
const d = smaFromValues(k, dLength);
|
|
1487
|
+
return { k, d };
|
|
1488
|
+
};
|
|
1489
|
+
var computeStochRsi = (data, rsiLength, stochLength, kSmoothing, dSmoothing) => {
|
|
1490
|
+
const rsi = computeRsiSeries(data, rsiLength);
|
|
1491
|
+
const finiteRsi = rsi.map((value) => value == null ? Number.NaN : value);
|
|
1492
|
+
const highest = rollingExtremeSeries(finiteRsi, stochLength, "max");
|
|
1493
|
+
const lowest = rollingExtremeSeries(finiteRsi, stochLength, "min");
|
|
1494
|
+
const rawK = rsi.map((value, idx) => {
|
|
1495
|
+
const hh = highest[idx];
|
|
1496
|
+
const ll = lowest[idx];
|
|
1497
|
+
if (value == null || hh == null || ll == null || !Number.isFinite(hh) || !Number.isFinite(ll)) return null;
|
|
1498
|
+
const range = hh - ll;
|
|
1499
|
+
return range === 0 ? 50 : (value - ll) / range * 100;
|
|
1500
|
+
});
|
|
1501
|
+
const k = smaFromValues(rawK, Math.max(1, kSmoothing));
|
|
1502
|
+
const d = smaFromValues(k, Math.max(1, dSmoothing));
|
|
1503
|
+
return { k, d };
|
|
1504
|
+
};
|
|
1505
|
+
var computeDmi = (data, length) => {
|
|
1506
|
+
const count = data.length;
|
|
1507
|
+
const plusDm = new Array(count).fill(null);
|
|
1508
|
+
const minusDm = new Array(count).fill(null);
|
|
1509
|
+
const trueRange = new Array(count).fill(null);
|
|
1510
|
+
for (let i = 1; i < count; i += 1) {
|
|
1511
|
+
const point = data[i];
|
|
1512
|
+
const prev = data[i - 1];
|
|
1513
|
+
const upMove = point.h - prev.h;
|
|
1514
|
+
const downMove = prev.l - point.l;
|
|
1515
|
+
plusDm[i] = upMove > downMove && upMove > 0 ? upMove : 0;
|
|
1516
|
+
minusDm[i] = downMove > upMove && downMove > 0 ? downMove : 0;
|
|
1517
|
+
trueRange[i] = Math.max(point.h - point.l, Math.abs(point.h - prev.c), Math.abs(point.l - prev.c));
|
|
1518
|
+
}
|
|
1519
|
+
const smoothPlus = rmaFromValues(plusDm, length);
|
|
1520
|
+
const smoothMinus = rmaFromValues(minusDm, length);
|
|
1521
|
+
const smoothTr = rmaFromValues(trueRange, length);
|
|
1522
|
+
const plusDi = smoothPlus.map((value, idx) => {
|
|
1523
|
+
const tr = smoothTr[idx];
|
|
1524
|
+
return value == null || tr == null || tr === 0 ? null : 100 * value / tr;
|
|
1525
|
+
});
|
|
1526
|
+
const minusDi = smoothMinus.map((value, idx) => {
|
|
1527
|
+
const tr = smoothTr[idx];
|
|
1528
|
+
return value == null || tr == null || tr === 0 ? null : 100 * value / tr;
|
|
1529
|
+
});
|
|
1530
|
+
const dx = plusDi.map((value, idx) => {
|
|
1531
|
+
const minus = minusDi[idx];
|
|
1532
|
+
if (value == null || minus == null) return null;
|
|
1533
|
+
const sum = value + minus;
|
|
1534
|
+
return sum === 0 ? 0 : 100 * Math.abs(value - minus) / sum;
|
|
1535
|
+
});
|
|
1536
|
+
const adx = rmaFromValues(dx, length);
|
|
1537
|
+
return { adx, plusDi, minusDi };
|
|
1538
|
+
};
|
|
1539
|
+
var computeObvSeries = (data) => {
|
|
1540
|
+
const result = new Array(data.length).fill(null);
|
|
1541
|
+
let obv = 0;
|
|
1542
|
+
for (let i = 0; i < data.length; i += 1) {
|
|
1543
|
+
const point = data[i];
|
|
1544
|
+
if (i > 0) {
|
|
1545
|
+
const prev = data[i - 1];
|
|
1546
|
+
const volume = Math.max(0, point.v ?? 0);
|
|
1547
|
+
if (point.c > prev.c) obv += volume;
|
|
1548
|
+
else if (point.c < prev.c) obv -= volume;
|
|
1549
|
+
}
|
|
1550
|
+
result[i] = obv;
|
|
1551
|
+
}
|
|
1552
|
+
return result;
|
|
1553
|
+
};
|
|
1554
|
+
var computeMfiSeries = (data, length) => {
|
|
1555
|
+
const result = new Array(data.length).fill(null);
|
|
1556
|
+
const positiveFlow = new Array(data.length).fill(0);
|
|
1557
|
+
const negativeFlow = new Array(data.length).fill(0);
|
|
1558
|
+
let prevTypical = null;
|
|
1559
|
+
for (let i = 0; i < data.length; i += 1) {
|
|
1560
|
+
const point = data[i];
|
|
1561
|
+
const typical = (point.h + point.l + point.c) / 3;
|
|
1562
|
+
const flow = typical * Math.max(0, point.v ?? 0);
|
|
1563
|
+
if (prevTypical !== null) {
|
|
1564
|
+
if (typical > prevTypical) positiveFlow[i] = flow;
|
|
1565
|
+
else if (typical < prevTypical) negativeFlow[i] = flow;
|
|
1566
|
+
}
|
|
1567
|
+
prevTypical = typical;
|
|
1568
|
+
}
|
|
1569
|
+
let posSum = 0;
|
|
1570
|
+
let negSum = 0;
|
|
1571
|
+
for (let i = 0; i < data.length; i += 1) {
|
|
1572
|
+
posSum += positiveFlow[i];
|
|
1573
|
+
negSum += negativeFlow[i];
|
|
1574
|
+
if (i >= length) {
|
|
1575
|
+
posSum -= positiveFlow[i - length];
|
|
1576
|
+
negSum -= negativeFlow[i - length];
|
|
1577
|
+
}
|
|
1578
|
+
if (i >= length) {
|
|
1579
|
+
result[i] = negSum === 0 ? 100 : 100 - 100 / (1 + posSum / negSum);
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
return result;
|
|
1583
|
+
};
|
|
1584
|
+
var computeCciSeries = (data, length) => {
|
|
1585
|
+
const result = new Array(data.length).fill(null);
|
|
1586
|
+
const typical = data.map((point) => (point.h + point.l + point.c) / 3);
|
|
1587
|
+
const smaTypical = smaFromValues(typical, length);
|
|
1588
|
+
for (let i = length - 1; i < data.length; i += 1) {
|
|
1589
|
+
const mean = smaTypical[i];
|
|
1590
|
+
if (mean == null) continue;
|
|
1591
|
+
let meanDeviation = 0;
|
|
1592
|
+
for (let j = 0; j < length; j += 1) {
|
|
1593
|
+
meanDeviation += Math.abs(typical[i - j] - mean);
|
|
1594
|
+
}
|
|
1595
|
+
meanDeviation /= length;
|
|
1596
|
+
result[i] = meanDeviation === 0 ? 0 : (typical[i] - mean) / (0.015 * meanDeviation);
|
|
1597
|
+
}
|
|
1598
|
+
return result;
|
|
1599
|
+
};
|
|
1600
|
+
var computeWilliamsRSeries = (data, length) => {
|
|
1601
|
+
const highest = rollingExtremeSeries(data.map((point) => point.h), length, "max");
|
|
1602
|
+
const lowest = rollingExtremeSeries(data.map((point) => point.l), length, "min");
|
|
1603
|
+
return data.map((point, idx) => {
|
|
1604
|
+
const hh = highest[idx];
|
|
1605
|
+
const ll = lowest[idx];
|
|
1606
|
+
if (hh == null || ll == null) return null;
|
|
1607
|
+
const range = hh - ll;
|
|
1608
|
+
return range === 0 ? -50 : (hh - point.c) / range * -100;
|
|
1609
|
+
});
|
|
1610
|
+
};
|
|
1611
|
+
var computeRocSeries = (data, length) => {
|
|
1612
|
+
return data.map((point, idx) => {
|
|
1613
|
+
if (idx < length) return null;
|
|
1614
|
+
const past = data[idx - length].c;
|
|
1615
|
+
return past === 0 ? null : 100 * (point.c - past) / past;
|
|
1616
|
+
});
|
|
1617
|
+
};
|
|
1618
|
+
var computeMomentumSeries = (data, length) => {
|
|
1619
|
+
return data.map((point, idx) => idx < length ? null : point.c - data[idx - length].c);
|
|
1620
|
+
};
|
|
1621
|
+
var computePsarSeries = (data, start, increment, maximum) => {
|
|
1622
|
+
const count = data.length;
|
|
1623
|
+
const result = new Array(count).fill(null);
|
|
1624
|
+
if (count < 2) return result;
|
|
1625
|
+
let isUp = data[1].c >= data[0].c;
|
|
1626
|
+
let sar = isUp ? data[0].l : data[0].h;
|
|
1627
|
+
let extremePoint = isUp ? data[0].h : data[0].l;
|
|
1628
|
+
let accelerationFactor = start;
|
|
1629
|
+
for (let i = 1; i < count; i += 1) {
|
|
1630
|
+
const point = data[i];
|
|
1631
|
+
sar += accelerationFactor * (extremePoint - sar);
|
|
1632
|
+
if (isUp) {
|
|
1633
|
+
sar = Math.min(sar, data[i - 1].l, data[Math.max(0, i - 2)].l);
|
|
1634
|
+
if (point.l < sar) {
|
|
1635
|
+
isUp = false;
|
|
1636
|
+
sar = extremePoint;
|
|
1637
|
+
extremePoint = point.l;
|
|
1638
|
+
accelerationFactor = start;
|
|
1639
|
+
} else if (point.h > extremePoint) {
|
|
1640
|
+
extremePoint = point.h;
|
|
1641
|
+
accelerationFactor = Math.min(maximum, accelerationFactor + increment);
|
|
1642
|
+
}
|
|
1643
|
+
} else {
|
|
1644
|
+
sar = Math.max(sar, data[i - 1].h, data[Math.max(0, i - 2)].h);
|
|
1645
|
+
if (point.h > sar) {
|
|
1646
|
+
isUp = true;
|
|
1647
|
+
sar = extremePoint;
|
|
1648
|
+
extremePoint = point.h;
|
|
1649
|
+
accelerationFactor = start;
|
|
1650
|
+
} else if (point.l < extremePoint) {
|
|
1651
|
+
extremePoint = point.l;
|
|
1652
|
+
accelerationFactor = Math.min(maximum, accelerationFactor + increment);
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
result[i] = sar;
|
|
1656
|
+
}
|
|
1657
|
+
return result;
|
|
1658
|
+
};
|
|
1659
|
+
var computeSuperTrend = (data, atrLength, multiplier) => {
|
|
1660
|
+
const count = data.length;
|
|
1661
|
+
const up = new Array(count).fill(null);
|
|
1662
|
+
const down = new Array(count).fill(null);
|
|
1663
|
+
const atr = computeAtrSeries(data, atrLength);
|
|
1664
|
+
let prevUpper = Number.NaN;
|
|
1665
|
+
let prevLower = Number.NaN;
|
|
1666
|
+
let trend = 1;
|
|
1667
|
+
let prevClose = 0;
|
|
1668
|
+
for (let i = 0; i < count; i += 1) {
|
|
1669
|
+
const point = data[i];
|
|
1670
|
+
const atrValue = atr[i];
|
|
1671
|
+
if (atrValue == null) {
|
|
1672
|
+
prevClose = point.c;
|
|
1673
|
+
continue;
|
|
1674
|
+
}
|
|
1675
|
+
const mid = (point.h + point.l) / 2;
|
|
1676
|
+
const basicUpper = mid + multiplier * atrValue;
|
|
1677
|
+
const basicLower = mid - multiplier * atrValue;
|
|
1678
|
+
if (!Number.isFinite(prevUpper)) {
|
|
1679
|
+
prevUpper = basicUpper;
|
|
1680
|
+
prevLower = basicLower;
|
|
1681
|
+
trend = point.c >= mid ? 1 : -1;
|
|
1682
|
+
} else {
|
|
1683
|
+
const finalUpper = basicUpper < prevUpper || prevClose > prevUpper ? basicUpper : prevUpper;
|
|
1684
|
+
const finalLower = basicLower > prevLower || prevClose < prevLower ? basicLower : prevLower;
|
|
1685
|
+
if (trend === 1) {
|
|
1686
|
+
trend = point.c < finalLower ? -1 : 1;
|
|
1687
|
+
} else {
|
|
1688
|
+
trend = point.c > finalUpper ? 1 : -1;
|
|
1689
|
+
}
|
|
1690
|
+
prevUpper = finalUpper;
|
|
1691
|
+
prevLower = finalLower;
|
|
1692
|
+
}
|
|
1693
|
+
if (trend === 1) up[i] = prevLower;
|
|
1694
|
+
else down[i] = prevUpper;
|
|
1695
|
+
prevClose = point.c;
|
|
1696
|
+
}
|
|
1697
|
+
return { up, down };
|
|
1698
|
+
};
|
|
1699
|
+
var computeIchimoku = (data, conversionLength, baseLength, spanBLength, displacement) => {
|
|
1700
|
+
const count = data.length;
|
|
1701
|
+
const highs = data.map((point) => point.h);
|
|
1702
|
+
const lows = data.map((point) => point.l);
|
|
1703
|
+
const midlineOf = (length) => {
|
|
1704
|
+
const highest = rollingExtremeSeries(highs, length, "max");
|
|
1705
|
+
const lowest = rollingExtremeSeries(lows, length, "min");
|
|
1706
|
+
return highest.map((value, idx) => {
|
|
1707
|
+
const low = lowest[idx];
|
|
1708
|
+
return value == null || low == null ? null : (value + low) / 2;
|
|
1709
|
+
});
|
|
1710
|
+
};
|
|
1711
|
+
const tenkan = midlineOf(conversionLength);
|
|
1712
|
+
const kijun = midlineOf(baseLength);
|
|
1713
|
+
const spanARaw = tenkan.map((value, idx) => {
|
|
1714
|
+
const base = kijun[idx];
|
|
1715
|
+
return value == null || base == null ? null : (value + base) / 2;
|
|
1716
|
+
});
|
|
1717
|
+
const spanBRaw = midlineOf(spanBLength);
|
|
1718
|
+
const spanA = new Array(count).fill(null);
|
|
1719
|
+
const spanB = new Array(count).fill(null);
|
|
1720
|
+
const chikou = new Array(count).fill(null);
|
|
1721
|
+
for (let i = 0; i < count; i += 1) {
|
|
1722
|
+
const shifted = i - displacement;
|
|
1723
|
+
if (shifted >= 0) {
|
|
1724
|
+
spanA[i] = spanARaw[shifted] ?? null;
|
|
1725
|
+
spanB[i] = spanBRaw[shifted] ?? null;
|
|
1726
|
+
}
|
|
1727
|
+
const forward = i + displacement;
|
|
1728
|
+
if (forward < count) {
|
|
1729
|
+
chikou[i] = data[forward].c;
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
return { tenkan, kijun, spanA, spanB, chikou };
|
|
1733
|
+
};
|
|
1734
|
+
var computeKeltner = (data, emaLength, atrLength, multiplier) => {
|
|
1735
|
+
const basis = computeEmaSeries(data, emaLength, "close");
|
|
1736
|
+
const atr = computeAtrSeries(data, atrLength);
|
|
1737
|
+
const upper = basis.map((value, idx) => {
|
|
1738
|
+
const atrValue = atr[idx];
|
|
1739
|
+
return value == null || atrValue == null ? null : value + multiplier * atrValue;
|
|
1740
|
+
});
|
|
1741
|
+
const lower = basis.map((value, idx) => {
|
|
1742
|
+
const atrValue = atr[idx];
|
|
1743
|
+
return value == null || atrValue == null ? null : value - multiplier * atrValue;
|
|
1744
|
+
});
|
|
1745
|
+
return { basis, upper, lower };
|
|
1746
|
+
};
|
|
1747
|
+
var computeDonchian = (data, length) => {
|
|
1748
|
+
const upper = rollingExtremeSeries(data.map((point) => point.h), length, "max");
|
|
1749
|
+
const lower = rollingExtremeSeries(data.map((point) => point.l), length, "min");
|
|
1750
|
+
const basis = upper.map((value, idx) => {
|
|
1751
|
+
const low = lower[idx];
|
|
1752
|
+
return value == null || low == null ? null : (value + low) / 2;
|
|
1753
|
+
});
|
|
1754
|
+
return { upper, lower, basis };
|
|
1755
|
+
};
|
|
1756
|
+
var BUILTIN_MACD_INDICATOR = {
|
|
1757
|
+
id: "macd",
|
|
1758
|
+
name: "MACD",
|
|
1759
|
+
pane: "separate",
|
|
1760
|
+
paneHeightRatio: 0.2,
|
|
1761
|
+
defaultInputs: {
|
|
1762
|
+
fast: 12,
|
|
1763
|
+
slow: 26,
|
|
1764
|
+
signal: 9,
|
|
1765
|
+
macdColor: "#2962ff",
|
|
1766
|
+
signalColor: "#ff6d00",
|
|
1767
|
+
histUpColor: "#26a69a",
|
|
1768
|
+
histDownColor: "#ef5350",
|
|
1769
|
+
showValueLine: false
|
|
1770
|
+
},
|
|
1771
|
+
draw: (ctx, renderContext, inputs) => {
|
|
1772
|
+
const fast = clampIndicatorLength(inputs.fast, 12);
|
|
1773
|
+
const slow = clampIndicatorLength(inputs.slow, 26);
|
|
1774
|
+
const signalLength = clampIndicatorLength(inputs.signal, 9);
|
|
1775
|
+
const { macd, signal, hist } = withCachedComputation(
|
|
1776
|
+
`macd|${fast}|${slow}|${signalLength}`,
|
|
1777
|
+
renderContext.data,
|
|
1778
|
+
() => computeMacd(renderContext.data, fast, slow, signalLength)
|
|
1779
|
+
);
|
|
1780
|
+
return drawSeparateMultiSeries(
|
|
1781
|
+
ctx,
|
|
1782
|
+
renderContext,
|
|
1783
|
+
[
|
|
1784
|
+
{
|
|
1785
|
+
values: hist,
|
|
1786
|
+
color: inputs.histUpColor ?? "#26a69a",
|
|
1787
|
+
negativeColor: inputs.histDownColor ?? "#ef5350",
|
|
1788
|
+
histogram: true
|
|
1789
|
+
},
|
|
1790
|
+
{ values: macd, color: inputs.macdColor ?? "#2962ff", label: "MACD" },
|
|
1791
|
+
{ values: signal, color: inputs.signalColor ?? "#ff6d00", label: "Signal" }
|
|
1792
|
+
],
|
|
1793
|
+
{
|
|
1794
|
+
title: `MACD ${fast} ${slow} ${signalLength}`,
|
|
1795
|
+
includeZero: true,
|
|
1796
|
+
guideLines: [0],
|
|
1797
|
+
decimals: 2,
|
|
1798
|
+
valueLabelSeriesIndex: 1,
|
|
1799
|
+
valueLines: inputs.showValueLine === true
|
|
1800
|
+
}
|
|
1801
|
+
);
|
|
1802
|
+
}
|
|
1803
|
+
};
|
|
1804
|
+
var BUILTIN_STOCHASTIC_INDICATOR = {
|
|
1805
|
+
id: "stochastic",
|
|
1806
|
+
name: "Stoch",
|
|
1807
|
+
pane: "separate",
|
|
1808
|
+
paneHeightRatio: 0.18,
|
|
1809
|
+
defaultInputs: { kLength: 14, kSmoothing: 1, dLength: 3, kColor: "#2962ff", dColor: "#ff6d00", showValueLine: false },
|
|
1810
|
+
draw: (ctx, renderContext, inputs) => {
|
|
1811
|
+
const kLength = clampIndicatorLength(inputs.kLength, 14);
|
|
1812
|
+
const kSmoothing = clampIndicatorLength(inputs.kSmoothing, 1);
|
|
1813
|
+
const dLength = clampIndicatorLength(inputs.dLength, 3);
|
|
1814
|
+
const { k, d } = withCachedComputation(
|
|
1815
|
+
`stochastic|${kLength}|${kSmoothing}|${dLength}`,
|
|
1816
|
+
renderContext.data,
|
|
1817
|
+
() => computeStochastic(renderContext.data, kLength, kSmoothing, dLength)
|
|
1818
|
+
);
|
|
1819
|
+
return drawSeparateMultiSeries(
|
|
1820
|
+
ctx,
|
|
1821
|
+
renderContext,
|
|
1822
|
+
[
|
|
1823
|
+
{ values: k, color: inputs.kColor ?? "#2962ff", label: "%K" },
|
|
1824
|
+
{ values: d, color: inputs.dColor ?? "#ff6d00", label: "%D" }
|
|
1825
|
+
],
|
|
1826
|
+
{
|
|
1827
|
+
title: `Stoch ${kLength} ${kSmoothing} ${dLength}`,
|
|
1828
|
+
minOverride: 0,
|
|
1829
|
+
maxOverride: 100,
|
|
1830
|
+
guideLines: [20, 80],
|
|
1831
|
+
axisTicks: [0, 20, 50, 80, 100],
|
|
1832
|
+
decimals: 2,
|
|
1833
|
+
valueLines: inputs.showValueLine === true
|
|
1834
|
+
}
|
|
1835
|
+
);
|
|
1836
|
+
}
|
|
1837
|
+
};
|
|
1838
|
+
var BUILTIN_STOCHRSI_INDICATOR = {
|
|
1839
|
+
id: "stochrsi",
|
|
1840
|
+
name: "Stoch RSI",
|
|
1841
|
+
pane: "separate",
|
|
1842
|
+
paneHeightRatio: 0.18,
|
|
1843
|
+
defaultInputs: {
|
|
1844
|
+
rsiLength: 14,
|
|
1845
|
+
stochLength: 14,
|
|
1846
|
+
kSmoothing: 3,
|
|
1847
|
+
dSmoothing: 3,
|
|
1848
|
+
kColor: "#2962ff",
|
|
1849
|
+
dColor: "#ff6d00",
|
|
1850
|
+
showValueLine: false
|
|
1851
|
+
},
|
|
1852
|
+
draw: (ctx, renderContext, inputs) => {
|
|
1853
|
+
const rsiLength = clampIndicatorLength(inputs.rsiLength, 14);
|
|
1854
|
+
const stochLength = clampIndicatorLength(inputs.stochLength, 14);
|
|
1855
|
+
const kSmoothing = clampIndicatorLength(inputs.kSmoothing, 3);
|
|
1856
|
+
const dSmoothing = clampIndicatorLength(inputs.dSmoothing, 3);
|
|
1857
|
+
const { k, d } = withCachedComputation(
|
|
1858
|
+
`stochrsi|${rsiLength}|${stochLength}|${kSmoothing}|${dSmoothing}`,
|
|
1859
|
+
renderContext.data,
|
|
1860
|
+
() => computeStochRsi(renderContext.data, rsiLength, stochLength, kSmoothing, dSmoothing)
|
|
1861
|
+
);
|
|
1862
|
+
return drawSeparateMultiSeries(
|
|
1863
|
+
ctx,
|
|
1864
|
+
renderContext,
|
|
1865
|
+
[
|
|
1866
|
+
{ values: k, color: inputs.kColor ?? "#2962ff", label: "%K" },
|
|
1867
|
+
{ values: d, color: inputs.dColor ?? "#ff6d00", label: "%D" }
|
|
1868
|
+
],
|
|
1869
|
+
{
|
|
1870
|
+
title: `Stoch RSI ${rsiLength} ${stochLength}`,
|
|
1871
|
+
minOverride: 0,
|
|
1872
|
+
maxOverride: 100,
|
|
1873
|
+
guideLines: [20, 80],
|
|
1874
|
+
axisTicks: [0, 20, 50, 80, 100],
|
|
1875
|
+
decimals: 2,
|
|
1876
|
+
valueLines: inputs.showValueLine === true
|
|
1877
|
+
}
|
|
1878
|
+
);
|
|
1879
|
+
}
|
|
1880
|
+
};
|
|
1881
|
+
var BUILTIN_ADX_INDICATOR = {
|
|
1882
|
+
id: "adx",
|
|
1883
|
+
name: "ADX/DMI",
|
|
1884
|
+
pane: "separate",
|
|
1885
|
+
paneHeightRatio: 0.18,
|
|
1886
|
+
defaultInputs: {
|
|
1887
|
+
length: 14,
|
|
1888
|
+
adxColor: "#ff6d00",
|
|
1889
|
+
plusDiColor: "#26a69a",
|
|
1890
|
+
minusDiColor: "#ef5350",
|
|
1891
|
+
showDi: true,
|
|
1892
|
+
showValueLine: false
|
|
1893
|
+
},
|
|
1894
|
+
draw: (ctx, renderContext, inputs) => {
|
|
1895
|
+
const length = clampIndicatorLength(inputs.length, 14);
|
|
1896
|
+
const { adx, plusDi, minusDi } = withCachedComputation(
|
|
1897
|
+
`adx|${length}`,
|
|
1898
|
+
renderContext.data,
|
|
1899
|
+
() => computeDmi(renderContext.data, length)
|
|
1900
|
+
);
|
|
1901
|
+
const seriesList = [
|
|
1902
|
+
{ values: adx, color: inputs.adxColor ?? "#ff6d00", label: "ADX" }
|
|
1903
|
+
];
|
|
1904
|
+
if (inputs.showDi !== false) {
|
|
1905
|
+
seriesList.push(
|
|
1906
|
+
{ values: plusDi, color: inputs.plusDiColor ?? "#26a69a", label: "+DI", width: 1 },
|
|
1907
|
+
{ values: minusDi, color: inputs.minusDiColor ?? "#ef5350", label: "-DI", width: 1 }
|
|
1908
|
+
);
|
|
1909
|
+
}
|
|
1910
|
+
return drawSeparateMultiSeries(ctx, renderContext, seriesList, {
|
|
1911
|
+
title: `ADX ${length}`,
|
|
1912
|
+
minOverride: 0,
|
|
1913
|
+
guideLines: [20],
|
|
1914
|
+
decimals: 2,
|
|
1915
|
+
valueLines: inputs.showValueLine === true
|
|
1916
|
+
});
|
|
1917
|
+
}
|
|
1918
|
+
};
|
|
1919
|
+
var BUILTIN_OBV_INDICATOR = {
|
|
1920
|
+
id: "obv",
|
|
1921
|
+
name: "OBV",
|
|
1922
|
+
pane: "separate",
|
|
1923
|
+
paneHeightRatio: 0.16,
|
|
1924
|
+
defaultInputs: { color: "#2962ff", width: 2, showValueLine: false },
|
|
1925
|
+
draw: (ctx, renderContext, inputs) => {
|
|
1926
|
+
const values = withCachedSeries("obv", renderContext.data, () => computeObvSeries(renderContext.data));
|
|
1927
|
+
return drawSeparateMultiSeries(
|
|
1928
|
+
ctx,
|
|
1929
|
+
renderContext,
|
|
1930
|
+
[{ values, color: inputs.color ?? "#2962ff", width: Number(inputs.width) || 2, label: "OBV" }],
|
|
1931
|
+
{ title: "OBV", format: formatCompactNumber, valueLines: inputs.showValueLine === true }
|
|
1932
|
+
);
|
|
1933
|
+
}
|
|
1934
|
+
};
|
|
1935
|
+
var BUILTIN_MFI_INDICATOR = {
|
|
1936
|
+
id: "mfi",
|
|
1937
|
+
name: "MFI",
|
|
1938
|
+
pane: "separate",
|
|
1939
|
+
paneHeightRatio: 0.16,
|
|
1940
|
+
defaultInputs: { length: 14, color: "#7e57c2", width: 2, showValueLine: false },
|
|
1941
|
+
draw: (ctx, renderContext, inputs) => {
|
|
1942
|
+
const length = clampIndicatorLength(inputs.length, 14);
|
|
1943
|
+
const values = withCachedSeries(
|
|
1944
|
+
`mfi|${length}`,
|
|
1945
|
+
renderContext.data,
|
|
1946
|
+
() => computeMfiSeries(renderContext.data, length)
|
|
1947
|
+
);
|
|
1948
|
+
return drawSeparateMultiSeries(
|
|
1949
|
+
ctx,
|
|
1950
|
+
renderContext,
|
|
1951
|
+
[{ values, color: inputs.color ?? "#7e57c2", width: Number(inputs.width) || 2 }],
|
|
1952
|
+
{
|
|
1953
|
+
title: `MFI ${length}`,
|
|
1954
|
+
minOverride: 0,
|
|
1955
|
+
maxOverride: 100,
|
|
1956
|
+
guideLines: [20, 80],
|
|
1957
|
+
axisTicks: [0, 20, 50, 80, 100],
|
|
1958
|
+
decimals: 2,
|
|
1959
|
+
valueLines: inputs.showValueLine === true
|
|
1960
|
+
}
|
|
1961
|
+
);
|
|
1962
|
+
}
|
|
1963
|
+
};
|
|
1964
|
+
var BUILTIN_CCI_INDICATOR = {
|
|
1965
|
+
id: "cci",
|
|
1966
|
+
name: "CCI",
|
|
1967
|
+
pane: "separate",
|
|
1968
|
+
paneHeightRatio: 0.16,
|
|
1969
|
+
defaultInputs: { length: 20, color: "#2962ff", width: 2, showValueLine: false },
|
|
1970
|
+
draw: (ctx, renderContext, inputs) => {
|
|
1971
|
+
const length = clampIndicatorLength(inputs.length, 20);
|
|
1972
|
+
const values = withCachedSeries(
|
|
1973
|
+
`cci|${length}`,
|
|
1974
|
+
renderContext.data,
|
|
1975
|
+
() => computeCciSeries(renderContext.data, length)
|
|
1976
|
+
);
|
|
1977
|
+
return drawSeparateMultiSeries(
|
|
1978
|
+
ctx,
|
|
1979
|
+
renderContext,
|
|
1980
|
+
[{ values, color: inputs.color ?? "#2962ff", width: Number(inputs.width) || 2 }],
|
|
1981
|
+
{ title: `CCI ${length}`, guideLines: [-100, 0, 100], decimals: 2, valueLines: inputs.showValueLine === true }
|
|
1982
|
+
);
|
|
1983
|
+
}
|
|
1984
|
+
};
|
|
1985
|
+
var BUILTIN_WILLIAMSR_INDICATOR = {
|
|
1986
|
+
id: "williamsr",
|
|
1987
|
+
name: "Williams %R",
|
|
1988
|
+
pane: "separate",
|
|
1989
|
+
paneHeightRatio: 0.16,
|
|
1990
|
+
defaultInputs: { length: 14, color: "#7e57c2", width: 2, showValueLine: false },
|
|
1991
|
+
draw: (ctx, renderContext, inputs) => {
|
|
1992
|
+
const length = clampIndicatorLength(inputs.length, 14);
|
|
1993
|
+
const values = withCachedSeries(
|
|
1994
|
+
`williamsr|${length}`,
|
|
1995
|
+
renderContext.data,
|
|
1996
|
+
() => computeWilliamsRSeries(renderContext.data, length)
|
|
1997
|
+
);
|
|
1998
|
+
return drawSeparateMultiSeries(
|
|
1999
|
+
ctx,
|
|
2000
|
+
renderContext,
|
|
2001
|
+
[{ values, color: inputs.color ?? "#7e57c2", width: Number(inputs.width) || 2 }],
|
|
2002
|
+
{
|
|
2003
|
+
title: `%R ${length}`,
|
|
2004
|
+
minOverride: -100,
|
|
2005
|
+
maxOverride: 0,
|
|
2006
|
+
guideLines: [-80, -20],
|
|
2007
|
+
axisTicks: [-100, -80, -50, -20, 0],
|
|
2008
|
+
decimals: 2,
|
|
2009
|
+
valueLines: inputs.showValueLine === true
|
|
1167
2010
|
}
|
|
1168
2011
|
);
|
|
1169
2012
|
}
|
|
1170
2013
|
};
|
|
2014
|
+
var BUILTIN_ROC_INDICATOR = {
|
|
2015
|
+
id: "roc",
|
|
2016
|
+
name: "ROC",
|
|
2017
|
+
pane: "separate",
|
|
2018
|
+
paneHeightRatio: 0.16,
|
|
2019
|
+
defaultInputs: { length: 9, color: "#2962ff", width: 2, showValueLine: false },
|
|
2020
|
+
draw: (ctx, renderContext, inputs) => {
|
|
2021
|
+
const length = clampIndicatorLength(inputs.length, 9);
|
|
2022
|
+
const values = withCachedSeries(
|
|
2023
|
+
`roc|${length}`,
|
|
2024
|
+
renderContext.data,
|
|
2025
|
+
() => computeRocSeries(renderContext.data, length)
|
|
2026
|
+
);
|
|
2027
|
+
return drawSeparateMultiSeries(
|
|
2028
|
+
ctx,
|
|
2029
|
+
renderContext,
|
|
2030
|
+
[{ values, color: inputs.color ?? "#2962ff", width: Number(inputs.width) || 2 }],
|
|
2031
|
+
{ title: `ROC ${length}`, includeZero: true, guideLines: [0], decimals: 2, valueLines: inputs.showValueLine === true }
|
|
2032
|
+
);
|
|
2033
|
+
}
|
|
2034
|
+
};
|
|
2035
|
+
var BUILTIN_MOMENTUM_INDICATOR = {
|
|
2036
|
+
id: "momentum",
|
|
2037
|
+
name: "Momentum",
|
|
2038
|
+
pane: "separate",
|
|
2039
|
+
paneHeightRatio: 0.16,
|
|
2040
|
+
defaultInputs: { length: 10, color: "#2962ff", width: 2, showValueLine: false },
|
|
2041
|
+
draw: (ctx, renderContext, inputs) => {
|
|
2042
|
+
const length = clampIndicatorLength(inputs.length, 10);
|
|
2043
|
+
const values = withCachedSeries(
|
|
2044
|
+
`momentum|${length}`,
|
|
2045
|
+
renderContext.data,
|
|
2046
|
+
() => computeMomentumSeries(renderContext.data, length)
|
|
2047
|
+
);
|
|
2048
|
+
return drawSeparateMultiSeries(
|
|
2049
|
+
ctx,
|
|
2050
|
+
renderContext,
|
|
2051
|
+
[{ values, color: inputs.color ?? "#2962ff", width: Number(inputs.width) || 2 }],
|
|
2052
|
+
{ title: `Mom ${length}`, includeZero: true, guideLines: [0], decimals: 2, valueLines: inputs.showValueLine === true }
|
|
2053
|
+
);
|
|
2054
|
+
}
|
|
2055
|
+
};
|
|
2056
|
+
var BUILTIN_PSAR_INDICATOR = {
|
|
2057
|
+
id: "psar",
|
|
2058
|
+
name: "PSAR",
|
|
2059
|
+
pane: "overlay",
|
|
2060
|
+
defaultInputs: { start: 0.02, increment: 0.02, maximum: 0.2, color: "#2962ff" },
|
|
2061
|
+
draw: (ctx, renderContext, inputs) => {
|
|
2062
|
+
if (!renderContext.yFromPrice) return;
|
|
2063
|
+
const yFromPrice = renderContext.yFromPrice;
|
|
2064
|
+
const start = Math.max(1e-3, Number(inputs.start) || 0.02);
|
|
2065
|
+
const increment = Math.max(1e-3, Number(inputs.increment) || 0.02);
|
|
2066
|
+
const maximum = Math.max(increment, Number(inputs.maximum) || 0.2);
|
|
2067
|
+
const values = withCachedSeries(
|
|
2068
|
+
`psar|${start}|${increment}|${maximum}`,
|
|
2069
|
+
renderContext.data,
|
|
2070
|
+
() => computePsarSeries(renderContext.data, start, increment, maximum)
|
|
2071
|
+
);
|
|
2072
|
+
const radius = Math.max(1, Math.min(2.5, renderContext.candleSpacing * 0.15));
|
|
2073
|
+
const dots = new Path2D();
|
|
2074
|
+
for (let index = renderContext.startIndex; index <= renderContext.endIndex; index += 1) {
|
|
2075
|
+
const value = values[index];
|
|
2076
|
+
if (!Number.isFinite(value ?? Number.NaN)) continue;
|
|
2077
|
+
const x = renderContext.xFromIndex(index);
|
|
2078
|
+
const y = yFromPrice(value);
|
|
2079
|
+
dots.moveTo(x + radius, y);
|
|
2080
|
+
dots.arc(x, y, radius, 0, Math.PI * 2);
|
|
2081
|
+
}
|
|
2082
|
+
ctx.save();
|
|
2083
|
+
ctx.fillStyle = inputs.color ?? "#2962ff";
|
|
2084
|
+
ctx.fill(dots);
|
|
2085
|
+
ctx.restore();
|
|
2086
|
+
},
|
|
2087
|
+
getAutoscaleRange: (data, startIndex, endIndex, inputs, skipIndex) => {
|
|
2088
|
+
const start = Math.max(1e-3, Number(inputs.start) || 0.02);
|
|
2089
|
+
const increment = Math.max(1e-3, Number(inputs.increment) || 0.02);
|
|
2090
|
+
const maximum = Math.max(increment, Number(inputs.maximum) || 0.2);
|
|
2091
|
+
const values = withCachedSeries(
|
|
2092
|
+
`psar|${start}|${increment}|${maximum}`,
|
|
2093
|
+
data,
|
|
2094
|
+
() => computePsarSeries(data, start, increment, maximum)
|
|
2095
|
+
);
|
|
2096
|
+
return rangeOfSeries([values], startIndex, endIndex, skipIndex);
|
|
2097
|
+
}
|
|
2098
|
+
};
|
|
2099
|
+
var BUILTIN_SUPERTREND_INDICATOR = {
|
|
2100
|
+
id: "supertrend",
|
|
2101
|
+
name: "SuperTrend",
|
|
2102
|
+
pane: "overlay",
|
|
2103
|
+
defaultInputs: { atrLength: 10, multiplier: 3, upColor: "#26a69a", downColor: "#ef5350", width: 2 },
|
|
2104
|
+
draw: (ctx, renderContext, inputs) => {
|
|
2105
|
+
const atrLength = clampIndicatorLength(inputs.atrLength, 10);
|
|
2106
|
+
const multiplier = Math.max(0.1, Number(inputs.multiplier) || 3);
|
|
2107
|
+
const { up, down } = withCachedComputation(
|
|
2108
|
+
`supertrend|${atrLength}|${multiplier}`,
|
|
2109
|
+
renderContext.data,
|
|
2110
|
+
() => computeSuperTrend(renderContext.data, atrLength, multiplier)
|
|
2111
|
+
);
|
|
2112
|
+
const width = Number(inputs.width) || 2;
|
|
2113
|
+
drawOverlaySeries(ctx, renderContext, up, inputs.upColor ?? "#26a69a", width);
|
|
2114
|
+
drawOverlaySeries(ctx, renderContext, down, inputs.downColor ?? "#ef5350", width);
|
|
2115
|
+
},
|
|
2116
|
+
getAutoscaleRange: (data, startIndex, endIndex, inputs, skipIndex) => {
|
|
2117
|
+
const atrLength = clampIndicatorLength(inputs.atrLength, 10);
|
|
2118
|
+
const multiplier = Math.max(0.1, Number(inputs.multiplier) || 3);
|
|
2119
|
+
const { up, down } = withCachedComputation(
|
|
2120
|
+
`supertrend|${atrLength}|${multiplier}`,
|
|
2121
|
+
data,
|
|
2122
|
+
() => computeSuperTrend(data, atrLength, multiplier)
|
|
2123
|
+
);
|
|
2124
|
+
return rangeOfSeries([up, down], startIndex, endIndex, skipIndex);
|
|
2125
|
+
}
|
|
2126
|
+
};
|
|
2127
|
+
var BUILTIN_ICHIMOKU_INDICATOR = {
|
|
2128
|
+
id: "ichimoku",
|
|
2129
|
+
name: "Ichimoku",
|
|
2130
|
+
pane: "overlay",
|
|
2131
|
+
defaultInputs: {
|
|
2132
|
+
conversionLength: 9,
|
|
2133
|
+
baseLength: 26,
|
|
2134
|
+
spanBLength: 52,
|
|
2135
|
+
displacement: 26,
|
|
2136
|
+
tenkanColor: "#2962ff",
|
|
2137
|
+
kijunColor: "#b71c1c",
|
|
2138
|
+
spanAColor: "#26a69a",
|
|
2139
|
+
spanBColor: "#ef5350",
|
|
2140
|
+
chikouColor: "#43a047",
|
|
2141
|
+
cloudOpacity: 0.08,
|
|
2142
|
+
showChikou: true
|
|
2143
|
+
},
|
|
2144
|
+
draw: (ctx, renderContext, inputs) => {
|
|
2145
|
+
const conversionLength = clampIndicatorLength(inputs.conversionLength, 9);
|
|
2146
|
+
const baseLength = clampIndicatorLength(inputs.baseLength, 26);
|
|
2147
|
+
const spanBLength = clampIndicatorLength(inputs.spanBLength, 52);
|
|
2148
|
+
const displacement = clampIndicatorLength(inputs.displacement, 26);
|
|
2149
|
+
const { tenkan, kijun, spanA, spanB, chikou } = withCachedComputation(
|
|
2150
|
+
`ichimoku|${conversionLength}|${baseLength}|${spanBLength}|${displacement}`,
|
|
2151
|
+
renderContext.data,
|
|
2152
|
+
() => computeIchimoku(renderContext.data, conversionLength, baseLength, spanBLength, displacement)
|
|
2153
|
+
);
|
|
2154
|
+
const cloudOpacity = Math.min(0.5, Math.max(0, Number(inputs.cloudOpacity) || 0.08));
|
|
2155
|
+
const bullishA = spanA.map((value, idx) => {
|
|
2156
|
+
const other = spanB[idx];
|
|
2157
|
+
return value != null && other != null && value >= other ? value : null;
|
|
2158
|
+
});
|
|
2159
|
+
const bullishB = spanB.map((value, idx) => {
|
|
2160
|
+
const other = spanA[idx];
|
|
2161
|
+
return value != null && other != null && other >= value ? value : null;
|
|
2162
|
+
});
|
|
2163
|
+
const bearishA = spanA.map((value, idx) => {
|
|
2164
|
+
const other = spanB[idx];
|
|
2165
|
+
return value != null && other != null && value < other ? value : null;
|
|
2166
|
+
});
|
|
2167
|
+
const bearishB = spanB.map((value, idx) => {
|
|
2168
|
+
const other = spanA[idx];
|
|
2169
|
+
return value != null && other != null && other < value ? value : null;
|
|
2170
|
+
});
|
|
2171
|
+
fillBetweenSeries(ctx, renderContext, bullishA, bullishB, inputs.spanAColor ?? "#26a69a", cloudOpacity);
|
|
2172
|
+
fillBetweenSeries(ctx, renderContext, bearishB, bearishA, inputs.spanBColor ?? "#ef5350", cloudOpacity);
|
|
2173
|
+
drawOverlaySeries(ctx, renderContext, spanA, inputs.spanAColor ?? "#26a69a", 1);
|
|
2174
|
+
drawOverlaySeries(ctx, renderContext, spanB, inputs.spanBColor ?? "#ef5350", 1);
|
|
2175
|
+
drawOverlaySeries(ctx, renderContext, tenkan, inputs.tenkanColor ?? "#2962ff", 1.5);
|
|
2176
|
+
drawOverlaySeries(ctx, renderContext, kijun, inputs.kijunColor ?? "#b71c1c", 1.5);
|
|
2177
|
+
if (inputs.showChikou !== false) {
|
|
2178
|
+
drawOverlaySeries(ctx, renderContext, chikou, inputs.chikouColor ?? "#43a047", 1);
|
|
2179
|
+
}
|
|
2180
|
+
},
|
|
2181
|
+
getAutoscaleRange: (data, startIndex, endIndex, inputs, skipIndex) => {
|
|
2182
|
+
const conversionLength = clampIndicatorLength(inputs.conversionLength, 9);
|
|
2183
|
+
const baseLength = clampIndicatorLength(inputs.baseLength, 26);
|
|
2184
|
+
const spanBLength = clampIndicatorLength(inputs.spanBLength, 52);
|
|
2185
|
+
const displacement = clampIndicatorLength(inputs.displacement, 26);
|
|
2186
|
+
const { tenkan, kijun, spanA, spanB } = withCachedComputation(
|
|
2187
|
+
`ichimoku|${conversionLength}|${baseLength}|${spanBLength}|${displacement}`,
|
|
2188
|
+
data,
|
|
2189
|
+
() => computeIchimoku(data, conversionLength, baseLength, spanBLength, displacement)
|
|
2190
|
+
);
|
|
2191
|
+
return rangeOfSeries([tenkan, kijun, spanA, spanB], startIndex, endIndex, skipIndex);
|
|
2192
|
+
}
|
|
2193
|
+
};
|
|
2194
|
+
var BUILTIN_KELTNER_INDICATOR = {
|
|
2195
|
+
id: "keltner",
|
|
2196
|
+
name: "KC",
|
|
2197
|
+
pane: "overlay",
|
|
2198
|
+
defaultInputs: {
|
|
2199
|
+
emaLength: 20,
|
|
2200
|
+
atrLength: 10,
|
|
2201
|
+
multiplier: 2,
|
|
2202
|
+
basisColor: "#2962ff",
|
|
2203
|
+
bandColor: "#2962ff",
|
|
2204
|
+
width: 1.5,
|
|
2205
|
+
fillOpacity: 0.04
|
|
2206
|
+
},
|
|
2207
|
+
draw: (ctx, renderContext, inputs) => {
|
|
2208
|
+
const emaLength = clampIndicatorLength(inputs.emaLength, 20);
|
|
2209
|
+
const atrLength = clampIndicatorLength(inputs.atrLength, 10);
|
|
2210
|
+
const multiplier = Math.max(0.1, Number(inputs.multiplier) || 2);
|
|
2211
|
+
const { basis, upper, lower } = withCachedComputation(
|
|
2212
|
+
`keltner|${emaLength}|${atrLength}|${multiplier}`,
|
|
2213
|
+
renderContext.data,
|
|
2214
|
+
() => computeKeltner(renderContext.data, emaLength, atrLength, multiplier)
|
|
2215
|
+
);
|
|
2216
|
+
const bandColor = inputs.bandColor ?? "#2962ff";
|
|
2217
|
+
const width = Number(inputs.width) || 1.5;
|
|
2218
|
+
fillBetweenSeries(ctx, renderContext, upper, lower, bandColor, Number(inputs.fillOpacity) || 0.04);
|
|
2219
|
+
drawOverlaySeries(ctx, renderContext, upper, bandColor, width);
|
|
2220
|
+
drawOverlaySeries(ctx, renderContext, lower, bandColor, width);
|
|
2221
|
+
drawOverlaySeries(ctx, renderContext, basis, inputs.basisColor ?? "#2962ff", width);
|
|
2222
|
+
},
|
|
2223
|
+
getAutoscaleRange: (data, startIndex, endIndex, inputs, skipIndex) => {
|
|
2224
|
+
const emaLength = clampIndicatorLength(inputs.emaLength, 20);
|
|
2225
|
+
const atrLength = clampIndicatorLength(inputs.atrLength, 10);
|
|
2226
|
+
const multiplier = Math.max(0.1, Number(inputs.multiplier) || 2);
|
|
2227
|
+
const { upper, lower } = withCachedComputation(
|
|
2228
|
+
`keltner|${emaLength}|${atrLength}|${multiplier}`,
|
|
2229
|
+
data,
|
|
2230
|
+
() => computeKeltner(data, emaLength, atrLength, multiplier)
|
|
2231
|
+
);
|
|
2232
|
+
return rangeOfSeries([upper, lower], startIndex, endIndex, skipIndex);
|
|
2233
|
+
}
|
|
2234
|
+
};
|
|
2235
|
+
var BUILTIN_DONCHIAN_INDICATOR = {
|
|
2236
|
+
id: "donchian",
|
|
2237
|
+
name: "DC",
|
|
2238
|
+
pane: "overlay",
|
|
2239
|
+
defaultInputs: { length: 20, bandColor: "#2962ff", basisColor: "#ff6d00", width: 1.5, fillOpacity: 0.04 },
|
|
2240
|
+
draw: (ctx, renderContext, inputs) => {
|
|
2241
|
+
const length = clampIndicatorLength(inputs.length, 20);
|
|
2242
|
+
const { upper, lower, basis } = withCachedComputation(
|
|
2243
|
+
`donchian|${length}`,
|
|
2244
|
+
renderContext.data,
|
|
2245
|
+
() => computeDonchian(renderContext.data, length)
|
|
2246
|
+
);
|
|
2247
|
+
const bandColor = inputs.bandColor ?? "#2962ff";
|
|
2248
|
+
const width = Number(inputs.width) || 1.5;
|
|
2249
|
+
fillBetweenSeries(ctx, renderContext, upper, lower, bandColor, Number(inputs.fillOpacity) || 0.04);
|
|
2250
|
+
drawOverlaySeries(ctx, renderContext, upper, bandColor, width);
|
|
2251
|
+
drawOverlaySeries(ctx, renderContext, lower, bandColor, width);
|
|
2252
|
+
drawOverlaySeries(ctx, renderContext, basis, inputs.basisColor ?? "#ff6d00", width);
|
|
2253
|
+
},
|
|
2254
|
+
getAutoscaleRange: (data, startIndex, endIndex, inputs, skipIndex) => {
|
|
2255
|
+
const length = clampIndicatorLength(inputs.length, 20);
|
|
2256
|
+
const { upper, lower } = withCachedComputation(`donchian|${length}`, data, () => computeDonchian(data, length));
|
|
2257
|
+
return rangeOfSeries([upper, lower], startIndex, endIndex, skipIndex);
|
|
2258
|
+
}
|
|
2259
|
+
};
|
|
1171
2260
|
var BUILTIN_INDICATORS = [
|
|
1172
2261
|
BUILTIN_VOLUME_INDICATOR,
|
|
1173
2262
|
BUILTIN_SMA_INDICATOR,
|
|
@@ -1180,7 +2269,22 @@ var BUILTIN_INDICATORS = [
|
|
|
1180
2269
|
BUILTIN_VWAP_INDICATOR,
|
|
1181
2270
|
BUILTIN_BOLLINGER_INDICATOR,
|
|
1182
2271
|
BUILTIN_STDDEV_INDICATOR,
|
|
1183
|
-
BUILTIN_ATR_INDICATOR
|
|
2272
|
+
BUILTIN_ATR_INDICATOR,
|
|
2273
|
+
BUILTIN_MACD_INDICATOR,
|
|
2274
|
+
BUILTIN_STOCHASTIC_INDICATOR,
|
|
2275
|
+
BUILTIN_STOCHRSI_INDICATOR,
|
|
2276
|
+
BUILTIN_ADX_INDICATOR,
|
|
2277
|
+
BUILTIN_OBV_INDICATOR,
|
|
2278
|
+
BUILTIN_MFI_INDICATOR,
|
|
2279
|
+
BUILTIN_CCI_INDICATOR,
|
|
2280
|
+
BUILTIN_WILLIAMSR_INDICATOR,
|
|
2281
|
+
BUILTIN_ROC_INDICATOR,
|
|
2282
|
+
BUILTIN_MOMENTUM_INDICATOR,
|
|
2283
|
+
BUILTIN_PSAR_INDICATOR,
|
|
2284
|
+
BUILTIN_SUPERTREND_INDICATOR,
|
|
2285
|
+
BUILTIN_ICHIMOKU_INDICATOR,
|
|
2286
|
+
BUILTIN_KELTNER_INDICATOR,
|
|
2287
|
+
BUILTIN_DONCHIAN_INDICATOR
|
|
1184
2288
|
];
|
|
1185
2289
|
function createChart(element, options = {}) {
|
|
1186
2290
|
let mergedOptions = mergeChartOptions(DEFAULT_OPTIONS, options);
|
|
@@ -1904,6 +3008,36 @@ function createChart(element, options = {}) {
|
|
|
1904
3008
|
}
|
|
1905
3009
|
return direction > 0 ? "up" : "down";
|
|
1906
3010
|
};
|
|
3011
|
+
let heikinAshiCache = null;
|
|
3012
|
+
let heikinAshiFingerprint = "";
|
|
3013
|
+
const getHeikinAshiData = () => {
|
|
3014
|
+
const last = data[data.length - 1];
|
|
3015
|
+
const fingerprint = !last ? "empty" : `${data.length}|${last.time.getTime()}|${last.o}|${last.h}|${last.l}|${last.c}`;
|
|
3016
|
+
if (heikinAshiCache && heikinAshiFingerprint === fingerprint) {
|
|
3017
|
+
return heikinAshiCache;
|
|
3018
|
+
}
|
|
3019
|
+
const result = new Array(data.length);
|
|
3020
|
+
let prevOpen = 0;
|
|
3021
|
+
let prevClose = 0;
|
|
3022
|
+
for (let i = 0; i < data.length; i += 1) {
|
|
3023
|
+
const point = data[i];
|
|
3024
|
+
const haClose = (point.o + point.h + point.l + point.c) / 4;
|
|
3025
|
+
const haOpen = i === 0 ? (point.o + point.c) / 2 : (prevOpen + prevClose) / 2;
|
|
3026
|
+
result[i] = {
|
|
3027
|
+
time: point.time,
|
|
3028
|
+
o: haOpen,
|
|
3029
|
+
h: Math.max(point.h, haOpen, haClose),
|
|
3030
|
+
l: Math.min(point.l, haOpen, haClose),
|
|
3031
|
+
c: haClose,
|
|
3032
|
+
...point.v === void 0 ? {} : { v: point.v }
|
|
3033
|
+
};
|
|
3034
|
+
prevOpen = haOpen;
|
|
3035
|
+
prevClose = haClose;
|
|
3036
|
+
}
|
|
3037
|
+
heikinAshiCache = result;
|
|
3038
|
+
heikinAshiFingerprint = fingerprint;
|
|
3039
|
+
return result;
|
|
3040
|
+
};
|
|
1907
3041
|
const formatHoverTimeLabel = (time, mode) => {
|
|
1908
3042
|
if (mode === "time") {
|
|
1909
3043
|
return time.toLocaleTimeString(void 0, {
|
|
@@ -2481,23 +3615,34 @@ function createChart(element, options = {}) {
|
|
|
2481
3615
|
const xEnd = xStart + xSpan;
|
|
2482
3616
|
const startIndex = Math.max(0, Math.floor(xStart));
|
|
2483
3617
|
const endIndex = Math.min(data.length - 1, Math.ceil(xEnd) - 1);
|
|
3618
|
+
const chartType = mergedOptions.chartType;
|
|
3619
|
+
const seriesData = chartType === "heikin-ashi" ? getHeikinAshiData() : data;
|
|
3620
|
+
const isLineStyleChart = chartType === "line" || chartType === "area" || chartType === "baseline";
|
|
2484
3621
|
const skipLatestIndex = mergedOptions.autoScaleIgnoreLatestCandle && data.length > 1 ? data.length - 1 : -1;
|
|
2485
3622
|
const scanCandleRange = (from, to) => {
|
|
2486
3623
|
let min = Number.POSITIVE_INFINITY;
|
|
2487
3624
|
let max = Number.NEGATIVE_INFINITY;
|
|
2488
3625
|
for (let idx = from; idx <= to; idx += 1) {
|
|
2489
3626
|
if (idx === skipLatestIndex) continue;
|
|
2490
|
-
const point =
|
|
3627
|
+
const point = seriesData[idx];
|
|
2491
3628
|
if (!point) continue;
|
|
2492
|
-
if (
|
|
2493
|
-
|
|
3629
|
+
if (isLineStyleChart) {
|
|
3630
|
+
if (point.c < min) min = point.c;
|
|
3631
|
+
if (point.c > max) max = point.c;
|
|
3632
|
+
} else {
|
|
3633
|
+
if (point.l < min) min = point.l;
|
|
3634
|
+
if (point.h > max) max = point.h;
|
|
3635
|
+
}
|
|
2494
3636
|
}
|
|
2495
3637
|
return min <= max ? { min, max } : null;
|
|
2496
3638
|
};
|
|
2497
3639
|
let candleRange = scanCandleRange(startIndex, endIndex);
|
|
2498
3640
|
if (!candleRange) {
|
|
2499
3641
|
const latestIndex = data.length - 1;
|
|
2500
|
-
candleRange = scanCandleRange(Math.max(0, latestIndex - 120), latestIndex - 1) ?? scanCandleRange(0, latestIndex) ?? {
|
|
3642
|
+
candleRange = scanCandleRange(Math.max(0, latestIndex - 120), latestIndex - 1) ?? scanCandleRange(0, latestIndex) ?? {
|
|
3643
|
+
min: isLineStyleChart ? seriesData[latestIndex].c : seriesData[latestIndex].l,
|
|
3644
|
+
max: isLineStyleChart ? seriesData[latestIndex].c : seriesData[latestIndex].h
|
|
3645
|
+
};
|
|
2501
3646
|
}
|
|
2502
3647
|
let minPrice = candleRange.min;
|
|
2503
3648
|
let maxPrice = candleRange.max;
|
|
@@ -3393,23 +4538,137 @@ function createChart(element, options = {}) {
|
|
|
3393
4538
|
}
|
|
3394
4539
|
return data[index]?.v;
|
|
3395
4540
|
};
|
|
3396
|
-
{
|
|
4541
|
+
if (isLineStyleChart) {
|
|
4542
|
+
const seriesLineWidth = Math.max(1, mergedOptions.lineWidth);
|
|
4543
|
+
const linePath = new Path2D();
|
|
4544
|
+
let started = false;
|
|
4545
|
+
let firstX = 0;
|
|
4546
|
+
let lastX = 0;
|
|
4547
|
+
for (let index = startIndex; index <= endIndex; index += 1) {
|
|
4548
|
+
const point = seriesData[index];
|
|
4549
|
+
if (!point) continue;
|
|
4550
|
+
const close = useSmoothedCandle && index === lastDataIndex ? smoothedTickerPrice : point.c;
|
|
4551
|
+
const x = chartLeft + (index + 0.5 - xStart) / xSpan * chartWidth;
|
|
4552
|
+
const y = yFromPrice(close);
|
|
4553
|
+
if (!started) {
|
|
4554
|
+
linePath.moveTo(x, y);
|
|
4555
|
+
firstX = x;
|
|
4556
|
+
started = true;
|
|
4557
|
+
} else {
|
|
4558
|
+
linePath.lineTo(x, y);
|
|
4559
|
+
}
|
|
4560
|
+
lastX = x;
|
|
4561
|
+
}
|
|
4562
|
+
if (started) {
|
|
4563
|
+
if (chartType === "baseline") {
|
|
4564
|
+
const baselinePrice = mergedOptions.baselinePrice ?? (yMin + yMax) / 2;
|
|
4565
|
+
const baselineY = yFromPrice(baselinePrice);
|
|
4566
|
+
const fillPath = new Path2D(linePath);
|
|
4567
|
+
fillPath.lineTo(lastX, baselineY);
|
|
4568
|
+
fillPath.lineTo(firstX, baselineY);
|
|
4569
|
+
fillPath.closePath();
|
|
4570
|
+
const halves = [
|
|
4571
|
+
{ clipTop: chartTop, clipBottom: baselineY, color: mergedOptions.upColor },
|
|
4572
|
+
{ clipTop: baselineY, clipBottom: chartBottom, color: mergedOptions.downColor }
|
|
4573
|
+
];
|
|
4574
|
+
for (const half of halves) {
|
|
4575
|
+
const clipHeight = half.clipBottom - half.clipTop;
|
|
4576
|
+
if (clipHeight <= 0) continue;
|
|
4577
|
+
ctx.save();
|
|
4578
|
+
ctx.beginPath();
|
|
4579
|
+
ctx.rect(chartLeft, half.clipTop, chartWidth, clipHeight);
|
|
4580
|
+
ctx.clip();
|
|
4581
|
+
ctx.globalAlpha = Math.max(0, Math.min(1, mergedOptions.areaFillOpacity));
|
|
4582
|
+
ctx.fillStyle = half.color;
|
|
4583
|
+
ctx.fill(fillPath);
|
|
4584
|
+
ctx.globalAlpha = 1;
|
|
4585
|
+
ctx.strokeStyle = half.color;
|
|
4586
|
+
ctx.lineWidth = seriesLineWidth;
|
|
4587
|
+
ctx.lineJoin = "round";
|
|
4588
|
+
ctx.stroke(linePath);
|
|
4589
|
+
ctx.restore();
|
|
4590
|
+
}
|
|
4591
|
+
ctx.save();
|
|
4592
|
+
ctx.strokeStyle = "rgba(148,163,184,0.5)";
|
|
4593
|
+
ctx.lineWidth = 1;
|
|
4594
|
+
ctx.setLineDash([4, 4]);
|
|
4595
|
+
ctx.beginPath();
|
|
4596
|
+
ctx.moveTo(chartLeft, crisp(baselineY));
|
|
4597
|
+
ctx.lineTo(chartRight, crisp(baselineY));
|
|
4598
|
+
ctx.stroke();
|
|
4599
|
+
ctx.restore();
|
|
4600
|
+
} else {
|
|
4601
|
+
if (chartType === "area") {
|
|
4602
|
+
const fillPath = new Path2D(linePath);
|
|
4603
|
+
fillPath.lineTo(lastX, chartBottom);
|
|
4604
|
+
fillPath.lineTo(firstX, chartBottom);
|
|
4605
|
+
fillPath.closePath();
|
|
4606
|
+
ctx.save();
|
|
4607
|
+
ctx.globalAlpha = Math.max(0, Math.min(1, mergedOptions.areaFillOpacity));
|
|
4608
|
+
ctx.fillStyle = mergedOptions.lineColor;
|
|
4609
|
+
ctx.fill(fillPath);
|
|
4610
|
+
ctx.restore();
|
|
4611
|
+
}
|
|
4612
|
+
ctx.save();
|
|
4613
|
+
ctx.strokeStyle = mergedOptions.lineColor;
|
|
4614
|
+
ctx.lineWidth = seriesLineWidth;
|
|
4615
|
+
ctx.lineJoin = "round";
|
|
4616
|
+
ctx.stroke(linePath);
|
|
4617
|
+
ctx.restore();
|
|
4618
|
+
}
|
|
4619
|
+
}
|
|
4620
|
+
} else if (chartType === "bars") {
|
|
4621
|
+
const upBars = new Path2D();
|
|
4622
|
+
const downBars = new Path2D();
|
|
4623
|
+
const tickLength = Math.max(2, Math.floor(bodyWidth / 2));
|
|
4624
|
+
for (let index = startIndex; index <= endIndex; index += 1) {
|
|
4625
|
+
const point = seriesData[index];
|
|
4626
|
+
if (!point) continue;
|
|
4627
|
+
const isLastCandle = useSmoothedCandle && index === lastDataIndex;
|
|
4628
|
+
const actualDirection = point.c >= point.o ? "up" : "down";
|
|
4629
|
+
const displayClose = isLastCandle ? actualDirection === "up" ? Math.max(point.o, smoothedTickerPrice) : Math.min(point.o, smoothedTickerPrice) : point.c;
|
|
4630
|
+
const displayHigh = isLastCandle && actualDirection === "up" ? Math.max(point.h, displayClose) : point.h;
|
|
4631
|
+
const displayLow = isLastCandle && actualDirection === "down" ? Math.min(point.l, displayClose) : point.l;
|
|
4632
|
+
const roundedCenterX = Math.round(chartLeft + (index + 0.5 - xStart) / xSpan * chartWidth);
|
|
4633
|
+
const direction = isLastCandle ? actualDirection : getCandleDirectionByIndex(index);
|
|
4634
|
+
const path = direction === "up" ? upBars : downBars;
|
|
4635
|
+
const centerX = roundedCenterX + 0.5;
|
|
4636
|
+
path.moveTo(centerX, crisp(yFromPrice(displayHigh)));
|
|
4637
|
+
path.lineTo(centerX, crisp(yFromPrice(displayLow)));
|
|
4638
|
+
const openY = crisp(yFromPrice(point.o));
|
|
4639
|
+
const closeY = crisp(yFromPrice(displayClose));
|
|
4640
|
+
path.moveTo(centerX - tickLength, openY);
|
|
4641
|
+
path.lineTo(centerX, openY);
|
|
4642
|
+
path.moveTo(centerX, closeY);
|
|
4643
|
+
path.lineTo(centerX + tickLength, closeY);
|
|
4644
|
+
}
|
|
4645
|
+
ctx.lineWidth = Math.max(1, candleWickWidth);
|
|
4646
|
+
ctx.strokeStyle = mergedOptions.upColor;
|
|
4647
|
+
ctx.stroke(upBars);
|
|
4648
|
+
ctx.strokeStyle = mergedOptions.downColor;
|
|
4649
|
+
ctx.stroke(downBars);
|
|
4650
|
+
} else {
|
|
4651
|
+
const isHeikinAshi = chartType === "heikin-ashi";
|
|
4652
|
+
const isHollow = chartType === "hollow-candles";
|
|
4653
|
+
const smoothLastCandle = useSmoothedCandle && !isHeikinAshi;
|
|
3397
4654
|
const upWicks = new Path2D();
|
|
3398
4655
|
const downWicks = new Path2D();
|
|
3399
4656
|
const upBodies = new Path2D();
|
|
3400
4657
|
const downBodies = new Path2D();
|
|
4658
|
+
const upHollowBodies = new Path2D();
|
|
4659
|
+
const downHollowBodies = new Path2D();
|
|
3401
4660
|
for (let index = startIndex; index <= endIndex; index += 1) {
|
|
3402
|
-
const point =
|
|
4661
|
+
const point = seriesData[index];
|
|
3403
4662
|
if (!point) {
|
|
3404
4663
|
continue;
|
|
3405
4664
|
}
|
|
3406
|
-
const isLastCandle =
|
|
4665
|
+
const isLastCandle = smoothLastCandle && index === lastDataIndex;
|
|
3407
4666
|
const actualDirection = point.c >= point.o ? "up" : "down";
|
|
3408
4667
|
const displayClose = isLastCandle ? actualDirection === "up" ? Math.max(point.o, smoothedTickerPrice) : Math.min(point.o, smoothedTickerPrice) : point.c;
|
|
3409
4668
|
const displayHigh = isLastCandle ? actualDirection === "up" ? Math.max(point.h, displayClose) : point.h : point.h;
|
|
3410
4669
|
const displayLow = isLastCandle ? actualDirection === "up" ? point.l : Math.min(point.l, displayClose) : point.l;
|
|
3411
4670
|
const centerX = chartLeft + (index + 0.5 - xStart) / xSpan * chartWidth;
|
|
3412
|
-
const direction = isLastCandle ? actualDirection : getCandleDirectionByIndex(index);
|
|
4671
|
+
const direction = isHeikinAshi || isLastCandle ? actualDirection : getCandleDirectionByIndex(index);
|
|
3413
4672
|
const isUp = direction === "up";
|
|
3414
4673
|
const roundedCenterX = Math.round(centerX);
|
|
3415
4674
|
const wicks = isUp ? upWicks : downWicks;
|
|
@@ -3421,7 +4680,9 @@ function createChart(element, options = {}) {
|
|
|
3421
4680
|
const bodyIsUp = displayClose >= point.o;
|
|
3422
4681
|
const bodyTop = bodyIsUp ? Math.min(closeYPx, openYPx - 1) : openYPx;
|
|
3423
4682
|
const bodyBottom = bodyIsUp ? openYPx : Math.max(closeYPx, openYPx + 1);
|
|
3424
|
-
|
|
4683
|
+
const useHollowBody = isHollow && bodyIsUp;
|
|
4684
|
+
const bodies = useHollowBody ? isUp ? upHollowBodies : downHollowBodies : isUp ? upBodies : downBodies;
|
|
4685
|
+
bodies.rect(bodyLeft, bodyTop, bodyWidth, bodyBottom - bodyTop);
|
|
3425
4686
|
}
|
|
3426
4687
|
ctx.lineWidth = candleWickWidth;
|
|
3427
4688
|
ctx.strokeStyle = mergedOptions.upColor;
|
|
@@ -3432,6 +4693,16 @@ function createChart(element, options = {}) {
|
|
|
3432
4693
|
ctx.stroke(downWicks);
|
|
3433
4694
|
ctx.fillStyle = mergedOptions.downColor;
|
|
3434
4695
|
ctx.fill(downBodies);
|
|
4696
|
+
if (isHollow) {
|
|
4697
|
+
ctx.fillStyle = mergedOptions.backgroundColor;
|
|
4698
|
+
ctx.fill(upHollowBodies);
|
|
4699
|
+
ctx.fill(downHollowBodies);
|
|
4700
|
+
ctx.lineWidth = 1;
|
|
4701
|
+
ctx.strokeStyle = mergedOptions.upColor;
|
|
4702
|
+
ctx.stroke(upHollowBodies);
|
|
4703
|
+
ctx.strokeStyle = mergedOptions.downColor;
|
|
4704
|
+
ctx.stroke(downHollowBodies);
|
|
4705
|
+
}
|
|
3435
4706
|
}
|
|
3436
4707
|
const activeOverlayIndicators = indicators.filter((indicator) => indicator.visible).map((indicator) => ({ indicator, plugin: indicatorRegistry.get(indicator.type) })).filter(
|
|
3437
4708
|
(value) => value.plugin !== void 0 && (value.indicator.pane ?? value.plugin.pane ?? "overlay") === "overlay"
|
|
@@ -5827,6 +7098,14 @@ function createChart(element, options = {}) {
|
|
|
5827
7098
|
}
|
|
5828
7099
|
scheduleDraw();
|
|
5829
7100
|
};
|
|
7101
|
+
const setChartType = (type) => {
|
|
7102
|
+
if (mergedOptions.chartType === type) {
|
|
7103
|
+
return;
|
|
7104
|
+
}
|
|
7105
|
+
mergedOptions = { ...mergedOptions, chartType: type };
|
|
7106
|
+
scheduleDraw({ updateAutoScale: true });
|
|
7107
|
+
};
|
|
7108
|
+
const getChartType = () => mergedOptions.chartType;
|
|
5830
7109
|
const resize = (nextWidth, nextHeight) => {
|
|
5831
7110
|
if (nextWidth && nextWidth > 0) {
|
|
5832
7111
|
width = nextWidth;
|
|
@@ -6184,6 +7463,8 @@ function createChart(element, options = {}) {
|
|
|
6184
7463
|
draw();
|
|
6185
7464
|
return {
|
|
6186
7465
|
updateOptions,
|
|
7466
|
+
setChartType,
|
|
7467
|
+
getChartType,
|
|
6187
7468
|
setData,
|
|
6188
7469
|
upsertBar,
|
|
6189
7470
|
setPriceLines,
|