acttrader-charts 1.0.16 → 1.0.17

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/README.md CHANGED
@@ -55,6 +55,8 @@ const chart = new ChartEngine({
55
55
  // No manual loadData() needed — the engine calls dataLoader on start and on timeframe/duration changes.
56
56
  ```
57
57
 
58
+ If the first fetch returns fewer than `minInitialBars` (default `10`), the engine automatically widens the lookback window and calls `dataLoader` again — up to the `maxLookbackMs` ceiling (default 365 days). This keeps the chart useful on weekends, holidays, or for instruments that just listed. If retries still yield zero bars, the engine shows a **"No data available"** overlay (customise the text via `labels.chart.noData`).
59
+
58
60
  ---
59
61
 
60
62
  ## Advanced Usage
@@ -160,6 +162,8 @@ chart.removeLevelByLabel('ORD-1');
160
162
  chart.setLevels([], 'label', 'price', 'pending'); // clear all of one type
161
163
  ```
162
164
 
165
+ **Visual differentiation:** pending orders and ES/EL entry working orders render as **dashed** lines tinted by side (`pendingBuyLine` green / `pendingSellLine` red). True open positions render as **solid** lines: green/red when `pnl` is set (sign-colored), otherwise `positionLine` (purple/indigo). The info-box border matches the line color. Each true open position also gets a small colored price tag on the right-side price axis showing the entry price — same visual language as the Bid/Ask tag — so you can read the entry price without hunting for the info box.
166
+
163
167
  **TFC events:**
164
168
 
165
169
  ```ts
@@ -236,6 +240,8 @@ When `enableTrading` is on and live BID/ASK data is streaming, hovering / activa
236
240
  | `minLots` | | `1` | Default lot size in the trade popover |
237
241
  | `tickActivityMs` | | `30000` | ms the stream dot stays green after last tick |
238
242
  | `maxCandles` | | `200` | Max bars fetched per data-load request |
243
+ | `minInitialBars` | | `10` | If `dataLoader` returns fewer bars, the fetch window auto-widens and retries (handles weekends, market closures, and sparse symbols) |
244
+ | `maxLookbackMs` | | `31_536_000_000` | Hard ceiling on auto-widening lookback (ms). Retries stop once the window reaches this. Default: 365 days |
239
245
  | `prefetchThreshold` | | `80` | Bars from start of data at which historical fetch triggers (min 20) |
240
246
  | `mobileBarDivisor` | | `2` | Divide desktop visible bar count on touch devices (`2`, `3`, or `4`) |
241
247
  | `momentumScrollEnabled` | | `true` | Enable momentum (kinetic) scrolling — chart coasts after a fast flick |
@@ -300,6 +300,8 @@ interface DialogLabels {
300
300
  interface ChartMiscLabels {
301
301
  /** Overlay text shown while data is being fetched. */
302
302
  loading: string;
303
+ /** Overlay text shown when a completed fetch returned zero bars. */
304
+ noData: string;
303
305
  /** Tooltip on the "jump to live edge" button. */
304
306
  scrollToLatest: string;
305
307
  /** Collapse button text in the indicator overlay when more than 2 indicators
@@ -679,6 +681,20 @@ interface ChartConfig {
679
681
  * of bars. Default: 200.
680
682
  */
681
683
  maxCandles?: number;
684
+ /**
685
+ * Minimum bars expected from the initial fetch before giving up. If the
686
+ * `dataLoader` returns fewer bars than this, ChartEngine automatically
687
+ * widens the lookback window and retries — this keeps charts useful after
688
+ * weekends, holidays, or for instruments with sparse recent history.
689
+ * Default: 10.
690
+ */
691
+ minInitialBars?: number;
692
+ /**
693
+ * Hard ceiling (in milliseconds) on how far back auto-widening retries
694
+ * can reach when chasing enough bars. Retries stop once the window meets
695
+ * or exceeds this value. Default: 365 days.
696
+ */
697
+ maxLookbackMs?: number;
682
698
  /**
683
699
  * How close (in bars) the viewport must be to the start of loaded data
684
700
  * before a historical fetch is triggered. Higher values prefetch earlier.
@@ -300,6 +300,8 @@ interface DialogLabels {
300
300
  interface ChartMiscLabels {
301
301
  /** Overlay text shown while data is being fetched. */
302
302
  loading: string;
303
+ /** Overlay text shown when a completed fetch returned zero bars. */
304
+ noData: string;
303
305
  /** Tooltip on the "jump to live edge" button. */
304
306
  scrollToLatest: string;
305
307
  /** Collapse button text in the indicator overlay when more than 2 indicators
@@ -679,6 +681,20 @@ interface ChartConfig {
679
681
  * of bars. Default: 200.
680
682
  */
681
683
  maxCandles?: number;
684
+ /**
685
+ * Minimum bars expected from the initial fetch before giving up. If the
686
+ * `dataLoader` returns fewer bars than this, ChartEngine automatically
687
+ * widens the lookback window and retries — this keeps charts useful after
688
+ * weekends, holidays, or for instruments with sparse recent history.
689
+ * Default: 10.
690
+ */
691
+ minInitialBars?: number;
692
+ /**
693
+ * Hard ceiling (in milliseconds) on how far back auto-widening retries
694
+ * can reach when chasing enough bars. Retries stop once the window meets
695
+ * or exceeds this value. Default: 365 days.
696
+ */
697
+ maxLookbackMs?: number;
682
698
  /**
683
699
  * How close (in bars) the viewport must be to the start of loaded data
684
700
  * before a historical fetch is triggered. Higher values prefetch earlier.
package/dist/index.cjs CHANGED
@@ -722,6 +722,7 @@ var DEFAULT_LABELS = {
722
722
  chart: {
723
723
  loading: "Loading\u2026",
724
724
  // 'Loading…'
725
+ noData: "No data available",
725
726
  scrollToLatest: "Scroll to latest",
726
727
  nIndicators: "{count} indicators"
727
728
  }
@@ -1826,6 +1827,10 @@ function monoBoxBg(theme, active) {
1826
1827
  }
1827
1828
  function levelColor(level, colors, theme) {
1828
1829
  if (level.type === "position") {
1830
+ const pos = level;
1831
+ if (pos.entryPriceEditable && pos.side) {
1832
+ return pos.side === "buy" ? colors.pendingBuyLine : colors.pendingSellLine;
1833
+ }
1829
1834
  const pnl = level.pnl;
1830
1835
  if (pnl !== void 0) return pnl >= 0 ? theme.candle.up : theme.candle.down;
1831
1836
  return colors.positionLine;
@@ -2356,11 +2361,12 @@ function drawLevel(ctx, level, layout, chartW, colors, theme, precision, hovered
2356
2361
  }
2357
2362
  }
2358
2363
  if (isMarketDraft) return;
2364
+ const isEntryOrderPos = level.type === "position" && level.entryPriceEditable === true;
2359
2365
  ctx.save();
2360
- ctx.strokeStyle = monoColor(theme);
2366
+ ctx.strokeStyle = color;
2361
2367
  ctx.lineWidth = LINE_WIDTH;
2362
- ctx.globalAlpha = active ? 0.9 : 0.6;
2363
- if (isPending) {
2368
+ ctx.globalAlpha = active ? 1 : 0.85;
2369
+ if (isPending || isEntryOrderPos) {
2364
2370
  ctx.setLineDash([5, 4]);
2365
2371
  }
2366
2372
  ctx.beginPath();
@@ -2373,15 +2379,14 @@ function drawLevel(ctx, level, layout, chartW, colors, theme, precision, hovered
2373
2379
  const midY = boxY + boxH / 2;
2374
2380
  ctx.save();
2375
2381
  ctx.fillStyle = monoBoxBg(theme, active);
2376
- ctx.strokeStyle = monoColor(theme);
2382
+ ctx.strokeStyle = color;
2377
2383
  ctx.lineWidth = 1;
2378
2384
  roundRect(ctx, boxX, boxY, boxW, boxH, BOX_RADIUS);
2379
2385
  ctx.fill();
2380
2386
  ctx.stroke();
2381
2387
  let textX = boxX + BOX_PAD_X;
2382
2388
  const isDraftLevel = level.data?._isDraft === true;
2383
- const isEntryOrderPosition = level.type === "position" && level.entryPriceEditable === true;
2384
- const supportsQtyField = level.type === "pending" || isEntryOrderPosition;
2389
+ const supportsQtyField = level.type === "pending" || isEntryOrderPos;
2385
2390
  const levelLots = level.lots;
2386
2391
  if (showQuantityField && (isDraftLevel || selected || hasPendingChanges) && supportsQtyField && levelLots !== void 0) {
2387
2392
  const qtyText = String(levelLots);
@@ -2787,6 +2792,43 @@ function hitTestTradeLevelDrag(x, y, dragAreas) {
2787
2792
  function hitTestTradeLevelAddBracket(x, y, areas) {
2788
2793
  return areas.find((a) => x >= a.x && x <= a.x + a.w && y >= a.y && y <= a.y + a.h) ?? null;
2789
2794
  }
2795
+ function renderPositionAxisTags(ctx, levels, scale, priceRange, theme, chartW, priceH, precision) {
2796
+ const tagH = 16;
2797
+ const items = [];
2798
+ for (const level of levels) {
2799
+ if (level.type !== "position") continue;
2800
+ const pos = level;
2801
+ if (pos.entryPriceEditable) continue;
2802
+ const y = scale.yToPixel(pos.price, priceRange, priceH);
2803
+ if (y < -tagH || y > priceH + tagH) continue;
2804
+ items.push({
2805
+ y,
2806
+ label: pos.price.toFixed(precision),
2807
+ color: levelColor(level, theme.tradeLevels, theme)
2808
+ });
2809
+ }
2810
+ if (items.length === 0) return;
2811
+ items.sort((a, b) => a.y - b.y);
2812
+ let lastBottom = -Infinity;
2813
+ for (const it of items) {
2814
+ if (it.y - tagH / 2 < lastBottom) {
2815
+ it.y = lastBottom + tagH / 2;
2816
+ }
2817
+ lastBottom = it.y + tagH / 2 + 1;
2818
+ }
2819
+ ctx.save();
2820
+ ctx.font = "10px 'Inter', system-ui, sans-serif";
2821
+ ctx.textAlign = "left";
2822
+ ctx.textBaseline = "middle";
2823
+ for (const it of items) {
2824
+ const tw = ctx.measureText(it.label).width;
2825
+ ctx.fillStyle = it.color;
2826
+ ctx.fillRect(chartW, it.y - tagH / 2, tw + 12, tagH);
2827
+ ctx.fillStyle = "#ffffff";
2828
+ ctx.fillText(it.label, chartW + 4, it.y);
2829
+ }
2830
+ ctx.restore();
2831
+ }
2790
2832
 
2791
2833
  // src/overlays/OffViewportLevelIndicator.ts
2792
2834
  function collectOffViewportMarkers(levels, priceRange, draggingLabel) {
@@ -17333,6 +17375,8 @@ var _ChartEngine = class _ChartEngine {
17333
17375
  this.noMoreData = false;
17334
17376
  this._loadGeneration = 0;
17335
17377
  this.maxCandles = 200;
17378
+ this.minInitialBars = 10;
17379
+ this.maxLookbackMs = 365 * 24 * 60 * 60 * 1e3;
17336
17380
  this.prefetchThreshold = 80;
17337
17381
  this.defaultVisibleBars = DEFAULT_VISIBLE_BARS;
17338
17382
  this.defaultMobileVisibleBars = void 0;
@@ -17344,6 +17388,7 @@ var _ChartEngine = class _ChartEngine {
17344
17388
  this.maxSubPanes = 3;
17345
17389
  this.showCandleCountdown = true;
17346
17390
  this.loadingOverlayEl = null;
17391
+ this.noDataOverlayEl = null;
17347
17392
  // Watermark
17348
17393
  this.logoImage = null;
17349
17394
  // ── State persistence ─────────────────────────────────────────────────────
@@ -18338,6 +18383,8 @@ var _ChartEngine = class _ChartEngine {
18338
18383
  this.labels = resolveLabels(labels);
18339
18384
  this.tickActivityMs = tickActivityMs;
18340
18385
  this.maxCandles = config.maxCandles ?? 200;
18386
+ this.minInitialBars = Math.max(0, config.minInitialBars ?? 10);
18387
+ this.maxLookbackMs = Math.max(0, config.maxLookbackMs ?? 365 * 24 * 60 * 60 * 1e3);
18341
18388
  this.prefetchThreshold = config.prefetchThreshold ?? 80;
18342
18389
  this.defaultVisibleBars = config.defaultVisibleBars ?? DEFAULT_VISIBLE_BARS;
18343
18390
  this.defaultMobileVisibleBars = config.defaultMobileVisibleBars;
@@ -18608,6 +18655,30 @@ var _ChartEngine = class _ChartEngine {
18608
18655
  });
18609
18656
  this.loadingOverlayEl.appendChild(loadingLabel);
18610
18657
  this.canvasWrap.appendChild(this.loadingOverlayEl);
18658
+ this.noDataOverlayEl = document.createElement("div");
18659
+ Object.assign(this.noDataOverlayEl.style, {
18660
+ display: "none",
18661
+ position: "absolute",
18662
+ top: "0",
18663
+ left: "0",
18664
+ width: "100%",
18665
+ height: "100%",
18666
+ alignItems: "center",
18667
+ justifyContent: "center",
18668
+ pointerEvents: "none",
18669
+ zIndex: "19"
18670
+ });
18671
+ const noDataLabel = document.createElement("span");
18672
+ noDataLabel.textContent = this.labels.chart.noData;
18673
+ Object.assign(noDataLabel.style, {
18674
+ color: "rgba(200,200,200,0.9)",
18675
+ fontSize: "13px",
18676
+ background: "rgba(0,0,0,0.45)",
18677
+ padding: "4px 12px",
18678
+ borderRadius: "4px"
18679
+ });
18680
+ this.noDataOverlayEl.appendChild(noDataLabel);
18681
+ this.canvasWrap.appendChild(this.noDataOverlayEl);
18611
18682
  }
18612
18683
  if (showUI && !hideSymbolAndTick) {
18613
18684
  this.siEl = document.createElement("div");
@@ -18948,6 +19019,7 @@ var _ChartEngine = class _ChartEngine {
18948
19019
  resetData() {
18949
19020
  ++this._loadGeneration;
18950
19021
  this.setLoading(false);
19022
+ this.setNoData(false);
18951
19023
  this.drawingManager.clearAll();
18952
19024
  this.levels = [];
18953
19025
  this._draftQtyPillArea = void 0;
@@ -18958,6 +19030,7 @@ var _ChartEngine = class _ChartEngine {
18958
19030
  this.isFetching = false;
18959
19031
  this.noMoreData = false;
18960
19032
  this._suppressTicks = false;
19033
+ if (bars.length > 0) this.setNoData(false);
18961
19034
  this.liveBidAsk = null;
18962
19035
  this.pricePanOffset = 0;
18963
19036
  this.priceScaleFactor = 1;
@@ -19037,6 +19110,14 @@ var _ChartEngine = class _ChartEngine {
19037
19110
  if (this.loadingOverlayEl) {
19038
19111
  this.loadingOverlayEl.style.display = loading ? "flex" : "none";
19039
19112
  }
19113
+ if (loading) this.setNoData(false);
19114
+ return this;
19115
+ }
19116
+ /** Shows or hides the "No data available" overlay. */
19117
+ setNoData(show) {
19118
+ if (this.noDataOverlayEl) {
19119
+ this.noDataOverlayEl.style.display = show ? "flex" : "none";
19120
+ }
19040
19121
  return this;
19041
19122
  }
19042
19123
  // ── Data loading helpers ──────────────────────────────────────────────────
@@ -19109,28 +19190,43 @@ var _ChartEngine = class _ChartEngine {
19109
19190
  this.isFetching = true;
19110
19191
  this.noMoreData = false;
19111
19192
  const end = /* @__PURE__ */ new Date();
19112
- const maxCandleStart = new Date(
19113
- end.getTime() - this.maxCandles * this.timeframeToMs(this.timeframe)
19114
- );
19115
- const durationStart = this.duration ? new Date(end.getTime() - this.durationToMs(this.duration)) : maxCandleStart;
19116
- const start = new Date(Math.max(durationStart.getTime(), maxCandleStart.getTime()));
19193
+ const maxCandleMs = this.maxCandles * this.timeframeToMs(this.timeframe);
19194
+ const durationMs = this.duration ? this.durationToMs(this.duration) : maxCandleMs;
19195
+ const baseWindowMs = Math.min(durationMs, maxCandleMs);
19117
19196
  const interval = this.timeframeToInterval(this.getBaseInterval());
19118
19197
  this.setLoading(true);
19119
- this.dataLoader({ start, end, interval }).then((bars) => {
19120
- if (gen !== this._loadGeneration) return;
19121
- this.loadData(this.maybeAggregate(bars), reason === "duration");
19122
- this.setLoading(false);
19123
- this.emitter.emit("dataLoaded", {
19124
- timeframe: this.timeframe,
19125
- interval,
19126
- start,
19127
- end
19198
+ const loader = this.dataLoader;
19199
+ const expansionFactors = [1, 5, 25, 125];
19200
+ const minBars = this.minInitialBars;
19201
+ const cap = this.maxLookbackMs;
19202
+ const attempt = (i, start) => {
19203
+ loader({ start, end, interval }).then((bars) => {
19204
+ if (gen !== this._loadGeneration) return;
19205
+ const aggregated = this.maybeAggregate(bars);
19206
+ const reachedCap = end.getTime() - start.getTime() >= cap;
19207
+ if (aggregated.length >= minBars || i + 1 >= expansionFactors.length || reachedCap) {
19208
+ this.loadData(aggregated, reason === "duration");
19209
+ this.setLoading(false);
19210
+ if (aggregated.length === 0) this.setNoData(true);
19211
+ this.emitter.emit("dataLoaded", {
19212
+ timeframe: this.timeframe,
19213
+ interval,
19214
+ start,
19215
+ end
19216
+ });
19217
+ return;
19218
+ }
19219
+ const nextWindowMs = Math.min(baseWindowMs * expansionFactors[i + 1], cap);
19220
+ const nextStart = new Date(end.getTime() - nextWindowMs);
19221
+ attempt(i + 1, nextStart);
19222
+ }).catch(() => {
19223
+ if (gen !== this._loadGeneration) return;
19224
+ this.isFetching = false;
19225
+ this.setLoading(false);
19128
19226
  });
19129
- }).catch(() => {
19130
- if (gen !== this._loadGeneration) return;
19131
- this.isFetching = false;
19132
- this.setLoading(false);
19133
- });
19227
+ };
19228
+ const firstWindowMs = Math.min(baseWindowMs * expansionFactors[0], cap);
19229
+ attempt(0, new Date(end.getTime() - firstWindowMs));
19134
19230
  }
19135
19231
  checkNeedMoreData() {
19136
19232
  if (!this.dataLoader || this.isFetching || this.noMoreData) return;
@@ -19141,22 +19237,36 @@ var _ChartEngine = class _ChartEngine {
19141
19237
  this.isFetching = true;
19142
19238
  const before = this.dataStore.all[0].time;
19143
19239
  const end = new Date(before - 1);
19144
- const maxCandleStart = new Date(
19145
- before - this.maxCandles * this.timeframeToMs(this.timeframe)
19146
- );
19147
- const durationStart = this.duration ? new Date(before - this.durationToMs(this.duration)) : maxCandleStart;
19148
- const start = new Date(Math.max(durationStart.getTime(), maxCandleStart.getTime()));
19240
+ const maxCandleMs = this.maxCandles * this.timeframeToMs(this.timeframe);
19241
+ const durationMs = this.duration ? this.durationToMs(this.duration) : maxCandleMs;
19242
+ const baseWindowMs = Math.min(durationMs, maxCandleMs);
19149
19243
  const interval = this.timeframeToInterval(this.getBaseInterval());
19150
19244
  this.setLoading(true);
19151
- this.dataLoader({ start, end, interval }).then((bars) => {
19152
- if (gen !== this._loadGeneration) return;
19153
- this.prependData(this.maybeAggregate(bars));
19154
- this.setLoading(false);
19155
- }).catch(() => {
19156
- if (gen !== this._loadGeneration) return;
19157
- this.isFetching = false;
19158
- this.setLoading(false);
19159
- });
19245
+ const loader = this.dataLoader;
19246
+ const expansionFactors = [1, 5, 25, 125];
19247
+ const minBars = this.minInitialBars;
19248
+ const cap = this.maxLookbackMs;
19249
+ const attempt = (i, start) => {
19250
+ loader({ start, end, interval }).then((bars) => {
19251
+ if (gen !== this._loadGeneration) return;
19252
+ const aggregated = this.maybeAggregate(bars);
19253
+ const reachedCap = end.getTime() - start.getTime() >= cap;
19254
+ if (aggregated.length >= minBars || i + 1 >= expansionFactors.length || reachedCap) {
19255
+ this.prependData(aggregated);
19256
+ this.setLoading(false);
19257
+ return;
19258
+ }
19259
+ const nextWindowMs = Math.min(baseWindowMs * expansionFactors[i + 1], cap);
19260
+ const nextStart = new Date(end.getTime() - nextWindowMs);
19261
+ attempt(i + 1, nextStart);
19262
+ }).catch(() => {
19263
+ if (gen !== this._loadGeneration) return;
19264
+ this.isFetching = false;
19265
+ this.setLoading(false);
19266
+ });
19267
+ };
19268
+ const firstWindowMs = Math.min(baseWindowMs * expansionFactors[0], cap);
19269
+ attempt(0, new Date(end.getTime() - firstWindowMs));
19160
19270
  }
19161
19271
  addIndicator(indicator) {
19162
19272
  if (indicator.pane === "sub") {
@@ -20675,6 +20785,16 @@ var _ChartEngine = class _ChartEngine {
20675
20785
  this.selectedTradeLabel
20676
20786
  );
20677
20787
  this.offViewportHitAreas = offVpResult.hitAreas;
20788
+ renderPositionAxisTags(
20789
+ ctx,
20790
+ filteredLevels,
20791
+ this.scaleManager,
20792
+ priceRange,
20793
+ theme,
20794
+ chartW,
20795
+ priceH,
20796
+ this.dataStore.getPricePrecision()
20797
+ );
20678
20798
  ctx.restore();
20679
20799
  this.tradeLevelHitAreas = result.hitAreas;
20680
20800
  this.tradeLevelEditAreas = result.editAreas;