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.js
CHANGED
|
@@ -189,6 +189,11 @@ var DEFAULT_OPTIONS = {
|
|
|
189
189
|
tickSize: 0,
|
|
190
190
|
candleColorMode: "openClose",
|
|
191
191
|
candleColorEpsilon: -1,
|
|
192
|
+
chartType: "candles",
|
|
193
|
+
lineColor: "#2962ff",
|
|
194
|
+
lineWidth: 2,
|
|
195
|
+
areaFillOpacity: 0.12,
|
|
196
|
+
baselinePrice: null,
|
|
192
197
|
// 0 = deterministic scale (lightweight-charts behavior): the y-range is a
|
|
193
198
|
// pure function of the visible data, so it can never drift or "breathe".
|
|
194
199
|
// Values > 0 re-enable eased contraction for hosts that prefer it.
|
|
@@ -597,6 +602,79 @@ var withCachedSeries = (key, data, compute) => {
|
|
|
597
602
|
builtInSeriesCache.set(key, { fingerprint, values });
|
|
598
603
|
return values;
|
|
599
604
|
};
|
|
605
|
+
var builtInComputationCache = /* @__PURE__ */ new Map();
|
|
606
|
+
var withCachedComputation = (key, data, compute) => {
|
|
607
|
+
const fingerprint = getSeriesFingerprint(data);
|
|
608
|
+
const existing = builtInComputationCache.get(key);
|
|
609
|
+
if (existing && existing.fingerprint === fingerprint) {
|
|
610
|
+
return existing.value;
|
|
611
|
+
}
|
|
612
|
+
const value = compute();
|
|
613
|
+
builtInComputationCache.set(key, { fingerprint, value });
|
|
614
|
+
return value;
|
|
615
|
+
};
|
|
616
|
+
var rollingExtremeSeries = (values, length, mode) => {
|
|
617
|
+
const count = values.length;
|
|
618
|
+
const result = new Array(count).fill(null);
|
|
619
|
+
const deque = [];
|
|
620
|
+
const better = (a, b) => mode === "max" ? a >= b : a <= b;
|
|
621
|
+
for (let i = 0; i < count; i += 1) {
|
|
622
|
+
const value = values[i];
|
|
623
|
+
while (deque.length > 0 && better(value, values[deque[deque.length - 1]])) {
|
|
624
|
+
deque.pop();
|
|
625
|
+
}
|
|
626
|
+
deque.push(i);
|
|
627
|
+
if (deque[0] <= i - length) {
|
|
628
|
+
deque.shift();
|
|
629
|
+
}
|
|
630
|
+
if (i >= length - 1) {
|
|
631
|
+
result[i] = values[deque[0]];
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
return result;
|
|
635
|
+
};
|
|
636
|
+
var smaFromValues = (values, length) => {
|
|
637
|
+
const result = new Array(values.length).fill(null);
|
|
638
|
+
let sum = 0;
|
|
639
|
+
let valid = 0;
|
|
640
|
+
for (let i = 0; i < values.length; i += 1) {
|
|
641
|
+
const value = values[i];
|
|
642
|
+
if (value != null && Number.isFinite(value)) {
|
|
643
|
+
sum += value;
|
|
644
|
+
valid += 1;
|
|
645
|
+
} else {
|
|
646
|
+
sum = 0;
|
|
647
|
+
valid = 0;
|
|
648
|
+
continue;
|
|
649
|
+
}
|
|
650
|
+
if (valid > length) {
|
|
651
|
+
sum -= values[i - length];
|
|
652
|
+
valid = length;
|
|
653
|
+
}
|
|
654
|
+
if (valid === length) {
|
|
655
|
+
result[i] = sum / length;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
return result;
|
|
659
|
+
};
|
|
660
|
+
var emaFromValues = (values, length) => {
|
|
661
|
+
const result = new Array(values.length).fill(null);
|
|
662
|
+
const alpha = 2 / (length + 1);
|
|
663
|
+
let prev = null;
|
|
664
|
+
let seen = 0;
|
|
665
|
+
for (let i = 0; i < values.length; i += 1) {
|
|
666
|
+
const value = values[i];
|
|
667
|
+
if (value == null || !Number.isFinite(value)) {
|
|
668
|
+
continue;
|
|
669
|
+
}
|
|
670
|
+
prev = prev === null ? value : alpha * value + (1 - alpha) * prev;
|
|
671
|
+
seen += 1;
|
|
672
|
+
if (seen >= length) {
|
|
673
|
+
result[i] = prev;
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
return result;
|
|
677
|
+
};
|
|
600
678
|
var drawOverlaySeries = (ctx, renderContext, values, color, width) => {
|
|
601
679
|
if (!renderContext.yFromPrice) return;
|
|
602
680
|
const yFromPrice = renderContext.yFromPrice;
|
|
@@ -746,6 +824,19 @@ var drawSeparateSeries = (ctx, renderContext, values, color, width, minOverride,
|
|
|
746
824
|
break;
|
|
747
825
|
}
|
|
748
826
|
}
|
|
827
|
+
if (options.valueLine && latestValue !== null && latestValue >= minValue && latestValue <= maxValue) {
|
|
828
|
+
ctx.save();
|
|
829
|
+
ctx.lineWidth = 1;
|
|
830
|
+
ctx.setLineDash([3, 3]);
|
|
831
|
+
ctx.globalAlpha = 0.65;
|
|
832
|
+
ctx.strokeStyle = color;
|
|
833
|
+
const y = yFromValue(latestValue);
|
|
834
|
+
ctx.beginPath();
|
|
835
|
+
ctx.moveTo(renderContext.chartLeft, y);
|
|
836
|
+
ctx.lineTo(renderContext.chartRight, y);
|
|
837
|
+
ctx.stroke();
|
|
838
|
+
ctx.restore();
|
|
839
|
+
}
|
|
749
840
|
const decimals = options.decimals ?? 2;
|
|
750
841
|
const formatValue = (value) => value.toFixed(decimals);
|
|
751
842
|
const axisTicks = options.axisTicks ?? guideLines;
|
|
@@ -1073,7 +1164,7 @@ var BUILTIN_STDDEV_INDICATOR = {
|
|
|
1073
1164
|
name: "StdDev",
|
|
1074
1165
|
pane: "separate",
|
|
1075
1166
|
paneHeightRatio: 0.16,
|
|
1076
|
-
defaultInputs: { length: 20, source: "close", color: "#f97316", width: 2 },
|
|
1167
|
+
defaultInputs: { length: 20, source: "close", color: "#f97316", width: 2, showValueLine: false },
|
|
1077
1168
|
draw: (ctx, renderContext, inputs) => {
|
|
1078
1169
|
const length = clampIndicatorLength(inputs.length, 20);
|
|
1079
1170
|
const values = withCachedSeries(
|
|
@@ -1083,7 +1174,8 @@ var BUILTIN_STDDEV_INDICATOR = {
|
|
|
1083
1174
|
);
|
|
1084
1175
|
return drawSeparateSeries(ctx, renderContext, values, inputs.color ?? "#f97316", Number(inputs.width) || 2, void 0, void 0, void 0, {
|
|
1085
1176
|
title: `StdDev ${length}`,
|
|
1086
|
-
decimals: 2
|
|
1177
|
+
decimals: 2,
|
|
1178
|
+
valueLine: inputs.showValueLine === true
|
|
1087
1179
|
});
|
|
1088
1180
|
}
|
|
1089
1181
|
};
|
|
@@ -1092,13 +1184,14 @@ var BUILTIN_ATR_INDICATOR = {
|
|
|
1092
1184
|
name: "ATR",
|
|
1093
1185
|
pane: "separate",
|
|
1094
1186
|
paneHeightRatio: 0.16,
|
|
1095
|
-
defaultInputs: { length: 14, color: "#eab308", width: 2 },
|
|
1187
|
+
defaultInputs: { length: 14, color: "#eab308", width: 2, showValueLine: false },
|
|
1096
1188
|
draw: (ctx, renderContext, inputs) => {
|
|
1097
1189
|
const length = clampIndicatorLength(inputs.length, 14);
|
|
1098
1190
|
const values = withCachedSeries(`atr|${length}`, renderContext.data, () => computeAtrSeries(renderContext.data, length));
|
|
1099
1191
|
return drawSeparateSeries(ctx, renderContext, values, inputs.color ?? "#eab308", Number(inputs.width) || 2, void 0, void 0, void 0, {
|
|
1100
1192
|
title: `ATR ${length}`,
|
|
1101
|
-
decimals: 2
|
|
1193
|
+
decimals: 2,
|
|
1194
|
+
valueLine: inputs.showValueLine === true
|
|
1102
1195
|
});
|
|
1103
1196
|
}
|
|
1104
1197
|
};
|
|
@@ -1114,7 +1207,8 @@ var BUILTIN_RSI_INDICATOR = {
|
|
|
1114
1207
|
showLegend: true,
|
|
1115
1208
|
showValueLabel: true,
|
|
1116
1209
|
showGuideLines: true,
|
|
1117
|
-
showScaleLabels: true
|
|
1210
|
+
showScaleLabels: true,
|
|
1211
|
+
showValueLine: false
|
|
1118
1212
|
},
|
|
1119
1213
|
draw: (ctx, renderContext, inputs) => {
|
|
1120
1214
|
const length = clampIndicatorLength(inputs.length, 14);
|
|
@@ -1137,11 +1231,1006 @@ var BUILTIN_RSI_INDICATOR = {
|
|
|
1137
1231
|
valueLabelColor: "#9E9E9E",
|
|
1138
1232
|
valueLabelBackgroundColor: "#9E9E9E",
|
|
1139
1233
|
valueLabelTextColor: "#0f172a",
|
|
1140
|
-
showLegend: inputs.showLegend !== false
|
|
1234
|
+
showLegend: inputs.showLegend !== false,
|
|
1235
|
+
valueLine: inputs.showValueLine === true
|
|
1236
|
+
}
|
|
1237
|
+
);
|
|
1238
|
+
}
|
|
1239
|
+
};
|
|
1240
|
+
var rmaFromValues = (values, length) => {
|
|
1241
|
+
const result = new Array(values.length).fill(null);
|
|
1242
|
+
let prev = null;
|
|
1243
|
+
let seedSum = 0;
|
|
1244
|
+
let seedCount = 0;
|
|
1245
|
+
for (let i = 0; i < values.length; i += 1) {
|
|
1246
|
+
const value = values[i];
|
|
1247
|
+
if (value == null || !Number.isFinite(value)) continue;
|
|
1248
|
+
if (prev === null) {
|
|
1249
|
+
seedSum += value;
|
|
1250
|
+
seedCount += 1;
|
|
1251
|
+
if (seedCount === length) {
|
|
1252
|
+
prev = seedSum / length;
|
|
1253
|
+
result[i] = prev;
|
|
1254
|
+
}
|
|
1255
|
+
continue;
|
|
1256
|
+
}
|
|
1257
|
+
prev = (prev * (length - 1) + value) / length;
|
|
1258
|
+
result[i] = prev;
|
|
1259
|
+
}
|
|
1260
|
+
return result;
|
|
1261
|
+
};
|
|
1262
|
+
var formatCompactNumber = (value) => {
|
|
1263
|
+
const abs = Math.abs(value);
|
|
1264
|
+
if (abs >= 1e9) return `${(value / 1e9).toFixed(2)}B`;
|
|
1265
|
+
if (abs >= 1e6) return `${(value / 1e6).toFixed(2)}M`;
|
|
1266
|
+
if (abs >= 1e3) return `${(value / 1e3).toFixed(1)}K`;
|
|
1267
|
+
return value.toFixed(0);
|
|
1268
|
+
};
|
|
1269
|
+
var drawSeparateMultiSeries = (ctx, renderContext, seriesList, options = {}) => {
|
|
1270
|
+
let min = Number.POSITIVE_INFINITY;
|
|
1271
|
+
let max = Number.NEGATIVE_INFINITY;
|
|
1272
|
+
for (const spec of seriesList) {
|
|
1273
|
+
for (let index = renderContext.startIndex; index <= renderContext.endIndex; index += 1) {
|
|
1274
|
+
const value = spec.values[index];
|
|
1275
|
+
if (value == null || !Number.isFinite(value)) continue;
|
|
1276
|
+
if (value < min) min = value;
|
|
1277
|
+
if (value > max) max = value;
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
if (min > max) return void 0;
|
|
1281
|
+
if (options.includeZero || seriesList.some((spec) => spec.histogram)) {
|
|
1282
|
+
min = Math.min(min, 0);
|
|
1283
|
+
max = Math.max(max, 0);
|
|
1284
|
+
}
|
|
1285
|
+
const minValue = options.minOverride ?? min;
|
|
1286
|
+
const maxValue = options.maxOverride ?? max;
|
|
1287
|
+
const range = maxValue - minValue || 1;
|
|
1288
|
+
const yFromValue = (value) => {
|
|
1289
|
+
const ratio = (value - minValue) / range;
|
|
1290
|
+
return renderContext.chartBottom - ratio * renderContext.chartHeight;
|
|
1291
|
+
};
|
|
1292
|
+
if (options.guideLines && options.guideLines.length > 0) {
|
|
1293
|
+
ctx.save();
|
|
1294
|
+
ctx.strokeStyle = "rgba(148,163,184,0.35)";
|
|
1295
|
+
ctx.lineWidth = 1;
|
|
1296
|
+
ctx.setLineDash([4, 4]);
|
|
1297
|
+
for (const guide of options.guideLines) {
|
|
1298
|
+
const y = yFromValue(guide);
|
|
1299
|
+
ctx.beginPath();
|
|
1300
|
+
ctx.moveTo(renderContext.chartLeft, y);
|
|
1301
|
+
ctx.lineTo(renderContext.chartRight, y);
|
|
1302
|
+
ctx.stroke();
|
|
1303
|
+
}
|
|
1304
|
+
ctx.restore();
|
|
1305
|
+
}
|
|
1306
|
+
for (const spec of seriesList) {
|
|
1307
|
+
if (!spec.histogram) continue;
|
|
1308
|
+
const zeroY = yFromValue(0);
|
|
1309
|
+
const barWidth = Math.max(
|
|
1310
|
+
1,
|
|
1311
|
+
Math.min(Math.max(1, renderContext.candleSpacing - 1), Math.floor(renderContext.candleSpacing * 0.7))
|
|
1312
|
+
);
|
|
1313
|
+
const positive = new Path2D();
|
|
1314
|
+
const negative = new Path2D();
|
|
1315
|
+
for (let index = renderContext.startIndex; index <= renderContext.endIndex; index += 1) {
|
|
1316
|
+
const value = spec.values[index];
|
|
1317
|
+
if (value == null || !Number.isFinite(value)) continue;
|
|
1318
|
+
const x = Math.round(renderContext.xFromIndex(index) - barWidth / 2);
|
|
1319
|
+
const y = yFromValue(value);
|
|
1320
|
+
const top = Math.min(y, zeroY);
|
|
1321
|
+
const height = Math.max(1, Math.abs(y - zeroY));
|
|
1322
|
+
(value >= 0 ? positive : negative).rect(x, top, barWidth, height);
|
|
1323
|
+
}
|
|
1324
|
+
ctx.save();
|
|
1325
|
+
ctx.globalAlpha = 0.85;
|
|
1326
|
+
ctx.fillStyle = spec.color;
|
|
1327
|
+
ctx.fill(positive);
|
|
1328
|
+
ctx.fillStyle = spec.negativeColor ?? spec.color;
|
|
1329
|
+
ctx.fill(negative);
|
|
1330
|
+
ctx.restore();
|
|
1331
|
+
}
|
|
1332
|
+
for (const spec of seriesList) {
|
|
1333
|
+
if (spec.histogram) continue;
|
|
1334
|
+
ctx.save();
|
|
1335
|
+
ctx.strokeStyle = spec.color;
|
|
1336
|
+
ctx.lineWidth = Math.max(1, spec.width ?? 2);
|
|
1337
|
+
ctx.setLineDash([]);
|
|
1338
|
+
let drawing = false;
|
|
1339
|
+
for (let index = renderContext.startIndex; index <= renderContext.endIndex; index += 1) {
|
|
1340
|
+
const value = spec.values[index];
|
|
1341
|
+
if (!Number.isFinite(value ?? Number.NaN)) {
|
|
1342
|
+
drawing = false;
|
|
1343
|
+
continue;
|
|
1344
|
+
}
|
|
1345
|
+
const x = renderContext.xFromIndex(index);
|
|
1346
|
+
const y = yFromValue(value);
|
|
1347
|
+
if (!drawing) {
|
|
1348
|
+
ctx.beginPath();
|
|
1349
|
+
ctx.moveTo(x, y);
|
|
1350
|
+
drawing = true;
|
|
1351
|
+
} else {
|
|
1352
|
+
ctx.lineTo(x, y);
|
|
1353
|
+
}
|
|
1354
|
+
const nextValue = spec.values[index + 1];
|
|
1355
|
+
if (!Number.isFinite(nextValue ?? Number.NaN) || index === renderContext.endIndex) {
|
|
1356
|
+
ctx.stroke();
|
|
1357
|
+
drawing = false;
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
ctx.restore();
|
|
1361
|
+
}
|
|
1362
|
+
const decimals = options.decimals ?? 2;
|
|
1363
|
+
const formatValue = options.format ?? ((value) => value.toFixed(decimals));
|
|
1364
|
+
const latestOf = (values) => {
|
|
1365
|
+
for (let index = values.length - 1; index >= 0; index -= 1) {
|
|
1366
|
+
const value = values[index];
|
|
1367
|
+
if (Number.isFinite(value ?? Number.NaN)) return value;
|
|
1368
|
+
}
|
|
1369
|
+
return null;
|
|
1370
|
+
};
|
|
1371
|
+
if (options.valueLines) {
|
|
1372
|
+
ctx.save();
|
|
1373
|
+
ctx.lineWidth = 1;
|
|
1374
|
+
ctx.setLineDash([3, 3]);
|
|
1375
|
+
ctx.globalAlpha = 0.65;
|
|
1376
|
+
for (const spec of seriesList) {
|
|
1377
|
+
if (spec.histogram) continue;
|
|
1378
|
+
const latest = latestOf(spec.values);
|
|
1379
|
+
if (latest === null || latest < minValue || latest > maxValue) continue;
|
|
1380
|
+
const y = yFromValue(latest);
|
|
1381
|
+
ctx.strokeStyle = spec.color;
|
|
1382
|
+
ctx.beginPath();
|
|
1383
|
+
ctx.moveTo(renderContext.chartLeft, y);
|
|
1384
|
+
ctx.lineTo(renderContext.chartRight, y);
|
|
1385
|
+
ctx.stroke();
|
|
1386
|
+
}
|
|
1387
|
+
ctx.restore();
|
|
1388
|
+
}
|
|
1389
|
+
const paneInfo = {
|
|
1390
|
+
...options.title ? { title: options.title } : {},
|
|
1391
|
+
axis: {
|
|
1392
|
+
min: minValue,
|
|
1393
|
+
max: maxValue,
|
|
1394
|
+
...options.axisTicks ? { ticks: options.axisTicks } : options.guideLines ? { ticks: options.guideLines } : {},
|
|
1395
|
+
decimals,
|
|
1396
|
+
format: formatValue
|
|
1397
|
+
}
|
|
1398
|
+
};
|
|
1399
|
+
if (options.guideLines) {
|
|
1400
|
+
paneInfo.guideLines = options.guideLines.map((value) => ({
|
|
1401
|
+
value,
|
|
1402
|
+
label: formatValue(value),
|
|
1403
|
+
style: "dotted"
|
|
1404
|
+
}));
|
|
1405
|
+
}
|
|
1406
|
+
const legendValues = [];
|
|
1407
|
+
for (const spec of seriesList) {
|
|
1408
|
+
const latest = latestOf(spec.values);
|
|
1409
|
+
if (latest === null) continue;
|
|
1410
|
+
legendValues.push({
|
|
1411
|
+
...spec.label ? { label: spec.label } : {},
|
|
1412
|
+
value: latest,
|
|
1413
|
+
text: formatValue(latest),
|
|
1414
|
+
color: spec.color
|
|
1415
|
+
});
|
|
1416
|
+
}
|
|
1417
|
+
if (legendValues.length > 0) {
|
|
1418
|
+
paneInfo.legendValues = legendValues;
|
|
1419
|
+
}
|
|
1420
|
+
const labelSpec = seriesList[options.valueLabelSeriesIndex ?? 0];
|
|
1421
|
+
const labelLatest = labelSpec ? latestOf(labelSpec.values) : null;
|
|
1422
|
+
if (labelSpec && labelLatest !== null) {
|
|
1423
|
+
paneInfo.valueLabels = [
|
|
1424
|
+
{
|
|
1425
|
+
value: labelLatest,
|
|
1426
|
+
text: formatValue(labelLatest),
|
|
1427
|
+
color: labelSpec.color,
|
|
1428
|
+
backgroundColor: labelSpec.color,
|
|
1429
|
+
textColor: "#0f172a"
|
|
1430
|
+
}
|
|
1431
|
+
];
|
|
1432
|
+
}
|
|
1433
|
+
return paneInfo;
|
|
1434
|
+
};
|
|
1435
|
+
var computeMacd = (data, fast, slow, signalLength) => {
|
|
1436
|
+
const fastEma = computeEmaSeries(data, fast, "close");
|
|
1437
|
+
const slowEma = computeEmaSeries(data, slow, "close");
|
|
1438
|
+
const macd = fastEma.map((value, idx) => {
|
|
1439
|
+
const slowValue = slowEma[idx];
|
|
1440
|
+
return value == null || slowValue == null ? null : value - slowValue;
|
|
1441
|
+
});
|
|
1442
|
+
const signal = emaFromValues(macd, signalLength);
|
|
1443
|
+
const hist = macd.map((value, idx) => {
|
|
1444
|
+
const signalValue = signal[idx];
|
|
1445
|
+
return value == null || signalValue == null ? null : value - signalValue;
|
|
1446
|
+
});
|
|
1447
|
+
return { macd, signal, hist };
|
|
1448
|
+
};
|
|
1449
|
+
var computeStochastic = (data, kLength, kSmoothing, dLength) => {
|
|
1450
|
+
const highestHigh = rollingExtremeSeries(data.map((point) => point.h), kLength, "max");
|
|
1451
|
+
const lowestLow = rollingExtremeSeries(data.map((point) => point.l), kLength, "min");
|
|
1452
|
+
const rawK = data.map((point, idx) => {
|
|
1453
|
+
const hh = highestHigh[idx];
|
|
1454
|
+
const ll = lowestLow[idx];
|
|
1455
|
+
if (hh == null || ll == null) return null;
|
|
1456
|
+
const range = hh - ll;
|
|
1457
|
+
return range === 0 ? 50 : (point.c - ll) / range * 100;
|
|
1458
|
+
});
|
|
1459
|
+
const k = kSmoothing > 1 ? smaFromValues(rawK, kSmoothing) : rawK;
|
|
1460
|
+
const d = smaFromValues(k, dLength);
|
|
1461
|
+
return { k, d };
|
|
1462
|
+
};
|
|
1463
|
+
var computeStochRsi = (data, rsiLength, stochLength, kSmoothing, dSmoothing) => {
|
|
1464
|
+
const rsi = computeRsiSeries(data, rsiLength);
|
|
1465
|
+
const finiteRsi = rsi.map((value) => value == null ? Number.NaN : value);
|
|
1466
|
+
const highest = rollingExtremeSeries(finiteRsi, stochLength, "max");
|
|
1467
|
+
const lowest = rollingExtremeSeries(finiteRsi, stochLength, "min");
|
|
1468
|
+
const rawK = rsi.map((value, idx) => {
|
|
1469
|
+
const hh = highest[idx];
|
|
1470
|
+
const ll = lowest[idx];
|
|
1471
|
+
if (value == null || hh == null || ll == null || !Number.isFinite(hh) || !Number.isFinite(ll)) return null;
|
|
1472
|
+
const range = hh - ll;
|
|
1473
|
+
return range === 0 ? 50 : (value - ll) / range * 100;
|
|
1474
|
+
});
|
|
1475
|
+
const k = smaFromValues(rawK, Math.max(1, kSmoothing));
|
|
1476
|
+
const d = smaFromValues(k, Math.max(1, dSmoothing));
|
|
1477
|
+
return { k, d };
|
|
1478
|
+
};
|
|
1479
|
+
var computeDmi = (data, length) => {
|
|
1480
|
+
const count = data.length;
|
|
1481
|
+
const plusDm = new Array(count).fill(null);
|
|
1482
|
+
const minusDm = new Array(count).fill(null);
|
|
1483
|
+
const trueRange = new Array(count).fill(null);
|
|
1484
|
+
for (let i = 1; i < count; i += 1) {
|
|
1485
|
+
const point = data[i];
|
|
1486
|
+
const prev = data[i - 1];
|
|
1487
|
+
const upMove = point.h - prev.h;
|
|
1488
|
+
const downMove = prev.l - point.l;
|
|
1489
|
+
plusDm[i] = upMove > downMove && upMove > 0 ? upMove : 0;
|
|
1490
|
+
minusDm[i] = downMove > upMove && downMove > 0 ? downMove : 0;
|
|
1491
|
+
trueRange[i] = Math.max(point.h - point.l, Math.abs(point.h - prev.c), Math.abs(point.l - prev.c));
|
|
1492
|
+
}
|
|
1493
|
+
const smoothPlus = rmaFromValues(plusDm, length);
|
|
1494
|
+
const smoothMinus = rmaFromValues(minusDm, length);
|
|
1495
|
+
const smoothTr = rmaFromValues(trueRange, length);
|
|
1496
|
+
const plusDi = smoothPlus.map((value, idx) => {
|
|
1497
|
+
const tr = smoothTr[idx];
|
|
1498
|
+
return value == null || tr == null || tr === 0 ? null : 100 * value / tr;
|
|
1499
|
+
});
|
|
1500
|
+
const minusDi = smoothMinus.map((value, idx) => {
|
|
1501
|
+
const tr = smoothTr[idx];
|
|
1502
|
+
return value == null || tr == null || tr === 0 ? null : 100 * value / tr;
|
|
1503
|
+
});
|
|
1504
|
+
const dx = plusDi.map((value, idx) => {
|
|
1505
|
+
const minus = minusDi[idx];
|
|
1506
|
+
if (value == null || minus == null) return null;
|
|
1507
|
+
const sum = value + minus;
|
|
1508
|
+
return sum === 0 ? 0 : 100 * Math.abs(value - minus) / sum;
|
|
1509
|
+
});
|
|
1510
|
+
const adx = rmaFromValues(dx, length);
|
|
1511
|
+
return { adx, plusDi, minusDi };
|
|
1512
|
+
};
|
|
1513
|
+
var computeObvSeries = (data) => {
|
|
1514
|
+
const result = new Array(data.length).fill(null);
|
|
1515
|
+
let obv = 0;
|
|
1516
|
+
for (let i = 0; i < data.length; i += 1) {
|
|
1517
|
+
const point = data[i];
|
|
1518
|
+
if (i > 0) {
|
|
1519
|
+
const prev = data[i - 1];
|
|
1520
|
+
const volume = Math.max(0, point.v ?? 0);
|
|
1521
|
+
if (point.c > prev.c) obv += volume;
|
|
1522
|
+
else if (point.c < prev.c) obv -= volume;
|
|
1523
|
+
}
|
|
1524
|
+
result[i] = obv;
|
|
1525
|
+
}
|
|
1526
|
+
return result;
|
|
1527
|
+
};
|
|
1528
|
+
var computeMfiSeries = (data, length) => {
|
|
1529
|
+
const result = new Array(data.length).fill(null);
|
|
1530
|
+
const positiveFlow = new Array(data.length).fill(0);
|
|
1531
|
+
const negativeFlow = new Array(data.length).fill(0);
|
|
1532
|
+
let prevTypical = null;
|
|
1533
|
+
for (let i = 0; i < data.length; i += 1) {
|
|
1534
|
+
const point = data[i];
|
|
1535
|
+
const typical = (point.h + point.l + point.c) / 3;
|
|
1536
|
+
const flow = typical * Math.max(0, point.v ?? 0);
|
|
1537
|
+
if (prevTypical !== null) {
|
|
1538
|
+
if (typical > prevTypical) positiveFlow[i] = flow;
|
|
1539
|
+
else if (typical < prevTypical) negativeFlow[i] = flow;
|
|
1540
|
+
}
|
|
1541
|
+
prevTypical = typical;
|
|
1542
|
+
}
|
|
1543
|
+
let posSum = 0;
|
|
1544
|
+
let negSum = 0;
|
|
1545
|
+
for (let i = 0; i < data.length; i += 1) {
|
|
1546
|
+
posSum += positiveFlow[i];
|
|
1547
|
+
negSum += negativeFlow[i];
|
|
1548
|
+
if (i >= length) {
|
|
1549
|
+
posSum -= positiveFlow[i - length];
|
|
1550
|
+
negSum -= negativeFlow[i - length];
|
|
1551
|
+
}
|
|
1552
|
+
if (i >= length) {
|
|
1553
|
+
result[i] = negSum === 0 ? 100 : 100 - 100 / (1 + posSum / negSum);
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
return result;
|
|
1557
|
+
};
|
|
1558
|
+
var computeCciSeries = (data, length) => {
|
|
1559
|
+
const result = new Array(data.length).fill(null);
|
|
1560
|
+
const typical = data.map((point) => (point.h + point.l + point.c) / 3);
|
|
1561
|
+
const smaTypical = smaFromValues(typical, length);
|
|
1562
|
+
for (let i = length - 1; i < data.length; i += 1) {
|
|
1563
|
+
const mean = smaTypical[i];
|
|
1564
|
+
if (mean == null) continue;
|
|
1565
|
+
let meanDeviation = 0;
|
|
1566
|
+
for (let j = 0; j < length; j += 1) {
|
|
1567
|
+
meanDeviation += Math.abs(typical[i - j] - mean);
|
|
1568
|
+
}
|
|
1569
|
+
meanDeviation /= length;
|
|
1570
|
+
result[i] = meanDeviation === 0 ? 0 : (typical[i] - mean) / (0.015 * meanDeviation);
|
|
1571
|
+
}
|
|
1572
|
+
return result;
|
|
1573
|
+
};
|
|
1574
|
+
var computeWilliamsRSeries = (data, length) => {
|
|
1575
|
+
const highest = rollingExtremeSeries(data.map((point) => point.h), length, "max");
|
|
1576
|
+
const lowest = rollingExtremeSeries(data.map((point) => point.l), length, "min");
|
|
1577
|
+
return data.map((point, idx) => {
|
|
1578
|
+
const hh = highest[idx];
|
|
1579
|
+
const ll = lowest[idx];
|
|
1580
|
+
if (hh == null || ll == null) return null;
|
|
1581
|
+
const range = hh - ll;
|
|
1582
|
+
return range === 0 ? -50 : (hh - point.c) / range * -100;
|
|
1583
|
+
});
|
|
1584
|
+
};
|
|
1585
|
+
var computeRocSeries = (data, length) => {
|
|
1586
|
+
return data.map((point, idx) => {
|
|
1587
|
+
if (idx < length) return null;
|
|
1588
|
+
const past = data[idx - length].c;
|
|
1589
|
+
return past === 0 ? null : 100 * (point.c - past) / past;
|
|
1590
|
+
});
|
|
1591
|
+
};
|
|
1592
|
+
var computeMomentumSeries = (data, length) => {
|
|
1593
|
+
return data.map((point, idx) => idx < length ? null : point.c - data[idx - length].c);
|
|
1594
|
+
};
|
|
1595
|
+
var computePsarSeries = (data, start, increment, maximum) => {
|
|
1596
|
+
const count = data.length;
|
|
1597
|
+
const result = new Array(count).fill(null);
|
|
1598
|
+
if (count < 2) return result;
|
|
1599
|
+
let isUp = data[1].c >= data[0].c;
|
|
1600
|
+
let sar = isUp ? data[0].l : data[0].h;
|
|
1601
|
+
let extremePoint = isUp ? data[0].h : data[0].l;
|
|
1602
|
+
let accelerationFactor = start;
|
|
1603
|
+
for (let i = 1; i < count; i += 1) {
|
|
1604
|
+
const point = data[i];
|
|
1605
|
+
sar += accelerationFactor * (extremePoint - sar);
|
|
1606
|
+
if (isUp) {
|
|
1607
|
+
sar = Math.min(sar, data[i - 1].l, data[Math.max(0, i - 2)].l);
|
|
1608
|
+
if (point.l < sar) {
|
|
1609
|
+
isUp = false;
|
|
1610
|
+
sar = extremePoint;
|
|
1611
|
+
extremePoint = point.l;
|
|
1612
|
+
accelerationFactor = start;
|
|
1613
|
+
} else if (point.h > extremePoint) {
|
|
1614
|
+
extremePoint = point.h;
|
|
1615
|
+
accelerationFactor = Math.min(maximum, accelerationFactor + increment);
|
|
1616
|
+
}
|
|
1617
|
+
} else {
|
|
1618
|
+
sar = Math.max(sar, data[i - 1].h, data[Math.max(0, i - 2)].h);
|
|
1619
|
+
if (point.h > sar) {
|
|
1620
|
+
isUp = true;
|
|
1621
|
+
sar = extremePoint;
|
|
1622
|
+
extremePoint = point.h;
|
|
1623
|
+
accelerationFactor = start;
|
|
1624
|
+
} else if (point.l < extremePoint) {
|
|
1625
|
+
extremePoint = point.l;
|
|
1626
|
+
accelerationFactor = Math.min(maximum, accelerationFactor + increment);
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
result[i] = sar;
|
|
1630
|
+
}
|
|
1631
|
+
return result;
|
|
1632
|
+
};
|
|
1633
|
+
var computeSuperTrend = (data, atrLength, multiplier) => {
|
|
1634
|
+
const count = data.length;
|
|
1635
|
+
const up = new Array(count).fill(null);
|
|
1636
|
+
const down = new Array(count).fill(null);
|
|
1637
|
+
const atr = computeAtrSeries(data, atrLength);
|
|
1638
|
+
let prevUpper = Number.NaN;
|
|
1639
|
+
let prevLower = Number.NaN;
|
|
1640
|
+
let trend = 1;
|
|
1641
|
+
let prevClose = 0;
|
|
1642
|
+
for (let i = 0; i < count; i += 1) {
|
|
1643
|
+
const point = data[i];
|
|
1644
|
+
const atrValue = atr[i];
|
|
1645
|
+
if (atrValue == null) {
|
|
1646
|
+
prevClose = point.c;
|
|
1647
|
+
continue;
|
|
1648
|
+
}
|
|
1649
|
+
const mid = (point.h + point.l) / 2;
|
|
1650
|
+
const basicUpper = mid + multiplier * atrValue;
|
|
1651
|
+
const basicLower = mid - multiplier * atrValue;
|
|
1652
|
+
if (!Number.isFinite(prevUpper)) {
|
|
1653
|
+
prevUpper = basicUpper;
|
|
1654
|
+
prevLower = basicLower;
|
|
1655
|
+
trend = point.c >= mid ? 1 : -1;
|
|
1656
|
+
} else {
|
|
1657
|
+
const finalUpper = basicUpper < prevUpper || prevClose > prevUpper ? basicUpper : prevUpper;
|
|
1658
|
+
const finalLower = basicLower > prevLower || prevClose < prevLower ? basicLower : prevLower;
|
|
1659
|
+
if (trend === 1) {
|
|
1660
|
+
trend = point.c < finalLower ? -1 : 1;
|
|
1661
|
+
} else {
|
|
1662
|
+
trend = point.c > finalUpper ? 1 : -1;
|
|
1663
|
+
}
|
|
1664
|
+
prevUpper = finalUpper;
|
|
1665
|
+
prevLower = finalLower;
|
|
1666
|
+
}
|
|
1667
|
+
if (trend === 1) up[i] = prevLower;
|
|
1668
|
+
else down[i] = prevUpper;
|
|
1669
|
+
prevClose = point.c;
|
|
1670
|
+
}
|
|
1671
|
+
return { up, down };
|
|
1672
|
+
};
|
|
1673
|
+
var computeIchimoku = (data, conversionLength, baseLength, spanBLength, displacement) => {
|
|
1674
|
+
const count = data.length;
|
|
1675
|
+
const highs = data.map((point) => point.h);
|
|
1676
|
+
const lows = data.map((point) => point.l);
|
|
1677
|
+
const midlineOf = (length) => {
|
|
1678
|
+
const highest = rollingExtremeSeries(highs, length, "max");
|
|
1679
|
+
const lowest = rollingExtremeSeries(lows, length, "min");
|
|
1680
|
+
return highest.map((value, idx) => {
|
|
1681
|
+
const low = lowest[idx];
|
|
1682
|
+
return value == null || low == null ? null : (value + low) / 2;
|
|
1683
|
+
});
|
|
1684
|
+
};
|
|
1685
|
+
const tenkan = midlineOf(conversionLength);
|
|
1686
|
+
const kijun = midlineOf(baseLength);
|
|
1687
|
+
const spanARaw = tenkan.map((value, idx) => {
|
|
1688
|
+
const base = kijun[idx];
|
|
1689
|
+
return value == null || base == null ? null : (value + base) / 2;
|
|
1690
|
+
});
|
|
1691
|
+
const spanBRaw = midlineOf(spanBLength);
|
|
1692
|
+
const spanA = new Array(count).fill(null);
|
|
1693
|
+
const spanB = new Array(count).fill(null);
|
|
1694
|
+
const chikou = new Array(count).fill(null);
|
|
1695
|
+
for (let i = 0; i < count; i += 1) {
|
|
1696
|
+
const shifted = i - displacement;
|
|
1697
|
+
if (shifted >= 0) {
|
|
1698
|
+
spanA[i] = spanARaw[shifted] ?? null;
|
|
1699
|
+
spanB[i] = spanBRaw[shifted] ?? null;
|
|
1700
|
+
}
|
|
1701
|
+
const forward = i + displacement;
|
|
1702
|
+
if (forward < count) {
|
|
1703
|
+
chikou[i] = data[forward].c;
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
return { tenkan, kijun, spanA, spanB, chikou };
|
|
1707
|
+
};
|
|
1708
|
+
var computeKeltner = (data, emaLength, atrLength, multiplier) => {
|
|
1709
|
+
const basis = computeEmaSeries(data, emaLength, "close");
|
|
1710
|
+
const atr = computeAtrSeries(data, atrLength);
|
|
1711
|
+
const upper = basis.map((value, idx) => {
|
|
1712
|
+
const atrValue = atr[idx];
|
|
1713
|
+
return value == null || atrValue == null ? null : value + multiplier * atrValue;
|
|
1714
|
+
});
|
|
1715
|
+
const lower = basis.map((value, idx) => {
|
|
1716
|
+
const atrValue = atr[idx];
|
|
1717
|
+
return value == null || atrValue == null ? null : value - multiplier * atrValue;
|
|
1718
|
+
});
|
|
1719
|
+
return { basis, upper, lower };
|
|
1720
|
+
};
|
|
1721
|
+
var computeDonchian = (data, length) => {
|
|
1722
|
+
const upper = rollingExtremeSeries(data.map((point) => point.h), length, "max");
|
|
1723
|
+
const lower = rollingExtremeSeries(data.map((point) => point.l), length, "min");
|
|
1724
|
+
const basis = upper.map((value, idx) => {
|
|
1725
|
+
const low = lower[idx];
|
|
1726
|
+
return value == null || low == null ? null : (value + low) / 2;
|
|
1727
|
+
});
|
|
1728
|
+
return { upper, lower, basis };
|
|
1729
|
+
};
|
|
1730
|
+
var BUILTIN_MACD_INDICATOR = {
|
|
1731
|
+
id: "macd",
|
|
1732
|
+
name: "MACD",
|
|
1733
|
+
pane: "separate",
|
|
1734
|
+
paneHeightRatio: 0.2,
|
|
1735
|
+
defaultInputs: {
|
|
1736
|
+
fast: 12,
|
|
1737
|
+
slow: 26,
|
|
1738
|
+
signal: 9,
|
|
1739
|
+
macdColor: "#2962ff",
|
|
1740
|
+
signalColor: "#ff6d00",
|
|
1741
|
+
histUpColor: "#26a69a",
|
|
1742
|
+
histDownColor: "#ef5350",
|
|
1743
|
+
showValueLine: false
|
|
1744
|
+
},
|
|
1745
|
+
draw: (ctx, renderContext, inputs) => {
|
|
1746
|
+
const fast = clampIndicatorLength(inputs.fast, 12);
|
|
1747
|
+
const slow = clampIndicatorLength(inputs.slow, 26);
|
|
1748
|
+
const signalLength = clampIndicatorLength(inputs.signal, 9);
|
|
1749
|
+
const { macd, signal, hist } = withCachedComputation(
|
|
1750
|
+
`macd|${fast}|${slow}|${signalLength}`,
|
|
1751
|
+
renderContext.data,
|
|
1752
|
+
() => computeMacd(renderContext.data, fast, slow, signalLength)
|
|
1753
|
+
);
|
|
1754
|
+
return drawSeparateMultiSeries(
|
|
1755
|
+
ctx,
|
|
1756
|
+
renderContext,
|
|
1757
|
+
[
|
|
1758
|
+
{
|
|
1759
|
+
values: hist,
|
|
1760
|
+
color: inputs.histUpColor ?? "#26a69a",
|
|
1761
|
+
negativeColor: inputs.histDownColor ?? "#ef5350",
|
|
1762
|
+
histogram: true
|
|
1763
|
+
},
|
|
1764
|
+
{ values: macd, color: inputs.macdColor ?? "#2962ff", label: "MACD" },
|
|
1765
|
+
{ values: signal, color: inputs.signalColor ?? "#ff6d00", label: "Signal" }
|
|
1766
|
+
],
|
|
1767
|
+
{
|
|
1768
|
+
title: `MACD ${fast} ${slow} ${signalLength}`,
|
|
1769
|
+
includeZero: true,
|
|
1770
|
+
guideLines: [0],
|
|
1771
|
+
decimals: 2,
|
|
1772
|
+
valueLabelSeriesIndex: 1,
|
|
1773
|
+
valueLines: inputs.showValueLine === true
|
|
1774
|
+
}
|
|
1775
|
+
);
|
|
1776
|
+
}
|
|
1777
|
+
};
|
|
1778
|
+
var BUILTIN_STOCHASTIC_INDICATOR = {
|
|
1779
|
+
id: "stochastic",
|
|
1780
|
+
name: "Stoch",
|
|
1781
|
+
pane: "separate",
|
|
1782
|
+
paneHeightRatio: 0.18,
|
|
1783
|
+
defaultInputs: { kLength: 14, kSmoothing: 1, dLength: 3, kColor: "#2962ff", dColor: "#ff6d00", showValueLine: false },
|
|
1784
|
+
draw: (ctx, renderContext, inputs) => {
|
|
1785
|
+
const kLength = clampIndicatorLength(inputs.kLength, 14);
|
|
1786
|
+
const kSmoothing = clampIndicatorLength(inputs.kSmoothing, 1);
|
|
1787
|
+
const dLength = clampIndicatorLength(inputs.dLength, 3);
|
|
1788
|
+
const { k, d } = withCachedComputation(
|
|
1789
|
+
`stochastic|${kLength}|${kSmoothing}|${dLength}`,
|
|
1790
|
+
renderContext.data,
|
|
1791
|
+
() => computeStochastic(renderContext.data, kLength, kSmoothing, dLength)
|
|
1792
|
+
);
|
|
1793
|
+
return drawSeparateMultiSeries(
|
|
1794
|
+
ctx,
|
|
1795
|
+
renderContext,
|
|
1796
|
+
[
|
|
1797
|
+
{ values: k, color: inputs.kColor ?? "#2962ff", label: "%K" },
|
|
1798
|
+
{ values: d, color: inputs.dColor ?? "#ff6d00", label: "%D" }
|
|
1799
|
+
],
|
|
1800
|
+
{
|
|
1801
|
+
title: `Stoch ${kLength} ${kSmoothing} ${dLength}`,
|
|
1802
|
+
minOverride: 0,
|
|
1803
|
+
maxOverride: 100,
|
|
1804
|
+
guideLines: [20, 80],
|
|
1805
|
+
axisTicks: [0, 20, 50, 80, 100],
|
|
1806
|
+
decimals: 2,
|
|
1807
|
+
valueLines: inputs.showValueLine === true
|
|
1808
|
+
}
|
|
1809
|
+
);
|
|
1810
|
+
}
|
|
1811
|
+
};
|
|
1812
|
+
var BUILTIN_STOCHRSI_INDICATOR = {
|
|
1813
|
+
id: "stochrsi",
|
|
1814
|
+
name: "Stoch RSI",
|
|
1815
|
+
pane: "separate",
|
|
1816
|
+
paneHeightRatio: 0.18,
|
|
1817
|
+
defaultInputs: {
|
|
1818
|
+
rsiLength: 14,
|
|
1819
|
+
stochLength: 14,
|
|
1820
|
+
kSmoothing: 3,
|
|
1821
|
+
dSmoothing: 3,
|
|
1822
|
+
kColor: "#2962ff",
|
|
1823
|
+
dColor: "#ff6d00",
|
|
1824
|
+
showValueLine: false
|
|
1825
|
+
},
|
|
1826
|
+
draw: (ctx, renderContext, inputs) => {
|
|
1827
|
+
const rsiLength = clampIndicatorLength(inputs.rsiLength, 14);
|
|
1828
|
+
const stochLength = clampIndicatorLength(inputs.stochLength, 14);
|
|
1829
|
+
const kSmoothing = clampIndicatorLength(inputs.kSmoothing, 3);
|
|
1830
|
+
const dSmoothing = clampIndicatorLength(inputs.dSmoothing, 3);
|
|
1831
|
+
const { k, d } = withCachedComputation(
|
|
1832
|
+
`stochrsi|${rsiLength}|${stochLength}|${kSmoothing}|${dSmoothing}`,
|
|
1833
|
+
renderContext.data,
|
|
1834
|
+
() => computeStochRsi(renderContext.data, rsiLength, stochLength, kSmoothing, dSmoothing)
|
|
1835
|
+
);
|
|
1836
|
+
return drawSeparateMultiSeries(
|
|
1837
|
+
ctx,
|
|
1838
|
+
renderContext,
|
|
1839
|
+
[
|
|
1840
|
+
{ values: k, color: inputs.kColor ?? "#2962ff", label: "%K" },
|
|
1841
|
+
{ values: d, color: inputs.dColor ?? "#ff6d00", label: "%D" }
|
|
1842
|
+
],
|
|
1843
|
+
{
|
|
1844
|
+
title: `Stoch RSI ${rsiLength} ${stochLength}`,
|
|
1845
|
+
minOverride: 0,
|
|
1846
|
+
maxOverride: 100,
|
|
1847
|
+
guideLines: [20, 80],
|
|
1848
|
+
axisTicks: [0, 20, 50, 80, 100],
|
|
1849
|
+
decimals: 2,
|
|
1850
|
+
valueLines: inputs.showValueLine === true
|
|
1851
|
+
}
|
|
1852
|
+
);
|
|
1853
|
+
}
|
|
1854
|
+
};
|
|
1855
|
+
var BUILTIN_ADX_INDICATOR = {
|
|
1856
|
+
id: "adx",
|
|
1857
|
+
name: "ADX/DMI",
|
|
1858
|
+
pane: "separate",
|
|
1859
|
+
paneHeightRatio: 0.18,
|
|
1860
|
+
defaultInputs: {
|
|
1861
|
+
length: 14,
|
|
1862
|
+
adxColor: "#ff6d00",
|
|
1863
|
+
plusDiColor: "#26a69a",
|
|
1864
|
+
minusDiColor: "#ef5350",
|
|
1865
|
+
showDi: true,
|
|
1866
|
+
showValueLine: false
|
|
1867
|
+
},
|
|
1868
|
+
draw: (ctx, renderContext, inputs) => {
|
|
1869
|
+
const length = clampIndicatorLength(inputs.length, 14);
|
|
1870
|
+
const { adx, plusDi, minusDi } = withCachedComputation(
|
|
1871
|
+
`adx|${length}`,
|
|
1872
|
+
renderContext.data,
|
|
1873
|
+
() => computeDmi(renderContext.data, length)
|
|
1874
|
+
);
|
|
1875
|
+
const seriesList = [
|
|
1876
|
+
{ values: adx, color: inputs.adxColor ?? "#ff6d00", label: "ADX" }
|
|
1877
|
+
];
|
|
1878
|
+
if (inputs.showDi !== false) {
|
|
1879
|
+
seriesList.push(
|
|
1880
|
+
{ values: plusDi, color: inputs.plusDiColor ?? "#26a69a", label: "+DI", width: 1 },
|
|
1881
|
+
{ values: minusDi, color: inputs.minusDiColor ?? "#ef5350", label: "-DI", width: 1 }
|
|
1882
|
+
);
|
|
1883
|
+
}
|
|
1884
|
+
return drawSeparateMultiSeries(ctx, renderContext, seriesList, {
|
|
1885
|
+
title: `ADX ${length}`,
|
|
1886
|
+
minOverride: 0,
|
|
1887
|
+
guideLines: [20],
|
|
1888
|
+
decimals: 2,
|
|
1889
|
+
valueLines: inputs.showValueLine === true
|
|
1890
|
+
});
|
|
1891
|
+
}
|
|
1892
|
+
};
|
|
1893
|
+
var BUILTIN_OBV_INDICATOR = {
|
|
1894
|
+
id: "obv",
|
|
1895
|
+
name: "OBV",
|
|
1896
|
+
pane: "separate",
|
|
1897
|
+
paneHeightRatio: 0.16,
|
|
1898
|
+
defaultInputs: { color: "#2962ff", width: 2, showValueLine: false },
|
|
1899
|
+
draw: (ctx, renderContext, inputs) => {
|
|
1900
|
+
const values = withCachedSeries("obv", renderContext.data, () => computeObvSeries(renderContext.data));
|
|
1901
|
+
return drawSeparateMultiSeries(
|
|
1902
|
+
ctx,
|
|
1903
|
+
renderContext,
|
|
1904
|
+
[{ values, color: inputs.color ?? "#2962ff", width: Number(inputs.width) || 2, label: "OBV" }],
|
|
1905
|
+
{ title: "OBV", format: formatCompactNumber, valueLines: inputs.showValueLine === true }
|
|
1906
|
+
);
|
|
1907
|
+
}
|
|
1908
|
+
};
|
|
1909
|
+
var BUILTIN_MFI_INDICATOR = {
|
|
1910
|
+
id: "mfi",
|
|
1911
|
+
name: "MFI",
|
|
1912
|
+
pane: "separate",
|
|
1913
|
+
paneHeightRatio: 0.16,
|
|
1914
|
+
defaultInputs: { length: 14, color: "#7e57c2", width: 2, showValueLine: false },
|
|
1915
|
+
draw: (ctx, renderContext, inputs) => {
|
|
1916
|
+
const length = clampIndicatorLength(inputs.length, 14);
|
|
1917
|
+
const values = withCachedSeries(
|
|
1918
|
+
`mfi|${length}`,
|
|
1919
|
+
renderContext.data,
|
|
1920
|
+
() => computeMfiSeries(renderContext.data, length)
|
|
1921
|
+
);
|
|
1922
|
+
return drawSeparateMultiSeries(
|
|
1923
|
+
ctx,
|
|
1924
|
+
renderContext,
|
|
1925
|
+
[{ values, color: inputs.color ?? "#7e57c2", width: Number(inputs.width) || 2 }],
|
|
1926
|
+
{
|
|
1927
|
+
title: `MFI ${length}`,
|
|
1928
|
+
minOverride: 0,
|
|
1929
|
+
maxOverride: 100,
|
|
1930
|
+
guideLines: [20, 80],
|
|
1931
|
+
axisTicks: [0, 20, 50, 80, 100],
|
|
1932
|
+
decimals: 2,
|
|
1933
|
+
valueLines: inputs.showValueLine === true
|
|
1934
|
+
}
|
|
1935
|
+
);
|
|
1936
|
+
}
|
|
1937
|
+
};
|
|
1938
|
+
var BUILTIN_CCI_INDICATOR = {
|
|
1939
|
+
id: "cci",
|
|
1940
|
+
name: "CCI",
|
|
1941
|
+
pane: "separate",
|
|
1942
|
+
paneHeightRatio: 0.16,
|
|
1943
|
+
defaultInputs: { length: 20, color: "#2962ff", width: 2, showValueLine: false },
|
|
1944
|
+
draw: (ctx, renderContext, inputs) => {
|
|
1945
|
+
const length = clampIndicatorLength(inputs.length, 20);
|
|
1946
|
+
const values = withCachedSeries(
|
|
1947
|
+
`cci|${length}`,
|
|
1948
|
+
renderContext.data,
|
|
1949
|
+
() => computeCciSeries(renderContext.data, length)
|
|
1950
|
+
);
|
|
1951
|
+
return drawSeparateMultiSeries(
|
|
1952
|
+
ctx,
|
|
1953
|
+
renderContext,
|
|
1954
|
+
[{ values, color: inputs.color ?? "#2962ff", width: Number(inputs.width) || 2 }],
|
|
1955
|
+
{ title: `CCI ${length}`, guideLines: [-100, 0, 100], decimals: 2, valueLines: inputs.showValueLine === true }
|
|
1956
|
+
);
|
|
1957
|
+
}
|
|
1958
|
+
};
|
|
1959
|
+
var BUILTIN_WILLIAMSR_INDICATOR = {
|
|
1960
|
+
id: "williamsr",
|
|
1961
|
+
name: "Williams %R",
|
|
1962
|
+
pane: "separate",
|
|
1963
|
+
paneHeightRatio: 0.16,
|
|
1964
|
+
defaultInputs: { length: 14, color: "#7e57c2", width: 2, showValueLine: false },
|
|
1965
|
+
draw: (ctx, renderContext, inputs) => {
|
|
1966
|
+
const length = clampIndicatorLength(inputs.length, 14);
|
|
1967
|
+
const values = withCachedSeries(
|
|
1968
|
+
`williamsr|${length}`,
|
|
1969
|
+
renderContext.data,
|
|
1970
|
+
() => computeWilliamsRSeries(renderContext.data, length)
|
|
1971
|
+
);
|
|
1972
|
+
return drawSeparateMultiSeries(
|
|
1973
|
+
ctx,
|
|
1974
|
+
renderContext,
|
|
1975
|
+
[{ values, color: inputs.color ?? "#7e57c2", width: Number(inputs.width) || 2 }],
|
|
1976
|
+
{
|
|
1977
|
+
title: `%R ${length}`,
|
|
1978
|
+
minOverride: -100,
|
|
1979
|
+
maxOverride: 0,
|
|
1980
|
+
guideLines: [-80, -20],
|
|
1981
|
+
axisTicks: [-100, -80, -50, -20, 0],
|
|
1982
|
+
decimals: 2,
|
|
1983
|
+
valueLines: inputs.showValueLine === true
|
|
1141
1984
|
}
|
|
1142
1985
|
);
|
|
1143
1986
|
}
|
|
1144
1987
|
};
|
|
1988
|
+
var BUILTIN_ROC_INDICATOR = {
|
|
1989
|
+
id: "roc",
|
|
1990
|
+
name: "ROC",
|
|
1991
|
+
pane: "separate",
|
|
1992
|
+
paneHeightRatio: 0.16,
|
|
1993
|
+
defaultInputs: { length: 9, color: "#2962ff", width: 2, showValueLine: false },
|
|
1994
|
+
draw: (ctx, renderContext, inputs) => {
|
|
1995
|
+
const length = clampIndicatorLength(inputs.length, 9);
|
|
1996
|
+
const values = withCachedSeries(
|
|
1997
|
+
`roc|${length}`,
|
|
1998
|
+
renderContext.data,
|
|
1999
|
+
() => computeRocSeries(renderContext.data, length)
|
|
2000
|
+
);
|
|
2001
|
+
return drawSeparateMultiSeries(
|
|
2002
|
+
ctx,
|
|
2003
|
+
renderContext,
|
|
2004
|
+
[{ values, color: inputs.color ?? "#2962ff", width: Number(inputs.width) || 2 }],
|
|
2005
|
+
{ title: `ROC ${length}`, includeZero: true, guideLines: [0], decimals: 2, valueLines: inputs.showValueLine === true }
|
|
2006
|
+
);
|
|
2007
|
+
}
|
|
2008
|
+
};
|
|
2009
|
+
var BUILTIN_MOMENTUM_INDICATOR = {
|
|
2010
|
+
id: "momentum",
|
|
2011
|
+
name: "Momentum",
|
|
2012
|
+
pane: "separate",
|
|
2013
|
+
paneHeightRatio: 0.16,
|
|
2014
|
+
defaultInputs: { length: 10, color: "#2962ff", width: 2, showValueLine: false },
|
|
2015
|
+
draw: (ctx, renderContext, inputs) => {
|
|
2016
|
+
const length = clampIndicatorLength(inputs.length, 10);
|
|
2017
|
+
const values = withCachedSeries(
|
|
2018
|
+
`momentum|${length}`,
|
|
2019
|
+
renderContext.data,
|
|
2020
|
+
() => computeMomentumSeries(renderContext.data, length)
|
|
2021
|
+
);
|
|
2022
|
+
return drawSeparateMultiSeries(
|
|
2023
|
+
ctx,
|
|
2024
|
+
renderContext,
|
|
2025
|
+
[{ values, color: inputs.color ?? "#2962ff", width: Number(inputs.width) || 2 }],
|
|
2026
|
+
{ title: `Mom ${length}`, includeZero: true, guideLines: [0], decimals: 2, valueLines: inputs.showValueLine === true }
|
|
2027
|
+
);
|
|
2028
|
+
}
|
|
2029
|
+
};
|
|
2030
|
+
var BUILTIN_PSAR_INDICATOR = {
|
|
2031
|
+
id: "psar",
|
|
2032
|
+
name: "PSAR",
|
|
2033
|
+
pane: "overlay",
|
|
2034
|
+
defaultInputs: { start: 0.02, increment: 0.02, maximum: 0.2, color: "#2962ff" },
|
|
2035
|
+
draw: (ctx, renderContext, inputs) => {
|
|
2036
|
+
if (!renderContext.yFromPrice) return;
|
|
2037
|
+
const yFromPrice = renderContext.yFromPrice;
|
|
2038
|
+
const start = Math.max(1e-3, Number(inputs.start) || 0.02);
|
|
2039
|
+
const increment = Math.max(1e-3, Number(inputs.increment) || 0.02);
|
|
2040
|
+
const maximum = Math.max(increment, Number(inputs.maximum) || 0.2);
|
|
2041
|
+
const values = withCachedSeries(
|
|
2042
|
+
`psar|${start}|${increment}|${maximum}`,
|
|
2043
|
+
renderContext.data,
|
|
2044
|
+
() => computePsarSeries(renderContext.data, start, increment, maximum)
|
|
2045
|
+
);
|
|
2046
|
+
const radius = Math.max(1, Math.min(2.5, renderContext.candleSpacing * 0.15));
|
|
2047
|
+
const dots = new Path2D();
|
|
2048
|
+
for (let index = renderContext.startIndex; index <= renderContext.endIndex; index += 1) {
|
|
2049
|
+
const value = values[index];
|
|
2050
|
+
if (!Number.isFinite(value ?? Number.NaN)) continue;
|
|
2051
|
+
const x = renderContext.xFromIndex(index);
|
|
2052
|
+
const y = yFromPrice(value);
|
|
2053
|
+
dots.moveTo(x + radius, y);
|
|
2054
|
+
dots.arc(x, y, radius, 0, Math.PI * 2);
|
|
2055
|
+
}
|
|
2056
|
+
ctx.save();
|
|
2057
|
+
ctx.fillStyle = inputs.color ?? "#2962ff";
|
|
2058
|
+
ctx.fill(dots);
|
|
2059
|
+
ctx.restore();
|
|
2060
|
+
},
|
|
2061
|
+
getAutoscaleRange: (data, startIndex, endIndex, inputs, skipIndex) => {
|
|
2062
|
+
const start = Math.max(1e-3, Number(inputs.start) || 0.02);
|
|
2063
|
+
const increment = Math.max(1e-3, Number(inputs.increment) || 0.02);
|
|
2064
|
+
const maximum = Math.max(increment, Number(inputs.maximum) || 0.2);
|
|
2065
|
+
const values = withCachedSeries(
|
|
2066
|
+
`psar|${start}|${increment}|${maximum}`,
|
|
2067
|
+
data,
|
|
2068
|
+
() => computePsarSeries(data, start, increment, maximum)
|
|
2069
|
+
);
|
|
2070
|
+
return rangeOfSeries([values], startIndex, endIndex, skipIndex);
|
|
2071
|
+
}
|
|
2072
|
+
};
|
|
2073
|
+
var BUILTIN_SUPERTREND_INDICATOR = {
|
|
2074
|
+
id: "supertrend",
|
|
2075
|
+
name: "SuperTrend",
|
|
2076
|
+
pane: "overlay",
|
|
2077
|
+
defaultInputs: { atrLength: 10, multiplier: 3, upColor: "#26a69a", downColor: "#ef5350", width: 2 },
|
|
2078
|
+
draw: (ctx, renderContext, inputs) => {
|
|
2079
|
+
const atrLength = clampIndicatorLength(inputs.atrLength, 10);
|
|
2080
|
+
const multiplier = Math.max(0.1, Number(inputs.multiplier) || 3);
|
|
2081
|
+
const { up, down } = withCachedComputation(
|
|
2082
|
+
`supertrend|${atrLength}|${multiplier}`,
|
|
2083
|
+
renderContext.data,
|
|
2084
|
+
() => computeSuperTrend(renderContext.data, atrLength, multiplier)
|
|
2085
|
+
);
|
|
2086
|
+
const width = Number(inputs.width) || 2;
|
|
2087
|
+
drawOverlaySeries(ctx, renderContext, up, inputs.upColor ?? "#26a69a", width);
|
|
2088
|
+
drawOverlaySeries(ctx, renderContext, down, inputs.downColor ?? "#ef5350", width);
|
|
2089
|
+
},
|
|
2090
|
+
getAutoscaleRange: (data, startIndex, endIndex, inputs, skipIndex) => {
|
|
2091
|
+
const atrLength = clampIndicatorLength(inputs.atrLength, 10);
|
|
2092
|
+
const multiplier = Math.max(0.1, Number(inputs.multiplier) || 3);
|
|
2093
|
+
const { up, down } = withCachedComputation(
|
|
2094
|
+
`supertrend|${atrLength}|${multiplier}`,
|
|
2095
|
+
data,
|
|
2096
|
+
() => computeSuperTrend(data, atrLength, multiplier)
|
|
2097
|
+
);
|
|
2098
|
+
return rangeOfSeries([up, down], startIndex, endIndex, skipIndex);
|
|
2099
|
+
}
|
|
2100
|
+
};
|
|
2101
|
+
var BUILTIN_ICHIMOKU_INDICATOR = {
|
|
2102
|
+
id: "ichimoku",
|
|
2103
|
+
name: "Ichimoku",
|
|
2104
|
+
pane: "overlay",
|
|
2105
|
+
defaultInputs: {
|
|
2106
|
+
conversionLength: 9,
|
|
2107
|
+
baseLength: 26,
|
|
2108
|
+
spanBLength: 52,
|
|
2109
|
+
displacement: 26,
|
|
2110
|
+
tenkanColor: "#2962ff",
|
|
2111
|
+
kijunColor: "#b71c1c",
|
|
2112
|
+
spanAColor: "#26a69a",
|
|
2113
|
+
spanBColor: "#ef5350",
|
|
2114
|
+
chikouColor: "#43a047",
|
|
2115
|
+
cloudOpacity: 0.08,
|
|
2116
|
+
showChikou: true
|
|
2117
|
+
},
|
|
2118
|
+
draw: (ctx, renderContext, inputs) => {
|
|
2119
|
+
const conversionLength = clampIndicatorLength(inputs.conversionLength, 9);
|
|
2120
|
+
const baseLength = clampIndicatorLength(inputs.baseLength, 26);
|
|
2121
|
+
const spanBLength = clampIndicatorLength(inputs.spanBLength, 52);
|
|
2122
|
+
const displacement = clampIndicatorLength(inputs.displacement, 26);
|
|
2123
|
+
const { tenkan, kijun, spanA, spanB, chikou } = withCachedComputation(
|
|
2124
|
+
`ichimoku|${conversionLength}|${baseLength}|${spanBLength}|${displacement}`,
|
|
2125
|
+
renderContext.data,
|
|
2126
|
+
() => computeIchimoku(renderContext.data, conversionLength, baseLength, spanBLength, displacement)
|
|
2127
|
+
);
|
|
2128
|
+
const cloudOpacity = Math.min(0.5, Math.max(0, Number(inputs.cloudOpacity) || 0.08));
|
|
2129
|
+
const bullishA = spanA.map((value, idx) => {
|
|
2130
|
+
const other = spanB[idx];
|
|
2131
|
+
return value != null && other != null && value >= other ? value : null;
|
|
2132
|
+
});
|
|
2133
|
+
const bullishB = spanB.map((value, idx) => {
|
|
2134
|
+
const other = spanA[idx];
|
|
2135
|
+
return value != null && other != null && other >= value ? value : null;
|
|
2136
|
+
});
|
|
2137
|
+
const bearishA = spanA.map((value, idx) => {
|
|
2138
|
+
const other = spanB[idx];
|
|
2139
|
+
return value != null && other != null && value < other ? value : null;
|
|
2140
|
+
});
|
|
2141
|
+
const bearishB = spanB.map((value, idx) => {
|
|
2142
|
+
const other = spanA[idx];
|
|
2143
|
+
return value != null && other != null && other < value ? value : null;
|
|
2144
|
+
});
|
|
2145
|
+
fillBetweenSeries(ctx, renderContext, bullishA, bullishB, inputs.spanAColor ?? "#26a69a", cloudOpacity);
|
|
2146
|
+
fillBetweenSeries(ctx, renderContext, bearishB, bearishA, inputs.spanBColor ?? "#ef5350", cloudOpacity);
|
|
2147
|
+
drawOverlaySeries(ctx, renderContext, spanA, inputs.spanAColor ?? "#26a69a", 1);
|
|
2148
|
+
drawOverlaySeries(ctx, renderContext, spanB, inputs.spanBColor ?? "#ef5350", 1);
|
|
2149
|
+
drawOverlaySeries(ctx, renderContext, tenkan, inputs.tenkanColor ?? "#2962ff", 1.5);
|
|
2150
|
+
drawOverlaySeries(ctx, renderContext, kijun, inputs.kijunColor ?? "#b71c1c", 1.5);
|
|
2151
|
+
if (inputs.showChikou !== false) {
|
|
2152
|
+
drawOverlaySeries(ctx, renderContext, chikou, inputs.chikouColor ?? "#43a047", 1);
|
|
2153
|
+
}
|
|
2154
|
+
},
|
|
2155
|
+
getAutoscaleRange: (data, startIndex, endIndex, inputs, skipIndex) => {
|
|
2156
|
+
const conversionLength = clampIndicatorLength(inputs.conversionLength, 9);
|
|
2157
|
+
const baseLength = clampIndicatorLength(inputs.baseLength, 26);
|
|
2158
|
+
const spanBLength = clampIndicatorLength(inputs.spanBLength, 52);
|
|
2159
|
+
const displacement = clampIndicatorLength(inputs.displacement, 26);
|
|
2160
|
+
const { tenkan, kijun, spanA, spanB } = withCachedComputation(
|
|
2161
|
+
`ichimoku|${conversionLength}|${baseLength}|${spanBLength}|${displacement}`,
|
|
2162
|
+
data,
|
|
2163
|
+
() => computeIchimoku(data, conversionLength, baseLength, spanBLength, displacement)
|
|
2164
|
+
);
|
|
2165
|
+
return rangeOfSeries([tenkan, kijun, spanA, spanB], startIndex, endIndex, skipIndex);
|
|
2166
|
+
}
|
|
2167
|
+
};
|
|
2168
|
+
var BUILTIN_KELTNER_INDICATOR = {
|
|
2169
|
+
id: "keltner",
|
|
2170
|
+
name: "KC",
|
|
2171
|
+
pane: "overlay",
|
|
2172
|
+
defaultInputs: {
|
|
2173
|
+
emaLength: 20,
|
|
2174
|
+
atrLength: 10,
|
|
2175
|
+
multiplier: 2,
|
|
2176
|
+
basisColor: "#2962ff",
|
|
2177
|
+
bandColor: "#2962ff",
|
|
2178
|
+
width: 1.5,
|
|
2179
|
+
fillOpacity: 0.04
|
|
2180
|
+
},
|
|
2181
|
+
draw: (ctx, renderContext, inputs) => {
|
|
2182
|
+
const emaLength = clampIndicatorLength(inputs.emaLength, 20);
|
|
2183
|
+
const atrLength = clampIndicatorLength(inputs.atrLength, 10);
|
|
2184
|
+
const multiplier = Math.max(0.1, Number(inputs.multiplier) || 2);
|
|
2185
|
+
const { basis, upper, lower } = withCachedComputation(
|
|
2186
|
+
`keltner|${emaLength}|${atrLength}|${multiplier}`,
|
|
2187
|
+
renderContext.data,
|
|
2188
|
+
() => computeKeltner(renderContext.data, emaLength, atrLength, multiplier)
|
|
2189
|
+
);
|
|
2190
|
+
const bandColor = inputs.bandColor ?? "#2962ff";
|
|
2191
|
+
const width = Number(inputs.width) || 1.5;
|
|
2192
|
+
fillBetweenSeries(ctx, renderContext, upper, lower, bandColor, Number(inputs.fillOpacity) || 0.04);
|
|
2193
|
+
drawOverlaySeries(ctx, renderContext, upper, bandColor, width);
|
|
2194
|
+
drawOverlaySeries(ctx, renderContext, lower, bandColor, width);
|
|
2195
|
+
drawOverlaySeries(ctx, renderContext, basis, inputs.basisColor ?? "#2962ff", width);
|
|
2196
|
+
},
|
|
2197
|
+
getAutoscaleRange: (data, startIndex, endIndex, inputs, skipIndex) => {
|
|
2198
|
+
const emaLength = clampIndicatorLength(inputs.emaLength, 20);
|
|
2199
|
+
const atrLength = clampIndicatorLength(inputs.atrLength, 10);
|
|
2200
|
+
const multiplier = Math.max(0.1, Number(inputs.multiplier) || 2);
|
|
2201
|
+
const { upper, lower } = withCachedComputation(
|
|
2202
|
+
`keltner|${emaLength}|${atrLength}|${multiplier}`,
|
|
2203
|
+
data,
|
|
2204
|
+
() => computeKeltner(data, emaLength, atrLength, multiplier)
|
|
2205
|
+
);
|
|
2206
|
+
return rangeOfSeries([upper, lower], startIndex, endIndex, skipIndex);
|
|
2207
|
+
}
|
|
2208
|
+
};
|
|
2209
|
+
var BUILTIN_DONCHIAN_INDICATOR = {
|
|
2210
|
+
id: "donchian",
|
|
2211
|
+
name: "DC",
|
|
2212
|
+
pane: "overlay",
|
|
2213
|
+
defaultInputs: { length: 20, bandColor: "#2962ff", basisColor: "#ff6d00", width: 1.5, fillOpacity: 0.04 },
|
|
2214
|
+
draw: (ctx, renderContext, inputs) => {
|
|
2215
|
+
const length = clampIndicatorLength(inputs.length, 20);
|
|
2216
|
+
const { upper, lower, basis } = withCachedComputation(
|
|
2217
|
+
`donchian|${length}`,
|
|
2218
|
+
renderContext.data,
|
|
2219
|
+
() => computeDonchian(renderContext.data, length)
|
|
2220
|
+
);
|
|
2221
|
+
const bandColor = inputs.bandColor ?? "#2962ff";
|
|
2222
|
+
const width = Number(inputs.width) || 1.5;
|
|
2223
|
+
fillBetweenSeries(ctx, renderContext, upper, lower, bandColor, Number(inputs.fillOpacity) || 0.04);
|
|
2224
|
+
drawOverlaySeries(ctx, renderContext, upper, bandColor, width);
|
|
2225
|
+
drawOverlaySeries(ctx, renderContext, lower, bandColor, width);
|
|
2226
|
+
drawOverlaySeries(ctx, renderContext, basis, inputs.basisColor ?? "#ff6d00", width);
|
|
2227
|
+
},
|
|
2228
|
+
getAutoscaleRange: (data, startIndex, endIndex, inputs, skipIndex) => {
|
|
2229
|
+
const length = clampIndicatorLength(inputs.length, 20);
|
|
2230
|
+
const { upper, lower } = withCachedComputation(`donchian|${length}`, data, () => computeDonchian(data, length));
|
|
2231
|
+
return rangeOfSeries([upper, lower], startIndex, endIndex, skipIndex);
|
|
2232
|
+
}
|
|
2233
|
+
};
|
|
1145
2234
|
var BUILTIN_INDICATORS = [
|
|
1146
2235
|
BUILTIN_VOLUME_INDICATOR,
|
|
1147
2236
|
BUILTIN_SMA_INDICATOR,
|
|
@@ -1154,7 +2243,22 @@ var BUILTIN_INDICATORS = [
|
|
|
1154
2243
|
BUILTIN_VWAP_INDICATOR,
|
|
1155
2244
|
BUILTIN_BOLLINGER_INDICATOR,
|
|
1156
2245
|
BUILTIN_STDDEV_INDICATOR,
|
|
1157
|
-
BUILTIN_ATR_INDICATOR
|
|
2246
|
+
BUILTIN_ATR_INDICATOR,
|
|
2247
|
+
BUILTIN_MACD_INDICATOR,
|
|
2248
|
+
BUILTIN_STOCHASTIC_INDICATOR,
|
|
2249
|
+
BUILTIN_STOCHRSI_INDICATOR,
|
|
2250
|
+
BUILTIN_ADX_INDICATOR,
|
|
2251
|
+
BUILTIN_OBV_INDICATOR,
|
|
2252
|
+
BUILTIN_MFI_INDICATOR,
|
|
2253
|
+
BUILTIN_CCI_INDICATOR,
|
|
2254
|
+
BUILTIN_WILLIAMSR_INDICATOR,
|
|
2255
|
+
BUILTIN_ROC_INDICATOR,
|
|
2256
|
+
BUILTIN_MOMENTUM_INDICATOR,
|
|
2257
|
+
BUILTIN_PSAR_INDICATOR,
|
|
2258
|
+
BUILTIN_SUPERTREND_INDICATOR,
|
|
2259
|
+
BUILTIN_ICHIMOKU_INDICATOR,
|
|
2260
|
+
BUILTIN_KELTNER_INDICATOR,
|
|
2261
|
+
BUILTIN_DONCHIAN_INDICATOR
|
|
1158
2262
|
];
|
|
1159
2263
|
function createChart(element, options = {}) {
|
|
1160
2264
|
let mergedOptions = mergeChartOptions(DEFAULT_OPTIONS, options);
|
|
@@ -1878,6 +2982,36 @@ function createChart(element, options = {}) {
|
|
|
1878
2982
|
}
|
|
1879
2983
|
return direction > 0 ? "up" : "down";
|
|
1880
2984
|
};
|
|
2985
|
+
let heikinAshiCache = null;
|
|
2986
|
+
let heikinAshiFingerprint = "";
|
|
2987
|
+
const getHeikinAshiData = () => {
|
|
2988
|
+
const last = data[data.length - 1];
|
|
2989
|
+
const fingerprint = !last ? "empty" : `${data.length}|${last.time.getTime()}|${last.o}|${last.h}|${last.l}|${last.c}`;
|
|
2990
|
+
if (heikinAshiCache && heikinAshiFingerprint === fingerprint) {
|
|
2991
|
+
return heikinAshiCache;
|
|
2992
|
+
}
|
|
2993
|
+
const result = new Array(data.length);
|
|
2994
|
+
let prevOpen = 0;
|
|
2995
|
+
let prevClose = 0;
|
|
2996
|
+
for (let i = 0; i < data.length; i += 1) {
|
|
2997
|
+
const point = data[i];
|
|
2998
|
+
const haClose = (point.o + point.h + point.l + point.c) / 4;
|
|
2999
|
+
const haOpen = i === 0 ? (point.o + point.c) / 2 : (prevOpen + prevClose) / 2;
|
|
3000
|
+
result[i] = {
|
|
3001
|
+
time: point.time,
|
|
3002
|
+
o: haOpen,
|
|
3003
|
+
h: Math.max(point.h, haOpen, haClose),
|
|
3004
|
+
l: Math.min(point.l, haOpen, haClose),
|
|
3005
|
+
c: haClose,
|
|
3006
|
+
...point.v === void 0 ? {} : { v: point.v }
|
|
3007
|
+
};
|
|
3008
|
+
prevOpen = haOpen;
|
|
3009
|
+
prevClose = haClose;
|
|
3010
|
+
}
|
|
3011
|
+
heikinAshiCache = result;
|
|
3012
|
+
heikinAshiFingerprint = fingerprint;
|
|
3013
|
+
return result;
|
|
3014
|
+
};
|
|
1881
3015
|
const formatHoverTimeLabel = (time, mode) => {
|
|
1882
3016
|
if (mode === "time") {
|
|
1883
3017
|
return time.toLocaleTimeString(void 0, {
|
|
@@ -2455,23 +3589,34 @@ function createChart(element, options = {}) {
|
|
|
2455
3589
|
const xEnd = xStart + xSpan;
|
|
2456
3590
|
const startIndex = Math.max(0, Math.floor(xStart));
|
|
2457
3591
|
const endIndex = Math.min(data.length - 1, Math.ceil(xEnd) - 1);
|
|
3592
|
+
const chartType = mergedOptions.chartType;
|
|
3593
|
+
const seriesData = chartType === "heikin-ashi" ? getHeikinAshiData() : data;
|
|
3594
|
+
const isLineStyleChart = chartType === "line" || chartType === "area" || chartType === "baseline";
|
|
2458
3595
|
const skipLatestIndex = mergedOptions.autoScaleIgnoreLatestCandle && data.length > 1 ? data.length - 1 : -1;
|
|
2459
3596
|
const scanCandleRange = (from, to) => {
|
|
2460
3597
|
let min = Number.POSITIVE_INFINITY;
|
|
2461
3598
|
let max = Number.NEGATIVE_INFINITY;
|
|
2462
3599
|
for (let idx = from; idx <= to; idx += 1) {
|
|
2463
3600
|
if (idx === skipLatestIndex) continue;
|
|
2464
|
-
const point =
|
|
3601
|
+
const point = seriesData[idx];
|
|
2465
3602
|
if (!point) continue;
|
|
2466
|
-
if (
|
|
2467
|
-
|
|
3603
|
+
if (isLineStyleChart) {
|
|
3604
|
+
if (point.c < min) min = point.c;
|
|
3605
|
+
if (point.c > max) max = point.c;
|
|
3606
|
+
} else {
|
|
3607
|
+
if (point.l < min) min = point.l;
|
|
3608
|
+
if (point.h > max) max = point.h;
|
|
3609
|
+
}
|
|
2468
3610
|
}
|
|
2469
3611
|
return min <= max ? { min, max } : null;
|
|
2470
3612
|
};
|
|
2471
3613
|
let candleRange = scanCandleRange(startIndex, endIndex);
|
|
2472
3614
|
if (!candleRange) {
|
|
2473
3615
|
const latestIndex = data.length - 1;
|
|
2474
|
-
candleRange = scanCandleRange(Math.max(0, latestIndex - 120), latestIndex - 1) ?? scanCandleRange(0, latestIndex) ?? {
|
|
3616
|
+
candleRange = scanCandleRange(Math.max(0, latestIndex - 120), latestIndex - 1) ?? scanCandleRange(0, latestIndex) ?? {
|
|
3617
|
+
min: isLineStyleChart ? seriesData[latestIndex].c : seriesData[latestIndex].l,
|
|
3618
|
+
max: isLineStyleChart ? seriesData[latestIndex].c : seriesData[latestIndex].h
|
|
3619
|
+
};
|
|
2475
3620
|
}
|
|
2476
3621
|
let minPrice = candleRange.min;
|
|
2477
3622
|
let maxPrice = candleRange.max;
|
|
@@ -3367,23 +4512,137 @@ function createChart(element, options = {}) {
|
|
|
3367
4512
|
}
|
|
3368
4513
|
return data[index]?.v;
|
|
3369
4514
|
};
|
|
3370
|
-
{
|
|
4515
|
+
if (isLineStyleChart) {
|
|
4516
|
+
const seriesLineWidth = Math.max(1, mergedOptions.lineWidth);
|
|
4517
|
+
const linePath = new Path2D();
|
|
4518
|
+
let started = false;
|
|
4519
|
+
let firstX = 0;
|
|
4520
|
+
let lastX = 0;
|
|
4521
|
+
for (let index = startIndex; index <= endIndex; index += 1) {
|
|
4522
|
+
const point = seriesData[index];
|
|
4523
|
+
if (!point) continue;
|
|
4524
|
+
const close = useSmoothedCandle && index === lastDataIndex ? smoothedTickerPrice : point.c;
|
|
4525
|
+
const x = chartLeft + (index + 0.5 - xStart) / xSpan * chartWidth;
|
|
4526
|
+
const y = yFromPrice(close);
|
|
4527
|
+
if (!started) {
|
|
4528
|
+
linePath.moveTo(x, y);
|
|
4529
|
+
firstX = x;
|
|
4530
|
+
started = true;
|
|
4531
|
+
} else {
|
|
4532
|
+
linePath.lineTo(x, y);
|
|
4533
|
+
}
|
|
4534
|
+
lastX = x;
|
|
4535
|
+
}
|
|
4536
|
+
if (started) {
|
|
4537
|
+
if (chartType === "baseline") {
|
|
4538
|
+
const baselinePrice = mergedOptions.baselinePrice ?? (yMin + yMax) / 2;
|
|
4539
|
+
const baselineY = yFromPrice(baselinePrice);
|
|
4540
|
+
const fillPath = new Path2D(linePath);
|
|
4541
|
+
fillPath.lineTo(lastX, baselineY);
|
|
4542
|
+
fillPath.lineTo(firstX, baselineY);
|
|
4543
|
+
fillPath.closePath();
|
|
4544
|
+
const halves = [
|
|
4545
|
+
{ clipTop: chartTop, clipBottom: baselineY, color: mergedOptions.upColor },
|
|
4546
|
+
{ clipTop: baselineY, clipBottom: chartBottom, color: mergedOptions.downColor }
|
|
4547
|
+
];
|
|
4548
|
+
for (const half of halves) {
|
|
4549
|
+
const clipHeight = half.clipBottom - half.clipTop;
|
|
4550
|
+
if (clipHeight <= 0) continue;
|
|
4551
|
+
ctx.save();
|
|
4552
|
+
ctx.beginPath();
|
|
4553
|
+
ctx.rect(chartLeft, half.clipTop, chartWidth, clipHeight);
|
|
4554
|
+
ctx.clip();
|
|
4555
|
+
ctx.globalAlpha = Math.max(0, Math.min(1, mergedOptions.areaFillOpacity));
|
|
4556
|
+
ctx.fillStyle = half.color;
|
|
4557
|
+
ctx.fill(fillPath);
|
|
4558
|
+
ctx.globalAlpha = 1;
|
|
4559
|
+
ctx.strokeStyle = half.color;
|
|
4560
|
+
ctx.lineWidth = seriesLineWidth;
|
|
4561
|
+
ctx.lineJoin = "round";
|
|
4562
|
+
ctx.stroke(linePath);
|
|
4563
|
+
ctx.restore();
|
|
4564
|
+
}
|
|
4565
|
+
ctx.save();
|
|
4566
|
+
ctx.strokeStyle = "rgba(148,163,184,0.5)";
|
|
4567
|
+
ctx.lineWidth = 1;
|
|
4568
|
+
ctx.setLineDash([4, 4]);
|
|
4569
|
+
ctx.beginPath();
|
|
4570
|
+
ctx.moveTo(chartLeft, crisp(baselineY));
|
|
4571
|
+
ctx.lineTo(chartRight, crisp(baselineY));
|
|
4572
|
+
ctx.stroke();
|
|
4573
|
+
ctx.restore();
|
|
4574
|
+
} else {
|
|
4575
|
+
if (chartType === "area") {
|
|
4576
|
+
const fillPath = new Path2D(linePath);
|
|
4577
|
+
fillPath.lineTo(lastX, chartBottom);
|
|
4578
|
+
fillPath.lineTo(firstX, chartBottom);
|
|
4579
|
+
fillPath.closePath();
|
|
4580
|
+
ctx.save();
|
|
4581
|
+
ctx.globalAlpha = Math.max(0, Math.min(1, mergedOptions.areaFillOpacity));
|
|
4582
|
+
ctx.fillStyle = mergedOptions.lineColor;
|
|
4583
|
+
ctx.fill(fillPath);
|
|
4584
|
+
ctx.restore();
|
|
4585
|
+
}
|
|
4586
|
+
ctx.save();
|
|
4587
|
+
ctx.strokeStyle = mergedOptions.lineColor;
|
|
4588
|
+
ctx.lineWidth = seriesLineWidth;
|
|
4589
|
+
ctx.lineJoin = "round";
|
|
4590
|
+
ctx.stroke(linePath);
|
|
4591
|
+
ctx.restore();
|
|
4592
|
+
}
|
|
4593
|
+
}
|
|
4594
|
+
} else if (chartType === "bars") {
|
|
4595
|
+
const upBars = new Path2D();
|
|
4596
|
+
const downBars = new Path2D();
|
|
4597
|
+
const tickLength = Math.max(2, Math.floor(bodyWidth / 2));
|
|
4598
|
+
for (let index = startIndex; index <= endIndex; index += 1) {
|
|
4599
|
+
const point = seriesData[index];
|
|
4600
|
+
if (!point) continue;
|
|
4601
|
+
const isLastCandle = useSmoothedCandle && index === lastDataIndex;
|
|
4602
|
+
const actualDirection = point.c >= point.o ? "up" : "down";
|
|
4603
|
+
const displayClose = isLastCandle ? actualDirection === "up" ? Math.max(point.o, smoothedTickerPrice) : Math.min(point.o, smoothedTickerPrice) : point.c;
|
|
4604
|
+
const displayHigh = isLastCandle && actualDirection === "up" ? Math.max(point.h, displayClose) : point.h;
|
|
4605
|
+
const displayLow = isLastCandle && actualDirection === "down" ? Math.min(point.l, displayClose) : point.l;
|
|
4606
|
+
const roundedCenterX = Math.round(chartLeft + (index + 0.5 - xStart) / xSpan * chartWidth);
|
|
4607
|
+
const direction = isLastCandle ? actualDirection : getCandleDirectionByIndex(index);
|
|
4608
|
+
const path = direction === "up" ? upBars : downBars;
|
|
4609
|
+
const centerX = roundedCenterX + 0.5;
|
|
4610
|
+
path.moveTo(centerX, crisp(yFromPrice(displayHigh)));
|
|
4611
|
+
path.lineTo(centerX, crisp(yFromPrice(displayLow)));
|
|
4612
|
+
const openY = crisp(yFromPrice(point.o));
|
|
4613
|
+
const closeY = crisp(yFromPrice(displayClose));
|
|
4614
|
+
path.moveTo(centerX - tickLength, openY);
|
|
4615
|
+
path.lineTo(centerX, openY);
|
|
4616
|
+
path.moveTo(centerX, closeY);
|
|
4617
|
+
path.lineTo(centerX + tickLength, closeY);
|
|
4618
|
+
}
|
|
4619
|
+
ctx.lineWidth = Math.max(1, candleWickWidth);
|
|
4620
|
+
ctx.strokeStyle = mergedOptions.upColor;
|
|
4621
|
+
ctx.stroke(upBars);
|
|
4622
|
+
ctx.strokeStyle = mergedOptions.downColor;
|
|
4623
|
+
ctx.stroke(downBars);
|
|
4624
|
+
} else {
|
|
4625
|
+
const isHeikinAshi = chartType === "heikin-ashi";
|
|
4626
|
+
const isHollow = chartType === "hollow-candles";
|
|
4627
|
+
const smoothLastCandle = useSmoothedCandle && !isHeikinAshi;
|
|
3371
4628
|
const upWicks = new Path2D();
|
|
3372
4629
|
const downWicks = new Path2D();
|
|
3373
4630
|
const upBodies = new Path2D();
|
|
3374
4631
|
const downBodies = new Path2D();
|
|
4632
|
+
const upHollowBodies = new Path2D();
|
|
4633
|
+
const downHollowBodies = new Path2D();
|
|
3375
4634
|
for (let index = startIndex; index <= endIndex; index += 1) {
|
|
3376
|
-
const point =
|
|
4635
|
+
const point = seriesData[index];
|
|
3377
4636
|
if (!point) {
|
|
3378
4637
|
continue;
|
|
3379
4638
|
}
|
|
3380
|
-
const isLastCandle =
|
|
4639
|
+
const isLastCandle = smoothLastCandle && index === lastDataIndex;
|
|
3381
4640
|
const actualDirection = point.c >= point.o ? "up" : "down";
|
|
3382
4641
|
const displayClose = isLastCandle ? actualDirection === "up" ? Math.max(point.o, smoothedTickerPrice) : Math.min(point.o, smoothedTickerPrice) : point.c;
|
|
3383
4642
|
const displayHigh = isLastCandle ? actualDirection === "up" ? Math.max(point.h, displayClose) : point.h : point.h;
|
|
3384
4643
|
const displayLow = isLastCandle ? actualDirection === "up" ? point.l : Math.min(point.l, displayClose) : point.l;
|
|
3385
4644
|
const centerX = chartLeft + (index + 0.5 - xStart) / xSpan * chartWidth;
|
|
3386
|
-
const direction = isLastCandle ? actualDirection : getCandleDirectionByIndex(index);
|
|
4645
|
+
const direction = isHeikinAshi || isLastCandle ? actualDirection : getCandleDirectionByIndex(index);
|
|
3387
4646
|
const isUp = direction === "up";
|
|
3388
4647
|
const roundedCenterX = Math.round(centerX);
|
|
3389
4648
|
const wicks = isUp ? upWicks : downWicks;
|
|
@@ -3395,7 +4654,9 @@ function createChart(element, options = {}) {
|
|
|
3395
4654
|
const bodyIsUp = displayClose >= point.o;
|
|
3396
4655
|
const bodyTop = bodyIsUp ? Math.min(closeYPx, openYPx - 1) : openYPx;
|
|
3397
4656
|
const bodyBottom = bodyIsUp ? openYPx : Math.max(closeYPx, openYPx + 1);
|
|
3398
|
-
|
|
4657
|
+
const useHollowBody = isHollow && bodyIsUp;
|
|
4658
|
+
const bodies = useHollowBody ? isUp ? upHollowBodies : downHollowBodies : isUp ? upBodies : downBodies;
|
|
4659
|
+
bodies.rect(bodyLeft, bodyTop, bodyWidth, bodyBottom - bodyTop);
|
|
3399
4660
|
}
|
|
3400
4661
|
ctx.lineWidth = candleWickWidth;
|
|
3401
4662
|
ctx.strokeStyle = mergedOptions.upColor;
|
|
@@ -3406,6 +4667,16 @@ function createChart(element, options = {}) {
|
|
|
3406
4667
|
ctx.stroke(downWicks);
|
|
3407
4668
|
ctx.fillStyle = mergedOptions.downColor;
|
|
3408
4669
|
ctx.fill(downBodies);
|
|
4670
|
+
if (isHollow) {
|
|
4671
|
+
ctx.fillStyle = mergedOptions.backgroundColor;
|
|
4672
|
+
ctx.fill(upHollowBodies);
|
|
4673
|
+
ctx.fill(downHollowBodies);
|
|
4674
|
+
ctx.lineWidth = 1;
|
|
4675
|
+
ctx.strokeStyle = mergedOptions.upColor;
|
|
4676
|
+
ctx.stroke(upHollowBodies);
|
|
4677
|
+
ctx.strokeStyle = mergedOptions.downColor;
|
|
4678
|
+
ctx.stroke(downHollowBodies);
|
|
4679
|
+
}
|
|
3409
4680
|
}
|
|
3410
4681
|
const activeOverlayIndicators = indicators.filter((indicator) => indicator.visible).map((indicator) => ({ indicator, plugin: indicatorRegistry.get(indicator.type) })).filter(
|
|
3411
4682
|
(value) => value.plugin !== void 0 && (value.indicator.pane ?? value.plugin.pane ?? "overlay") === "overlay"
|
|
@@ -5801,6 +7072,14 @@ function createChart(element, options = {}) {
|
|
|
5801
7072
|
}
|
|
5802
7073
|
scheduleDraw();
|
|
5803
7074
|
};
|
|
7075
|
+
const setChartType = (type) => {
|
|
7076
|
+
if (mergedOptions.chartType === type) {
|
|
7077
|
+
return;
|
|
7078
|
+
}
|
|
7079
|
+
mergedOptions = { ...mergedOptions, chartType: type };
|
|
7080
|
+
scheduleDraw({ updateAutoScale: true });
|
|
7081
|
+
};
|
|
7082
|
+
const getChartType = () => mergedOptions.chartType;
|
|
5804
7083
|
const resize = (nextWidth, nextHeight) => {
|
|
5805
7084
|
if (nextWidth && nextWidth > 0) {
|
|
5806
7085
|
width = nextWidth;
|
|
@@ -6158,6 +7437,8 @@ function createChart(element, options = {}) {
|
|
|
6158
7437
|
draw();
|
|
6159
7438
|
return {
|
|
6160
7439
|
updateOptions,
|
|
7440
|
+
setChartType,
|
|
7441
|
+
getChartType,
|
|
6161
7442
|
setData,
|
|
6162
7443
|
upsertBar,
|
|
6163
7444
|
setPriceLines,
|