@forgecharts/sdk 1.5.68 → 1.5.70
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/core/Chart.d.ts +2 -0
- package/dist/core/Chart.d.ts.map +1 -1
- package/dist/core/__tests__/cursorTime.test.d.ts +2 -0
- package/dist/core/__tests__/cursorTime.test.d.ts.map +1 -0
- package/dist/core/cursorTime.d.ts +27 -0
- package/dist/core/cursorTime.d.ts.map +1 -0
- package/dist/datafeed/DatafeedConnector.d.ts +5 -3
- package/dist/datafeed/DatafeedConnector.d.ts.map +1 -1
- package/dist/index.js +95 -65
- package/dist/index.js.map +1 -1
- package/dist/internal.js +95 -65
- package/dist/internal.js.map +1 -1
- package/dist/pixi/PixiChart.d.ts +2 -0
- package/dist/pixi/PixiChart.d.ts.map +1 -1
- package/dist/pixi/PixiCrosshairRenderer.d.ts +9 -1
- package/dist/pixi/PixiCrosshairRenderer.d.ts.map +1 -1
- package/dist/react/index.js +95 -65
- package/dist/react/index.js.map +1 -1
- package/dist/react/internal.js +95 -65
- package/dist/react/internal.js.map +1 -1
- package/package.json +1 -1
package/dist/react/internal.js
CHANGED
|
@@ -102,6 +102,36 @@ function isGLBXDailyBreak(nowMs = Date.now()) {
|
|
|
102
102
|
return weekday === "Mon" || weekday === "Tue" || weekday === "Wed" || weekday === "Thu";
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
+
// src/core/cursorTime.ts
|
|
106
|
+
function cursorBucketTime(bars, transform, interval, x) {
|
|
107
|
+
const raw = transform.xToTime(x);
|
|
108
|
+
const tfSec = parseTfSeconds(interval) ?? 0;
|
|
109
|
+
const floored = tfSec > 0 ? Math.floor(raw / tfSec) * tfSec : raw;
|
|
110
|
+
if (!bars || bars.length === 0) return floored;
|
|
111
|
+
let lo = 0, hi = bars.length - 1;
|
|
112
|
+
while (lo < hi) {
|
|
113
|
+
const mid = lo + hi >>> 1;
|
|
114
|
+
if (bars[mid].time < raw) lo = mid + 1;
|
|
115
|
+
else hi = mid;
|
|
116
|
+
}
|
|
117
|
+
let nearest = lo;
|
|
118
|
+
let best = Math.abs(transform.timeToX(bars[lo].time) - x);
|
|
119
|
+
for (const i of [lo - 1, lo + 1]) {
|
|
120
|
+
if (i < 0 || i >= bars.length) continue;
|
|
121
|
+
const d = Math.abs(transform.timeToX(bars[i].time) - x);
|
|
122
|
+
if (d < best) {
|
|
123
|
+
best = d;
|
|
124
|
+
nearest = i;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const first = bars[0].time;
|
|
128
|
+
const last = bars[bars.length - 1].time;
|
|
129
|
+
const halfCol = bars.length > 1 ? Math.abs(transform.timeToX(bars[1].time) - transform.timeToX(first)) / 2 : Infinity;
|
|
130
|
+
const edgeX = raw > last ? transform.timeToX(last) : raw < first ? transform.timeToX(first) : null;
|
|
131
|
+
if (edgeX !== null && Math.abs(x - edgeX) > halfCol) return floored;
|
|
132
|
+
return bars[nearest].time;
|
|
133
|
+
}
|
|
134
|
+
|
|
105
135
|
// src/core/CanvasLayer.ts
|
|
106
136
|
var CanvasLayer = class {
|
|
107
137
|
canvas;
|
|
@@ -4740,10 +4770,7 @@ var Chart = class {
|
|
|
4740
4770
|
onCrosshairMove: (x, y) => {
|
|
4741
4771
|
if (!this._crosshairEnabled) return;
|
|
4742
4772
|
if (this._magnetMode === "off") {
|
|
4743
|
-
|
|
4744
|
-
const rawTime = this._transform.xToTime(x);
|
|
4745
|
-
const flooredTime = Math.floor(rawTime / tfSec) * tfSec;
|
|
4746
|
-
this._crosshair.update(x, y, flooredTime);
|
|
4773
|
+
this._crosshair.update(x, y, this._cursorBucketTime(x));
|
|
4747
4774
|
} else {
|
|
4748
4775
|
const snap = this.snapXToBar(x);
|
|
4749
4776
|
const sy = this._magnetMode === "snap-xy" ? snap.closeY ?? y : y;
|
|
@@ -4784,7 +4811,8 @@ var Chart = class {
|
|
|
4784
4811
|
},
|
|
4785
4812
|
onDrawPointerMove: (x, y) => {
|
|
4786
4813
|
this._drawingCursorPt = {
|
|
4787
|
-
time:
|
|
4814
|
+
// Bucket time, not raw: the label and the stored anchor agree.
|
|
4815
|
+
time: this._cursorBucketTime(x),
|
|
4788
4816
|
price: this._transform.yToPrice(y)
|
|
4789
4817
|
};
|
|
4790
4818
|
this._dirty = true;
|
|
@@ -5057,9 +5085,19 @@ var Chart = class {
|
|
|
5057
5085
|
closeY: this._transform.priceToY(bar.close)
|
|
5058
5086
|
};
|
|
5059
5087
|
}
|
|
5060
|
-
|
|
5061
|
-
|
|
5062
|
-
|
|
5088
|
+
return { x, time: this._cursorBucketTime(x) };
|
|
5089
|
+
}
|
|
5090
|
+
/** Bucket time for a cursor x — see core/cursorTime.ts for the rule. */
|
|
5091
|
+
_cursorBucketTime(x) {
|
|
5092
|
+
let bars = null;
|
|
5093
|
+
for (const s of this._series) {
|
|
5094
|
+
const data = s.data();
|
|
5095
|
+
if (data.length > 0) {
|
|
5096
|
+
bars = data;
|
|
5097
|
+
break;
|
|
5098
|
+
}
|
|
5099
|
+
}
|
|
5100
|
+
return cursorBucketTime(bars, this._transform, this._interval, x);
|
|
5063
5101
|
}
|
|
5064
5102
|
_computeViewport() {
|
|
5065
5103
|
return {
|
|
@@ -5536,7 +5574,7 @@ var Chart = class {
|
|
|
5536
5574
|
const MAGNET_PX = 15;
|
|
5537
5575
|
const t = this._transform;
|
|
5538
5576
|
const price = t.yToPrice(y);
|
|
5539
|
-
const time =
|
|
5577
|
+
const time = this._cursorBucketTime(x);
|
|
5540
5578
|
if (this._magnetMode === "off") return { time, price };
|
|
5541
5579
|
let bestBar = null;
|
|
5542
5580
|
let bestDist = SNAP_PX;
|
|
@@ -6235,7 +6273,16 @@ var PixiCrosshairRenderer = class {
|
|
|
6235
6273
|
interval = "1h";
|
|
6236
6274
|
/** IANA timezone for the time label. Falls back to UTC. */
|
|
6237
6275
|
timezone = "UTC";
|
|
6238
|
-
|
|
6276
|
+
/**
|
|
6277
|
+
* The time the chart computed for this cursor position — nearest bar, else
|
|
6278
|
+
* floored to the timeframe grid. This renderer must never derive its own:
|
|
6279
|
+
* it once did (raw xToTime), and every pointer style's time label showed
|
|
6280
|
+
* interpolated minute noise (10:02 on an hourly chart) on any timeframe
|
|
6281
|
+
* wider than the interpolation error.
|
|
6282
|
+
*/
|
|
6283
|
+
_snapTime = null;
|
|
6284
|
+
update(x, y, snapTime) {
|
|
6285
|
+
this._snapTime = snapTime ?? null;
|
|
6239
6286
|
this._x = x;
|
|
6240
6287
|
this._y = y;
|
|
6241
6288
|
}
|
|
@@ -6277,7 +6324,7 @@ var PixiCrosshairRenderer = class {
|
|
|
6277
6324
|
this._priceLabel.text = priceText;
|
|
6278
6325
|
this._priceLabel.x = pw + 4;
|
|
6279
6326
|
this._priceLabel.y = y - this._priceLabel.height / 2;
|
|
6280
|
-
const time = transform.xToTime(x);
|
|
6327
|
+
const time = this._snapTime ?? transform.xToTime(x);
|
|
6281
6328
|
const timeText = this._formatTime(time);
|
|
6282
6329
|
const tlw = DAILY_INTERVALS.has(this.interval) ? 120 : 170;
|
|
6283
6330
|
const tlh = tsh - 4;
|
|
@@ -8674,11 +8721,11 @@ var PixiChart = class {
|
|
|
8674
8721
|
onCrosshairMove: (x, y) => {
|
|
8675
8722
|
if (!this._crosshairEnabled) return;
|
|
8676
8723
|
if (this._magnetMode === "off") {
|
|
8677
|
-
this._crosshairRenderer.update(x, y);
|
|
8724
|
+
this._crosshairRenderer.update(x, y, this._cursorBucketTime(x));
|
|
8678
8725
|
} else {
|
|
8679
8726
|
const snap = this.snapXToBar(x);
|
|
8680
8727
|
const sy = this._magnetMode === "snap-xy" ? snap.closeY ?? y : y;
|
|
8681
|
-
this._crosshairRenderer.update(snap.x, sy);
|
|
8728
|
+
this._crosshairRenderer.update(snap.x, sy, snap.time);
|
|
8682
8729
|
}
|
|
8683
8730
|
const ah = this._interaction.axisHover;
|
|
8684
8731
|
if (ah !== this._lastAxisHover) {
|
|
@@ -8724,7 +8771,9 @@ var PixiChart = class {
|
|
|
8724
8771
|
},
|
|
8725
8772
|
onDrawPointerMove: (x, y) => {
|
|
8726
8773
|
this._drawingCursorPt = {
|
|
8727
|
-
time:
|
|
8774
|
+
// Bucket time, not raw: what the crosshair label shows is what a
|
|
8775
|
+
// drawing placed here will store.
|
|
8776
|
+
time: this._cursorBucketTime(x),
|
|
8728
8777
|
price: this._transform.yToPrice(y)
|
|
8729
8778
|
};
|
|
8730
8779
|
this._layers.dirty.mark(3 /* Drawing */);
|
|
@@ -9291,6 +9340,18 @@ var PixiChart = class {
|
|
|
9291
9340
|
setNativeCursorHidden(hidden) {
|
|
9292
9341
|
this._interaction.setHideCursor(hidden);
|
|
9293
9342
|
}
|
|
9343
|
+
/** Bucket time for a cursor x — see core/cursorTime.ts for the rule. */
|
|
9344
|
+
_cursorBucketTime(x) {
|
|
9345
|
+
let bars = null;
|
|
9346
|
+
for (const s of this._series) {
|
|
9347
|
+
const data = s.data();
|
|
9348
|
+
if (data.length > 0) {
|
|
9349
|
+
bars = data;
|
|
9350
|
+
break;
|
|
9351
|
+
}
|
|
9352
|
+
}
|
|
9353
|
+
return cursorBucketTime(bars, this._transform, this._interval, x);
|
|
9354
|
+
}
|
|
9294
9355
|
snapXToBar(x) {
|
|
9295
9356
|
const SNAP_PX = 20;
|
|
9296
9357
|
let bars = null;
|
|
@@ -9334,7 +9395,7 @@ var PixiChart = class {
|
|
|
9334
9395
|
closeY: t.priceToY(bar.close)
|
|
9335
9396
|
};
|
|
9336
9397
|
}
|
|
9337
|
-
return { x, time:
|
|
9398
|
+
return { x, time: this._cursorBucketTime(x) };
|
|
9338
9399
|
}
|
|
9339
9400
|
setIndicatorsVisible(v) {
|
|
9340
9401
|
this._indicatorsVisible = v;
|
|
@@ -10304,14 +10365,13 @@ function toOHLCV(bar) {
|
|
|
10304
10365
|
function _historyWindow(tf) {
|
|
10305
10366
|
return 500 * (parseTfSeconds(tf) ?? 3600);
|
|
10306
10367
|
}
|
|
10307
|
-
var
|
|
10368
|
+
var REFETCH_ON_DOUBT_DELAY_MS = 3e3;
|
|
10308
10369
|
function tfSeconds(tf) {
|
|
10309
10370
|
const m = /^(\d+)\s*(s|m|h|D|W|M)$/.exec(tf);
|
|
10310
10371
|
if (!m) return 0;
|
|
10311
10372
|
const mult = { s: 1, m: 60, h: 3600, D: 86400, W: 604800, M: 2592e3 }[m[2]];
|
|
10312
10373
|
return parseInt(m[1], 10) * mult;
|
|
10313
10374
|
}
|
|
10314
|
-
var LIVE_GAP_REPAIR_DELAY_MS = 3e3;
|
|
10315
10375
|
function _pageSize(tf) {
|
|
10316
10376
|
return 500 * (parseTfSeconds(tf) ?? 3600);
|
|
10317
10377
|
}
|
|
@@ -10347,9 +10407,11 @@ var DatafeedConnector = class {
|
|
|
10347
10407
|
*/
|
|
10348
10408
|
_emptyPageStreak = 0;
|
|
10349
10409
|
/**
|
|
10350
|
-
*
|
|
10351
|
-
*
|
|
10352
|
-
*
|
|
10410
|
+
* Nothing arms this any more — live-tick suppression went with the rest of
|
|
10411
|
+
* the client-side stitching once history became authoritative through the
|
|
10412
|
+
* forming bar. Kept (with its clear helper) because reconnect paths call
|
|
10413
|
+
* the helper and older subclasses may too; clearing a never-set timer is
|
|
10414
|
+
* free. Remove both in the next breaking cleanup.
|
|
10353
10415
|
*/
|
|
10354
10416
|
_batchSafetyTimer = null;
|
|
10355
10417
|
constructor(datafeed, series, callbacks) {
|
|
@@ -10373,12 +10435,10 @@ var DatafeedConnector = class {
|
|
|
10373
10435
|
const to = Math.floor(Date.now() / 1e3);
|
|
10374
10436
|
const from = to - _historyWindow(timeframe);
|
|
10375
10437
|
this._clearBatchTimer();
|
|
10376
|
-
let batchPending = false;
|
|
10377
10438
|
const engine = new CandleEngine({
|
|
10378
10439
|
// Every live tick mutates the engine's bar array and the chart series.
|
|
10379
10440
|
onBarUpdated: (bar) => {
|
|
10380
10441
|
if (this._activeUID !== uid2) return;
|
|
10381
|
-
if (batchPending) return;
|
|
10382
10442
|
const ohlcv = toOHLCV(bar);
|
|
10383
10443
|
this._series.update(ohlcv);
|
|
10384
10444
|
this._cb.onUpdate?.(ohlcv);
|
|
@@ -10432,54 +10492,33 @@ var DatafeedConnector = class {
|
|
|
10432
10492
|
}).catch(() => {
|
|
10433
10493
|
});
|
|
10434
10494
|
if (this._activeUID !== uid2) return;
|
|
10435
|
-
if (lastBarTime != null) {
|
|
10436
|
-
batchPending = true;
|
|
10437
|
-
this._batchSafetyTimer = setTimeout(() => {
|
|
10438
|
-
batchPending = false;
|
|
10439
|
-
this._batchSafetyTimer = null;
|
|
10440
|
-
}, BATCH_SUPPRESSION_MS);
|
|
10441
|
-
}
|
|
10442
10495
|
const tfSec = tfSeconds(timeframe);
|
|
10443
10496
|
let lastSeenBarTime = lastBarTime ?? 0;
|
|
10444
|
-
let
|
|
10445
|
-
const
|
|
10446
|
-
|
|
10447
|
-
|
|
10448
|
-
|
|
10449
|
-
if (repairTimer) clearTimeout(repairTimer);
|
|
10450
|
-
repairTimer = setTimeout(() => {
|
|
10451
|
-
repairTimer = null;
|
|
10497
|
+
let refetchTimer = null;
|
|
10498
|
+
const refetchOnDoubt = (fromTime, toTime) => {
|
|
10499
|
+
if (refetchTimer) clearTimeout(refetchTimer);
|
|
10500
|
+
refetchTimer = setTimeout(() => {
|
|
10501
|
+
refetchTimer = null;
|
|
10452
10502
|
if (this._activeUID !== uid2 || this._destroyed) return;
|
|
10453
10503
|
void this._datafeed.getHistoricalBars(symbol, timeframe, fromTime, toTime).then(({ bars: filled }) => {
|
|
10454
|
-
if (this._activeUID !== uid2) return;
|
|
10455
|
-
|
|
10456
|
-
const newest = filled
|
|
10457
|
-
|
|
10458
|
-
if (!covered && repairAttempts < REPAIR_MAX_ATTEMPTS) {
|
|
10459
|
-
repairAttempts += 1;
|
|
10460
|
-
if (repairTimer) clearTimeout(repairTimer);
|
|
10461
|
-
repairTimer = setTimeout(() => {
|
|
10462
|
-
repairTimer = null;
|
|
10463
|
-
repairGap(fromTime, toTime);
|
|
10464
|
-
}, REPAIR_RETRY_MS);
|
|
10465
|
-
} else if (covered) {
|
|
10466
|
-
repairAttempts = 0;
|
|
10467
|
-
}
|
|
10504
|
+
if (this._activeUID !== uid2 || filled.length === 0) return;
|
|
10505
|
+
engine.backfillGap(filled);
|
|
10506
|
+
const newest = filled[filled.length - 1].time;
|
|
10507
|
+
if (newest > lastSeenBarTime) lastSeenBarTime = newest;
|
|
10468
10508
|
}).catch(() => {
|
|
10469
10509
|
});
|
|
10470
|
-
},
|
|
10510
|
+
}, REFETCH_ON_DOUBT_DELAY_MS);
|
|
10471
10511
|
};
|
|
10472
10512
|
this._datafeed.subscribeBars(symbol, timeframe, (bar) => {
|
|
10473
10513
|
if (this._activeUID !== uid2) return;
|
|
10474
10514
|
const t = bar.time;
|
|
10475
10515
|
if (tfSec > 0 && lastSeenBarTime > 0 && t > lastSeenBarTime + tfSec) {
|
|
10476
|
-
|
|
10516
|
+
refetchOnDoubt(lastSeenBarTime, t);
|
|
10477
10517
|
}
|
|
10478
10518
|
if (t > lastSeenBarTime) lastSeenBarTime = t;
|
|
10479
10519
|
engine.applyLiveUpdate(bar);
|
|
10480
10520
|
}, uid2, lastBarTime, (bars) => {
|
|
10481
10521
|
this._clearBatchTimer();
|
|
10482
|
-
batchPending = false;
|
|
10483
10522
|
if (this._activeUID === uid2) {
|
|
10484
10523
|
engine.backfillGap(bars);
|
|
10485
10524
|
const newest = bars.length > 0 ? bars[bars.length - 1].time : 0;
|
|
@@ -10487,16 +10526,7 @@ var DatafeedConnector = class {
|
|
|
10487
10526
|
}
|
|
10488
10527
|
fireLoaded();
|
|
10489
10528
|
});
|
|
10490
|
-
if (result.filling)
|
|
10491
|
-
setTimeout(() => {
|
|
10492
|
-
if (this._activeUID !== uid2 || this._destroyed) return;
|
|
10493
|
-
void this._datafeed.getHistoricalBars(symbol, timeframe, from, to).then(({ bars: refetchedBars }) => {
|
|
10494
|
-
if (this._activeUID !== uid2) return;
|
|
10495
|
-
engine.backfillGap(refetchedBars);
|
|
10496
|
-
}).catch(() => {
|
|
10497
|
-
});
|
|
10498
|
-
}, 8e3);
|
|
10499
|
-
}
|
|
10529
|
+
if (result.filling) refetchOnDoubt(from, to);
|
|
10500
10530
|
}).catch((err) => {
|
|
10501
10531
|
if (this._activeUID !== uid2) return;
|
|
10502
10532
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -18312,7 +18342,7 @@ var DEMONSTRATION_STROKE = "rgba(255, 200, 50, 0.7)";
|
|
|
18312
18342
|
var DEMONSTRATION_COLOR = "var(--crosshair-overlay, rgba(255,255,255,0.75))";
|
|
18313
18343
|
|
|
18314
18344
|
// src/version.ts
|
|
18315
|
-
var FORGECHARTS_VERSION = "1.5.
|
|
18345
|
+
var FORGECHARTS_VERSION = "1.5.70" ;
|
|
18316
18346
|
function applyRuntimeConfig(caps, config) {
|
|
18317
18347
|
if (!config) return caps;
|
|
18318
18348
|
const orderEntry = config.enableOrderEntry === false ? false : caps.orderEntry;
|