@forgecharts/sdk 1.1.31 → 1.1.34

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.
Files changed (163) hide show
  1. package/dist/api/IndicatorDAG.d.ts +2 -0
  2. package/dist/api/IndicatorDAG.d.ts.map +1 -1
  3. package/dist/api/TChart.d.ts +13 -0
  4. package/dist/api/TChart.d.ts.map +1 -1
  5. package/dist/api/drawingUtils.d.ts +6 -0
  6. package/dist/api/drawingUtils.d.ts.map +1 -1
  7. package/dist/core/Chart.d.ts +30 -1
  8. package/dist/core/Chart.d.ts.map +1 -1
  9. package/dist/core/Crosshair.d.ts.map +1 -1
  10. package/dist/core/InteractionManager.d.ts +16 -1
  11. package/dist/core/InteractionManager.d.ts.map +1 -1
  12. package/dist/core/TimeScale.d.ts.map +1 -1
  13. package/dist/datafeed/DatafeedConnector.d.ts.map +1 -1
  14. package/dist/{tscript/TScriptIndicator.d.ts → forgescript/ForgeScriptIndicator.d.ts} +19 -5
  15. package/dist/forgescript/ForgeScriptIndicator.d.ts.map +1 -0
  16. package/dist/forgescript/ForgeScriptTypes.d.ts +89 -0
  17. package/dist/forgescript/ForgeScriptTypes.d.ts.map +1 -0
  18. package/dist/forgescript/__tests__/ai-response-parser.test.d.ts +2 -0
  19. package/dist/forgescript/__tests__/ai-response-parser.test.d.ts.map +1 -0
  20. package/dist/forgescript/__tests__/language-detector.test.d.ts +2 -0
  21. package/dist/forgescript/__tests__/language-detector.test.d.ts.map +1 -0
  22. package/dist/forgescript/__tests__/lexer.test.d.ts +2 -0
  23. package/dist/forgescript/__tests__/lexer.test.d.ts.map +1 -0
  24. package/dist/forgescript/__tests__/orchestrator.test.d.ts +2 -0
  25. package/dist/forgescript/__tests__/orchestrator.test.d.ts.map +1 -0
  26. package/dist/forgescript/__tests__/parser.test.d.ts +2 -0
  27. package/dist/forgescript/__tests__/parser.test.d.ts.map +1 -0
  28. package/dist/forgescript/__tests__/pine-transpiler.test.d.ts +2 -0
  29. package/dist/forgescript/__tests__/pine-transpiler.test.d.ts.map +1 -0
  30. package/dist/forgescript/__tests__/runtime.test.d.ts +2 -0
  31. package/dist/forgescript/__tests__/runtime.test.d.ts.map +1 -0
  32. package/dist/forgescript/__tests__/sandbox.test.d.ts +2 -0
  33. package/dist/forgescript/__tests__/sandbox.test.d.ts.map +1 -0
  34. package/dist/forgescript/__tests__/save-gate.test.d.ts +2 -0
  35. package/dist/forgescript/__tests__/save-gate.test.d.ts.map +1 -0
  36. package/dist/forgescript/__tests__/series.test.d.ts +2 -0
  37. package/dist/forgescript/__tests__/series.test.d.ts.map +1 -0
  38. package/dist/forgescript/__tests__/telemetry.test.d.ts +2 -0
  39. package/dist/forgescript/__tests__/telemetry.test.d.ts.map +1 -0
  40. package/dist/forgescript/__tests__/validation.test.d.ts +2 -0
  41. package/dist/forgescript/__tests__/validation.test.d.ts.map +1 -0
  42. package/dist/forgescript/ast.d.ts +153 -0
  43. package/dist/forgescript/ast.d.ts.map +1 -0
  44. package/dist/forgescript/builtins/color.d.ts +29 -0
  45. package/dist/forgescript/builtins/color.d.ts.map +1 -0
  46. package/dist/forgescript/builtins/math.d.ts +22 -0
  47. package/dist/forgescript/builtins/math.d.ts.map +1 -0
  48. package/dist/forgescript/builtins/request.d.ts +22 -0
  49. package/dist/forgescript/builtins/request.d.ts.map +1 -0
  50. package/dist/forgescript/builtins/syminfo.d.ts +27 -0
  51. package/dist/forgescript/builtins/syminfo.d.ts.map +1 -0
  52. package/dist/forgescript/builtins/ta.d.ts +39 -0
  53. package/dist/forgescript/builtins/ta.d.ts.map +1 -0
  54. package/dist/forgescript/conversion/ai-conversion-agent.d.ts +41 -0
  55. package/dist/forgescript/conversion/ai-conversion-agent.d.ts.map +1 -0
  56. package/dist/forgescript/conversion/conversion-orchestrator.d.ts +44 -0
  57. package/dist/forgescript/conversion/conversion-orchestrator.d.ts.map +1 -0
  58. package/dist/forgescript/conversion/index.d.ts +19 -0
  59. package/dist/forgescript/conversion/index.d.ts.map +1 -0
  60. package/dist/forgescript/conversion/language-detector.d.ts +27 -0
  61. package/dist/forgescript/conversion/language-detector.d.ts.map +1 -0
  62. package/dist/forgescript/conversion/prompts/index.d.ts +17 -0
  63. package/dist/forgescript/conversion/prompts/index.d.ts.map +1 -0
  64. package/dist/forgescript/conversion/sandbox.d.ts +23 -0
  65. package/dist/forgescript/conversion/sandbox.d.ts.map +1 -0
  66. package/dist/forgescript/conversion/save-gate.d.ts +29 -0
  67. package/dist/forgescript/conversion/save-gate.d.ts.map +1 -0
  68. package/dist/forgescript/conversion/telemetry.d.ts +22 -0
  69. package/dist/forgescript/conversion/telemetry.d.ts.map +1 -0
  70. package/dist/forgescript/conversion/types.d.ts +120 -0
  71. package/dist/forgescript/conversion/types.d.ts.map +1 -0
  72. package/dist/forgescript/conversion/validation.d.ts +18 -0
  73. package/dist/forgescript/conversion/validation.d.ts.map +1 -0
  74. package/dist/{tscript → forgescript}/lexer.d.ts +7 -1
  75. package/dist/forgescript/lexer.d.ts.map +1 -0
  76. package/dist/{tscript → forgescript}/parser.d.ts +14 -0
  77. package/dist/forgescript/parser.d.ts.map +1 -0
  78. package/dist/{pine → forgescript/pine}/PineCompiler.d.ts +7 -4
  79. package/dist/forgescript/pine/PineCompiler.d.ts.map +1 -0
  80. package/dist/forgescript/pine/diagnostics.d.ts.map +1 -0
  81. package/dist/forgescript/pine/index.d.ts.map +1 -0
  82. package/dist/{pine → forgescript/pine}/pine-ast.d.ts +50 -2
  83. package/dist/forgescript/pine/pine-ast.d.ts.map +1 -0
  84. package/dist/forgescript/pine/pine-lexer.d.ts.map +1 -0
  85. package/dist/forgescript/pine/pine-parser.d.ts +108 -0
  86. package/dist/forgescript/pine/pine-parser.d.ts.map +1 -0
  87. package/dist/{pine → forgescript/pine}/pine-transpiler.d.ts +10 -3
  88. package/dist/forgescript/pine/pine-transpiler.d.ts.map +1 -0
  89. package/dist/forgescript/runtime.d.ts +307 -0
  90. package/dist/forgescript/runtime.d.ts.map +1 -0
  91. package/dist/forgescript/series.d.ts.map +1 -0
  92. package/dist/index.d.ts +9 -6
  93. package/dist/index.d.ts.map +1 -1
  94. package/dist/index.js +3365 -265
  95. package/dist/index.js.map +1 -1
  96. package/dist/internal.js +3409 -309
  97. package/dist/internal.js.map +1 -1
  98. package/dist/licensing/ChartRuntimeResolver.d.ts +4 -0
  99. package/dist/licensing/ChartRuntimeResolver.d.ts.map +1 -1
  100. package/dist/licensing/licenseTypes.d.ts +18 -0
  101. package/dist/licensing/licenseTypes.d.ts.map +1 -1
  102. package/dist/logo_dark-B2KCSRPJ.png +0 -0
  103. package/dist/logo_light-NAWNBY4G.png +0 -0
  104. package/dist/react/canvas/ChartCanvas.d.ts +26 -2
  105. package/dist/react/canvas/ChartCanvas.d.ts.map +1 -1
  106. package/dist/react/canvas/PointerOverlay.d.ts +3 -1
  107. package/dist/react/canvas/PointerOverlay.d.ts.map +1 -1
  108. package/dist/react/canvas/TableOverlay.d.ts +12 -0
  109. package/dist/react/canvas/TableOverlay.d.ts.map +1 -0
  110. package/dist/react/canvas/toolbars/LeftToolbar.d.ts +14 -1
  111. package/dist/react/canvas/toolbars/LeftToolbar.d.ts.map +1 -1
  112. package/dist/react/hooks/useBrokerEvents.d.ts +32 -0
  113. package/dist/react/hooks/useBrokerEvents.d.ts.map +1 -0
  114. package/dist/react/index.js +2615 -380
  115. package/dist/react/index.js.map +1 -1
  116. package/dist/react/internal.d.ts +1 -1
  117. package/dist/react/internal.d.ts.map +1 -1
  118. package/dist/react/internal.js +6068 -849
  119. package/dist/react/internal.js.map +1 -1
  120. package/dist/react/shell/ManagedAppShell.d.ts +28 -0
  121. package/dist/react/shell/ManagedAppShell.d.ts.map +1 -1
  122. package/dist/react/shell/OrderEntryPanel.d.ts +96 -0
  123. package/dist/react/shell/OrderEntryPanel.d.ts.map +1 -0
  124. package/dist/react/shell/ScriptDrawer.d.ts +5 -1
  125. package/dist/react/shell/ScriptDrawer.d.ts.map +1 -1
  126. package/dist/react/shell/TradeDrawer.d.ts +25 -1
  127. package/dist/react/shell/TradeDrawer.d.ts.map +1 -1
  128. package/dist/react/shell/WatchlistDrawer.d.ts.map +1 -1
  129. package/dist/react/shell/useWatchlistQuotes.d.ts +1 -0
  130. package/dist/react/shell/useWatchlistQuotes.d.ts.map +1 -1
  131. package/dist/react/workspace/ChartWorkspace.d.ts +8 -1
  132. package/dist/react/workspace/ChartWorkspace.d.ts.map +1 -1
  133. package/dist/react/workspace/SymbolSearchDialog.d.ts.map +1 -1
  134. package/dist/react/workspace/TabBar.d.ts.map +1 -1
  135. package/dist/react/workspace/toolbars/BottomToolbar.d.ts.map +1 -1
  136. package/dist/react/workspace/toolbars/RightToolbar.d.ts +4 -1
  137. package/dist/react/workspace/toolbars/RightToolbar.d.ts.map +1 -1
  138. package/dist/react/workspace/toolbars/TopToolbar.d.ts +2 -1
  139. package/dist/react/workspace/toolbars/TopToolbar.d.ts.map +1 -1
  140. package/dist/renderers/CandlestickRenderer.d.ts.map +1 -1
  141. package/dist/services/serverClock.d.ts +61 -0
  142. package/dist/services/serverClock.d.ts.map +1 -0
  143. package/package.json +1 -1
  144. package/dist/pine/PineCompiler.d.ts.map +0 -1
  145. package/dist/pine/diagnostics.d.ts.map +0 -1
  146. package/dist/pine/index.d.ts.map +0 -1
  147. package/dist/pine/pine-ast.d.ts.map +0 -1
  148. package/dist/pine/pine-lexer.d.ts.map +0 -1
  149. package/dist/pine/pine-parser.d.ts +0 -51
  150. package/dist/pine/pine-parser.d.ts.map +0 -1
  151. package/dist/pine/pine-transpiler.d.ts.map +0 -1
  152. package/dist/tscript/TScriptIndicator.d.ts.map +0 -1
  153. package/dist/tscript/ast.d.ts +0 -89
  154. package/dist/tscript/ast.d.ts.map +0 -1
  155. package/dist/tscript/lexer.d.ts.map +0 -1
  156. package/dist/tscript/parser.d.ts.map +0 -1
  157. package/dist/tscript/runtime.d.ts +0 -123
  158. package/dist/tscript/runtime.d.ts.map +0 -1
  159. package/dist/tscript/series.d.ts.map +0 -1
  160. /package/dist/{pine → forgescript/pine}/diagnostics.d.ts +0 -0
  161. /package/dist/{pine → forgescript/pine}/index.d.ts +0 -0
  162. /package/dist/{pine → forgescript/pine}/pine-lexer.d.ts +0 -0
  163. /package/dist/{tscript → forgescript}/series.d.ts +0 -0
@@ -97,9 +97,73 @@ function computeTicks(min, max, targetCount) {
97
97
  return ticks;
98
98
  }
99
99
 
100
+ // ../shared/src/time/timeframeRegistry.ts
101
+ function isCalendarTimeframe(key) {
102
+ return key === "1w" || key === "1M" || key === "3M" || key === "6M" || key === "12M";
103
+ }
104
+
105
+ // ../shared/src/time/timeUtils.ts
106
+ var MONDAY_EPOCH_OFFSET_MS = 3456e5;
107
+ var WEEK_MS = 6048e5;
108
+ function timeframeToMs(tf) {
109
+ if (isCalendarTimeframe(tf)) return null;
110
+ const secs = parseTfSeconds(tf);
111
+ return secs !== null ? secs * 1e3 : null;
112
+ }
113
+ function parseTfSeconds(tf) {
114
+ const m = /^(\d+)(s|m|h|d|w|M)$/.exec(tf);
115
+ if (!m) return null;
116
+ const n = parseInt(m[1], 10);
117
+ const unit = m[2];
118
+ const UNIT_SECONDS = {
119
+ s: 1,
120
+ m: 60,
121
+ h: 3600,
122
+ d: 86400,
123
+ w: 604800,
124
+ M: 2592e3
125
+ };
126
+ const unitSec = UNIT_SECONDS[unit];
127
+ return unitSec !== void 0 ? n * unitSec : null;
128
+ }
129
+ function getBucketStart(timeMs, timeframe) {
130
+ if (timeframe === "1w") return getWeekBucketStart(timeMs);
131
+ if (timeframe === "1M") return getMonthBucketStart(timeMs);
132
+ if (timeframe === "3M") return getQuarterBucketStart(timeMs);
133
+ if (timeframe === "6M") return getHalfYearBucketStart(timeMs);
134
+ if (timeframe === "12M") return getYearBucketStart(timeMs);
135
+ const secs = parseTfSeconds(timeframe);
136
+ if (secs && secs > 0) return Math.floor(timeMs / (secs * 1e3)) * (secs * 1e3);
137
+ return Math.floor(timeMs / 6e4) * 6e4;
138
+ }
139
+ function getWeekBucketStart(timeMs) {
140
+ return Math.floor((timeMs - MONDAY_EPOCH_OFFSET_MS) / WEEK_MS) * WEEK_MS + MONDAY_EPOCH_OFFSET_MS;
141
+ }
142
+ function getMonthBucketStart(timeMs) {
143
+ const d = new Date(timeMs);
144
+ return Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), 1);
145
+ }
146
+ function getQuarterBucketStart(timeMs) {
147
+ const d = new Date(timeMs);
148
+ const quarterMonth = Math.floor(d.getUTCMonth() / 3) * 3;
149
+ return Date.UTC(d.getUTCFullYear(), quarterMonth, 1);
150
+ }
151
+ function getHalfYearBucketStart(timeMs) {
152
+ const d = new Date(timeMs);
153
+ const halfMonth = d.getUTCMonth() < 6 ? 0 : 6;
154
+ return Date.UTC(d.getUTCFullYear(), halfMonth, 1);
155
+ }
156
+ function getYearBucketStart(timeMs) {
157
+ return Date.UTC(new Date(timeMs).getUTCFullYear(), 0, 1);
158
+ }
159
+
100
160
  // ../utils/src/time.ts
161
+ function parseTimeframeSeconds(tf) {
162
+ return parseTfSeconds(tf);
163
+ }
101
164
  function formatTimestampLabel(ts, stepSeconds, tz) {
102
- const timeZone = !tz || tz === "exchange" ? "UTC" : tz;
165
+ const userTZ = !tz || tz === "exchange" ? "UTC" : tz;
166
+ const timeZone = stepSeconds >= 86400 ? "UTC" : userTZ;
103
167
  const d = new Date(ts * 1e3);
104
168
  if (stepSeconds < 3600) {
105
169
  return new Intl.DateTimeFormat("en-US", {
@@ -130,9 +194,19 @@ function formatTimestampLabel(ts, stepSeconds, tz) {
130
194
  );
131
195
  const yearStr = new Intl.DateTimeFormat("en-US", { year: "numeric", timeZone }).format(d);
132
196
  const monthStr = new Intl.DateTimeFormat("en-US", { month: "short", timeZone }).format(d);
133
- if (stepSeconds >= 25 * 86400) {
197
+ if (stepSeconds >= 182 * 86400) {
134
198
  return month === 1 ? yearStr : monthStr;
135
199
  }
200
+ if (stepSeconds <= 3 * 86400) {
201
+ const dayNum2 = parseInt(
202
+ new Intl.DateTimeFormat("en-US", { day: "numeric", timeZone }).format(d),
203
+ 10
204
+ );
205
+ if (dayNum2 === 1) {
206
+ return month === 1 ? yearStr : monthStr;
207
+ }
208
+ return String(dayNum2);
209
+ }
136
210
  const dayNum = parseInt(
137
211
  new Intl.DateTimeFormat("en-US", { day: "numeric", timeZone }).format(d),
138
212
  10
@@ -270,6 +344,8 @@ var TimeScale = class {
270
344
  return 70;
271
345
  }
272
346
  _niceTimeStep(rawSeconds) {
347
+ const minStep = parseTimeframeSeconds(this._activeTimeframe) ?? 1;
348
+ const effectiveRaw = Math.max(rawSeconds, minStep);
273
349
  const nice = [
274
350
  1,
275
351
  5,
@@ -287,6 +363,10 @@ var TimeScale = class {
287
363
  43200,
288
364
  86400,
289
365
  // 1 day
366
+ 2 * 86400,
367
+ // 2 days
368
+ 3 * 86400,
369
+ // 3 days
290
370
  7 * 86400,
291
371
  // 1 week
292
372
  14 * 86400,
@@ -301,9 +381,9 @@ var TimeScale = class {
301
381
  // ~1 year
302
382
  ];
303
383
  for (const s of nice) {
304
- if (s >= rawSeconds) return s;
384
+ if (s >= effectiveRaw) return s;
305
385
  }
306
- return 365 * 86400 * Math.ceil(rawSeconds / (365 * 86400));
386
+ return 365 * 86400 * Math.ceil(effectiveRaw / (365 * 86400));
307
387
  }
308
388
  };
309
389
 
@@ -738,17 +818,18 @@ var Crosshair = class {
738
818
  }
739
819
  /** Formats a Unix timestamp (seconds) as a human-readable date/time string. */
740
820
  _formatTime(time) {
741
- const timeZone = !this.timezone || this.timezone === "exchange" ? "UTC" : this.timezone;
821
+ const userTZ = !this.timezone || this.timezone === "exchange" ? "UTC" : this.timezone;
742
822
  const d = new Date(time * 1e3);
743
- const weekday = d.toLocaleDateString("en-US", { weekday: "short", timeZone });
744
- const day = d.toLocaleDateString("en-US", { day: "2-digit", timeZone });
745
- const month = d.toLocaleDateString("en-US", { month: "short", timeZone });
746
- const year = d.toLocaleDateString("en-US", { year: "2-digit", timeZone });
747
- const datePart = `${weekday} ${day} ${month} '${year}`;
748
823
  const dailyIntervals = /* @__PURE__ */ new Set(["1d", "2d", "3d", "1w", "2w", "1M", "3M", "6M", "12M"]);
824
+ const dateZone = dailyIntervals.has(this.interval) ? "UTC" : userTZ;
825
+ const weekday = d.toLocaleDateString("en-US", { weekday: "short", timeZone: dateZone });
826
+ const day = d.toLocaleDateString("en-US", { day: "2-digit", timeZone: dateZone });
827
+ const month = d.toLocaleDateString("en-US", { month: "short", timeZone: dateZone });
828
+ const year = d.toLocaleDateString("en-US", { year: "2-digit", timeZone: dateZone });
829
+ const datePart = `${weekday} ${day} ${month} '${year}`;
749
830
  if (dailyIntervals.has(this.interval)) return datePart;
750
- const hh = d.toLocaleString("en-US", { hour: "2-digit", hour12: false, timeZone });
751
- const mm = d.toLocaleString("en-US", { minute: "2-digit", timeZone }).padStart(2, "0");
831
+ const hh = d.toLocaleString("en-US", { hour: "2-digit", hour12: false, timeZone: userTZ });
832
+ const mm = d.toLocaleString("en-US", { minute: "2-digit", timeZone: userTZ }).padStart(2, "0");
752
833
  return `${datePart} ${hh}:${mm}`;
753
834
  }
754
835
  };
@@ -787,12 +868,8 @@ var CandlestickRenderer = class {
787
868
  const pixelsPerSecond = visibleSpan > 0 ? plotW / visibleSpan : 1;
788
869
  const barPitch = candleInterval > 0 ? candleInterval * pixelsPerSecond : plotW / Math.max(1, plotData.length);
789
870
  const minBarWidth = 1;
790
- const maxBarWidth = 20;
791
871
  const gap = Math.max(1, Math.round(barPitch * 0.15));
792
- const barWidth = Math.min(
793
- maxBarWidth,
794
- Math.max(minBarWidth, Math.floor(barPitch) - gap)
795
- );
872
+ const barWidth = Math.max(minBarWidth, Math.floor(barPitch) - gap);
796
873
  const halfBar = Math.max(0.5, barWidth / 2);
797
874
  for (const bar of plotData) {
798
875
  const x = mapRange(
@@ -1045,6 +1122,8 @@ var InteractionManager = class {
1045
1122
  // ── Hit-test data feeds ───────────────────────────────────────────────────
1046
1123
  _drawings = [];
1047
1124
  _bars = [];
1125
+ /** Overlay indicator polylines for hit-testing. Each entry is an indicator id + its data points. */
1126
+ _indicatorLines = [];
1048
1127
  // ── Mouse state ───────────────────────────────────────────────────────────
1049
1128
  _isDragging = false;
1050
1129
  _dragStartX = 0;
@@ -1061,9 +1140,11 @@ var InteractionManager = class {
1061
1140
  _drawingMode = false;
1062
1141
  /** True when the native cursor should be hidden (crosshair / dot / demonstration). */
1063
1142
  _hideCursor = false;
1064
- /** Sets cursor style, respecting _hideCursor — 'none' always wins when hiding. */
1143
+ /** Sets cursor style, respecting _hideCursor — axis resize cursors always show
1144
+ * even in crosshair/dot/demonstration mode so the user knows they can drag the axes. */
1065
1145
  _setCursor(style) {
1066
- this._el.style.cursor = this._hideCursor ? "none" : style;
1146
+ const isAxisCursor = style === "ns-resize" || style === "ew-resize";
1147
+ this._el.style.cursor = this._hideCursor && !isAxisCursor ? "none" : style;
1067
1148
  }
1068
1149
  /** Set when a drawing is committed mid-mousedown; prevents the paired mouseup from auto-selecting it. */
1069
1150
  _suppressNextClick = false;
@@ -1104,6 +1185,13 @@ var InteractionManager = class {
1104
1185
  setBars(bars) {
1105
1186
  this._bars = bars;
1106
1187
  }
1188
+ /**
1189
+ * Provide overlay indicator polylines for proximity hit-testing.
1190
+ * Each line is an indicator id + array of { time, value } points.
1191
+ */
1192
+ setIndicatorLines(lines) {
1193
+ this._indicatorLines = lines;
1194
+ }
1107
1195
  // ─── Selection ────────────────────────────────────────────────────────────
1108
1196
  /** Id of the currently selected drawing, or `null`. */
1109
1197
  get selectedDrawingId() {
@@ -1260,10 +1348,16 @@ var InteractionManager = class {
1260
1348
  if (!this._isDragging) {
1261
1349
  if (x >= this._transform.plotWidth && y < this._transform.plotHeight) {
1262
1350
  this._setCursor("ns-resize");
1351
+ } else if (y >= this._transform.plotHeight) {
1352
+ this._setCursor("ew-resize");
1263
1353
  } else if (hit.kind === "drawingHandle") {
1264
1354
  this._setCursor("grab");
1265
1355
  } else if (hit.kind === "drawing") {
1266
1356
  this._setCursor("grab");
1357
+ } else if (hit.kind === "indicator") {
1358
+ this._setCursor("pointer");
1359
+ } else if (hit.kind === "bar") {
1360
+ this._setCursor("pointer");
1267
1361
  } else {
1268
1362
  this._setCursor("default");
1269
1363
  }
@@ -1401,6 +1495,9 @@ var InteractionManager = class {
1401
1495
  if (hit.kind === "drawing") {
1402
1496
  this._selectedDrawingId = hit.id;
1403
1497
  this._handlers.onSelect?.(x, y, hit);
1498
+ } else if (hit.kind === "indicator") {
1499
+ this._selectedDrawingId = null;
1500
+ this._handlers.onSelect?.(x, y, hit);
1404
1501
  } else if (hit.kind === "bar") {
1405
1502
  this._selectedDrawingId = null;
1406
1503
  this._handlers.onSelect?.(x, y, hit);
@@ -1431,6 +1528,10 @@ var InteractionManager = class {
1431
1528
  if (drawingHit !== null) {
1432
1529
  return { kind: "drawing", id: drawingHit.id, type: drawingHit.type };
1433
1530
  }
1531
+ const indicatorId = this._hitTestIndicators(x, y);
1532
+ if (indicatorId !== null) {
1533
+ return { kind: "indicator", indicatorId };
1534
+ }
1434
1535
  const barHit = this._hitTestBar(x);
1435
1536
  if (barHit !== null) {
1436
1537
  return { kind: "bar", index: barHit.index, bar: barHit.bar };
@@ -1447,6 +1548,34 @@ var InteractionManager = class {
1447
1548
  }
1448
1549
  return -1;
1449
1550
  }
1551
+ _hitTestIndicators(x, y) {
1552
+ const t = this._transform;
1553
+ const { from, to } = t.timeRange;
1554
+ let bestId = null;
1555
+ let bestDist = DRAWING_HIT_PX + 1;
1556
+ for (const line of this._indicatorLines) {
1557
+ const pts = line.points;
1558
+ for (let i = 0; i < pts.length - 1; i++) {
1559
+ const a = pts[i];
1560
+ const b = pts[i + 1];
1561
+ if (a.time < from && b.time < from) continue;
1562
+ if (a.time > to && b.time > to) continue;
1563
+ const dist = this._distPointToSegment(
1564
+ x,
1565
+ y,
1566
+ t.timeToX(a.time),
1567
+ t.priceToY(a.value),
1568
+ t.timeToX(b.time),
1569
+ t.priceToY(b.value)
1570
+ );
1571
+ if (dist < bestDist) {
1572
+ bestDist = dist;
1573
+ bestId = line.id;
1574
+ }
1575
+ }
1576
+ }
1577
+ return bestId;
1578
+ }
1450
1579
  _hitTestDrawings(x, y) {
1451
1580
  let bestId = null;
1452
1581
  let bestType = null;
@@ -1683,26 +1812,14 @@ function rayExit(plotW, plotH, px, py, dx, dy) {
1683
1812
  else if (dy < 0) tMin = Math.min(tMin, -py / dy);
1684
1813
  return tMin === Infinity ? { x: px, y: py } : { x: px + dx * tMin, y: py + dy * tMin };
1685
1814
  }
1686
- var TF_SECS = {
1687
- "1s": 1,
1688
- "5s": 5,
1689
- "10s": 10,
1690
- "30s": 30,
1691
- "1m": 60,
1692
- "3m": 180,
1693
- "5m": 300,
1694
- "15m": 900,
1695
- "30m": 1800,
1696
- "1h": 3600,
1697
- "2h": 7200,
1698
- "4h": 14400,
1699
- "6h": 21600,
1700
- "12h": 43200,
1701
- "1d": 86400,
1702
- "3d": 259200,
1703
- "1w": 604800,
1704
- "1M": 2592e3
1705
- };
1815
+ var TF_SECS = new Proxy({}, {
1816
+ get(_t, tf) {
1817
+ return parseTfSeconds(tf) ?? 3600;
1818
+ },
1819
+ has(_t, _tf) {
1820
+ return true;
1821
+ }
1822
+ });
1706
1823
  function formatDuration(seconds) {
1707
1824
  if (seconds < 60) return `${seconds}s`;
1708
1825
  if (seconds < 3600) {
@@ -2539,30 +2656,15 @@ var LIGHT_COLORS = {
2539
2656
  border: "#cccccc",
2540
2657
  crosshair: "#000000"
2541
2658
  };
2659
+ var _clientToServerOffset = 0;
2660
+ var _providerOffset = 0;
2661
+ function exchangeNow() {
2662
+ return Date.now() + _clientToServerOffset + _providerOffset;
2663
+ }
2542
2664
 
2543
2665
  // src/core/Chart.ts
2544
- var _TF_SECONDS = {
2545
- "1s": 1,
2546
- "5s": 5,
2547
- "10s": 10,
2548
- "30s": 30,
2549
- "1m": 60,
2550
- "3m": 180,
2551
- "5m": 300,
2552
- "15m": 900,
2553
- "30m": 1800,
2554
- "1h": 3600,
2555
- "2h": 7200,
2556
- "4h": 14400,
2557
- "6h": 21600,
2558
- "12h": 43200,
2559
- "1d": 86400,
2560
- "3d": 259200,
2561
- "1w": 604800,
2562
- "1M": 2592e3
2563
- };
2564
2666
  function _tfToSeconds(tf) {
2565
- return _TF_SECONDS[tf] ?? 3600;
2667
+ return parseTfSeconds(tf) ?? 3600;
2566
2668
  }
2567
2669
  function _formatLastPrice(price) {
2568
2670
  const decimals = price < 1 ? 6 : price < 1e3 ? 4 : 2;
@@ -2575,6 +2677,20 @@ function _formatCountdown(secs) {
2575
2677
  if (h > 0) return `${h}:${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`;
2576
2678
  return `${m}:${String(s).padStart(2, "0")}`;
2577
2679
  }
2680
+ function _orderRoleLabel(role) {
2681
+ switch (role) {
2682
+ case "stop_loss":
2683
+ return "SL";
2684
+ case "take_profit":
2685
+ return "TP";
2686
+ case "limit":
2687
+ return "LMT";
2688
+ case "stop":
2689
+ return "STP";
2690
+ default:
2691
+ return "";
2692
+ }
2693
+ }
2578
2694
  var Chart = class {
2579
2695
  _container;
2580
2696
  _options;
@@ -2583,6 +2699,9 @@ var Chart = class {
2583
2699
  _gridLayer;
2584
2700
  _seriesLayer;
2585
2701
  _overlayLayer;
2702
+ /** Dedicated layer for the crosshair — rendered synchronously on every
2703
+ * mousemove so there is zero rAF-frame lag between the pointer and the cross. */
2704
+ _crosshairLayer;
2586
2705
  _timeScale;
2587
2706
  _priceScale;
2588
2707
  _transform;
@@ -2609,6 +2728,9 @@ var Chart = class {
2609
2728
  _drawingCursorPt = null;
2610
2729
  /** When false the crosshair is suppressed (e.g. Arrow/cursor mode). */
2611
2730
  _crosshairEnabled = true;
2731
+ // ── Trading overlay ───────────────────────────────────────────────────────────
2732
+ /** Current set of order lines supplied by TChart for canvas rendering. */
2733
+ _tradingOrders = [];
2612
2734
  /** Called whenever a drawing is committed (created or updated). */
2613
2735
  onDrawingCommitted;
2614
2736
  /** Called whenever a drawing is deleted. */
@@ -2632,6 +2754,7 @@ var Chart = class {
2632
2754
  this._gridLayer = new CanvasLayer(container, { width, height, dpr, zIndex: 1 });
2633
2755
  this._seriesLayer = new CanvasLayer(container, { width, height, dpr, zIndex: 2 });
2634
2756
  this._overlayLayer = new CanvasLayer(container, { width, height, dpr, zIndex: 3 });
2757
+ this._crosshairLayer = new CanvasLayer(container, { width, height, dpr, zIndex: 4 });
2635
2758
  this._timeScale = new TimeScale(this._gridLayer, this._colors, this._options.timeScale);
2636
2759
  this._priceScale = new PriceScale(this._gridLayer, this._colors, this._options.priceScale);
2637
2760
  this._transform = new CoordTransform(
@@ -2663,19 +2786,20 @@ var Chart = class {
2663
2786
  },
2664
2787
  onCrosshairMove: (x, y) => {
2665
2788
  if (!this._crosshairEnabled) return;
2666
- const { x: snappedX, time: snappedTime } = this._snapXToBar(x);
2789
+ const { x: snappedX, time: snappedTime } = this.snapXToBar(x);
2667
2790
  this._crosshair.update(snappedX, y, snappedTime);
2668
- this._dirty = true;
2791
+ this._renderCrosshairNow();
2669
2792
  },
2670
2793
  onCrosshairHide: () => {
2671
2794
  this._crosshair.hide();
2672
- this._dirty = true;
2795
+ this._renderCrosshairNow();
2673
2796
  },
2674
2797
  onKeyAction: (action) => {
2675
2798
  this._handleKeyAction(action);
2676
2799
  },
2677
2800
  onFitContent: () => {
2678
- this.scrollToEnd();
2801
+ this.fitDefaultView();
2802
+ this._dirty = true;
2679
2803
  },
2680
2804
  onPriceScaleZoom: (factor, originY) => {
2681
2805
  this._priceScaleManual = true;
@@ -2814,9 +2938,11 @@ var Chart = class {
2814
2938
  this._gridLayer.resize(width, height, dpr);
2815
2939
  this._seriesLayer.resize(width, height, dpr);
2816
2940
  this._overlayLayer.resize(width, height, dpr);
2941
+ this._crosshairLayer.resize(width, height, dpr);
2817
2942
  this._transform.update(width, height);
2818
2943
  this._dirty = true;
2819
2944
  this._render();
2945
+ this._renderCrosshairNow();
2820
2946
  }
2821
2947
  destroy() {
2822
2948
  if (this._animationFrame !== null) cancelAnimationFrame(this._animationFrame);
@@ -2825,6 +2951,7 @@ var Chart = class {
2825
2951
  this._gridLayer.destroy();
2826
2952
  this._seriesLayer.destroy();
2827
2953
  this._overlayLayer.destroy();
2954
+ this._crosshairLayer.destroy();
2828
2955
  this._crosshair.destroy();
2829
2956
  this._plugins.forEach((p) => p.onDetach());
2830
2957
  this._series = [];
@@ -2851,6 +2978,18 @@ var Chart = class {
2851
2978
  this._renderOverlay();
2852
2979
  this._plugins.forEach((p) => p.onRender());
2853
2980
  }
2981
+ /**
2982
+ * Clears and redraws only the crosshair canvas layer.
2983
+ * Called synchronously inside the mousemove handler — bypassing the
2984
+ * dirty-flag / rAF queue — so the crosshair has zero frame lag relative
2985
+ * to the actual pointer position.
2986
+ */
2987
+ _renderCrosshairNow() {
2988
+ const ctx = this._crosshairLayer.context;
2989
+ const { width, height } = this._crosshairLayer;
2990
+ ctx.clearRect(0, 0, width, height);
2991
+ this._crosshair.render(ctx, width, height);
2992
+ }
2854
2993
  _renderGrid() {
2855
2994
  const ctx = this._gridLayer.context;
2856
2995
  const { width, height } = this._gridLayer;
@@ -2884,16 +3023,17 @@ var Chart = class {
2884
3023
  const ctx = this._overlayLayer.context;
2885
3024
  const { width, height } = this._overlayLayer;
2886
3025
  ctx.clearRect(0, 0, width, height);
3026
+ this._renderTradingOrders(ctx);
2887
3027
  const selectedId = this._interaction.selectedDrawingId;
2888
3028
  const candles = this._series[0]?.data() ?? [];
2889
3029
  this._drawingMgr.render(ctx, this._transform, selectedId, this._interval, candles);
2890
3030
  this._interaction.setDrawings(this._drawingMgr.all());
3031
+ this._interaction.setBars(candles);
2891
3032
  if (this._drawingTool !== null && this._drawingCursorPt !== null) {
2892
3033
  const draft = this._buildDraftPreview();
2893
3034
  if (draft) this._drawingMgr.renderPreview(ctx, this._transform, draft, this._interval, candles);
2894
3035
  }
2895
3036
  this._renderLastPriceLine(ctx);
2896
- this._crosshair.render(ctx, width, height);
2897
3037
  }
2898
3038
  // ─── Private helpers ─────────────────────────────────────────────────────────
2899
3039
  /**
@@ -2901,7 +3041,8 @@ var Chart = class {
2901
3041
  * If no bars are loaded, or the nearest bar is more than SNAP_PX pixels away,
2902
3042
  * returns the original x so the crosshair always renders at the cursor position.
2903
3043
  */
2904
- _snapXToBar(x) {
3044
+ /** Snap a pixel x-coordinate to the nearest bar center. Public so it can be used by PointerOverlay. */
3045
+ snapXToBar(x) {
2905
3046
  const SNAP_PX = 20;
2906
3047
  let times = null;
2907
3048
  for (const s of this._series) {
@@ -2961,7 +3102,8 @@ var Chart = class {
2961
3102
  this._transform.zoomTime(zf, cx);
2962
3103
  break;
2963
3104
  case "scrollToEnd":
2964
- this._scrollToLatestBar();
3105
+ this.fitDefaultView();
3106
+ this._dirty = true;
2965
3107
  break;
2966
3108
  case "scrollToStart":
2967
3109
  break;
@@ -3024,12 +3166,11 @@ var Chart = class {
3024
3166
  ctx.stroke();
3025
3167
  ctx.setLineDash([]);
3026
3168
  const tfSecs = _tfToSeconds(this._interval);
3027
- const now = Math.floor(Date.now() / 1e3);
3028
- const remaining = lastBar.time + tfSecs - now;
3029
- const showCd = remaining > 0 && remaining <= tfSecs;
3169
+ const now = Math.floor(exchangeNow() / 1e3);
3170
+ const remaining = tfSecs - now % tfSecs;
3030
3171
  const priceLabel = _formatLastPrice(price);
3031
3172
  const lineH = 15;
3032
- const boxH = showCd ? lineH * 2 + 1 : lineH;
3173
+ const boxH = lineH * 2 + 1 ;
3033
3174
  const boxY = y - lineH / 2;
3034
3175
  ctx.fillStyle = lineColor;
3035
3176
  ctx.fillRect(plotWidth, boxY, psWidth, boxH);
@@ -3038,13 +3179,57 @@ var Chart = class {
3038
3179
  ctx.textBaseline = "middle";
3039
3180
  ctx.font = "bold 11px system-ui, sans-serif";
3040
3181
  ctx.fillText(priceLabel, plotWidth + 5, boxY + lineH / 2);
3041
- if (showCd) {
3182
+ {
3042
3183
  ctx.font = "10px system-ui, sans-serif";
3043
3184
  ctx.fillStyle = "rgba(255,255,255,0.85)";
3044
3185
  ctx.fillText(_formatCountdown(remaining), plotWidth + 5, boxY + lineH + 1 + lineH / 2);
3045
3186
  }
3046
3187
  ctx.restore();
3047
3188
  }
3189
+ /** Provides the overlay renderer with the latest order list from TChart. */
3190
+ setTradingOrders(orders) {
3191
+ this._tradingOrders = orders;
3192
+ }
3193
+ /** Renders active order lines above the series but below chart drawings. */
3194
+ _renderTradingOrders(ctx) {
3195
+ if (this._tradingOrders.length === 0) return;
3196
+ const plotWidth = this._transform.plotWidth;
3197
+ const plotHeight = this._transform.plotHeight;
3198
+ const psWidth = this._transform.priceScaleWidth;
3199
+ ctx.save();
3200
+ ctx.font = "bold 10px system-ui, sans-serif";
3201
+ ctx.textBaseline = "middle";
3202
+ ctx.textAlign = "left";
3203
+ for (const order of this._tradingOrders) {
3204
+ if (order.status === "cancelled" || order.status === "rejected" || order.status === "expired" || order.status === "filled") continue;
3205
+ const y = this._transform.priceToY(order.price);
3206
+ if (y < 0 || y > plotHeight) continue;
3207
+ const isBuy = order.side === "buy";
3208
+ const isPending = order.status === "pending";
3209
+ const color = isBuy ? "#26a641" : "#f85149";
3210
+ ctx.beginPath();
3211
+ ctx.strokeStyle = color;
3212
+ ctx.lineWidth = 1;
3213
+ ctx.globalAlpha = isPending ? 0.6 : 1;
3214
+ ctx.setLineDash(isPending ? [4, 4] : []);
3215
+ ctx.moveTo(0, y);
3216
+ ctx.lineTo(plotWidth, y);
3217
+ ctx.stroke();
3218
+ ctx.setLineDash([]);
3219
+ ctx.globalAlpha = 1;
3220
+ const roleLabel = _orderRoleLabel(order.role);
3221
+ const label = roleLabel ? `${roleLabel} ${order.qty}` : `${order.side.toUpperCase()} ${order.qty}`;
3222
+ const lineH = 15;
3223
+ const boxY = y - lineH / 2;
3224
+ ctx.globalAlpha = isPending ? 0.6 : 1;
3225
+ ctx.fillStyle = color;
3226
+ ctx.fillRect(plotWidth, boxY, psWidth, lineH);
3227
+ ctx.globalAlpha = 1;
3228
+ ctx.fillStyle = "#ffffff";
3229
+ ctx.fillText(label, plotWidth + 4, y);
3230
+ }
3231
+ ctx.restore();
3232
+ }
3048
3233
  /** Pans the viewport so the latest bar sits near the right edge. */
3049
3234
  _scrollToLatestBar() {
3050
3235
  let latestTime = -Infinity;
@@ -3075,6 +3260,7 @@ var Chart = class {
3075
3260
  this._priceScale.setVisibleRange(v.priceRange);
3076
3261
  this._priceScaleManual = true;
3077
3262
  this._dirty = true;
3263
+ this.onViewportChanged?.();
3078
3264
  }
3079
3265
  /**
3080
3266
  * Clears the manual-price-scale flag so the price axis returns to auto-fit mode.
@@ -3106,6 +3292,7 @@ var Chart = class {
3106
3292
  });
3107
3293
  this._priceScaleManual = false;
3108
3294
  this._dirty = true;
3295
+ this.onViewportChanged?.();
3109
3296
  }
3110
3297
  // ─── Drawing tool API ──────────────────────────────────────────────────────
3111
3298
  /** Returns the DrawingManager owned by this chart. */
@@ -3159,6 +3346,10 @@ var Chart = class {
3159
3346
  setNativeCursorHidden(hidden) {
3160
3347
  this._interaction.setHideCursor(hidden);
3161
3348
  }
3349
+ /** Feed overlay indicator polylines for hit-testing. */
3350
+ setIndicatorLines(lines) {
3351
+ this._interaction.setIndicatorLines(lines);
3352
+ }
3162
3353
  /** Deletes the currently selected drawing (if any). */
3163
3354
  deleteSelectedDrawing() {
3164
3355
  const id = this._interaction.selectedDrawingId;
@@ -3524,58 +3715,6 @@ var PaneManager = class {
3524
3715
  }
3525
3716
  };
3526
3717
 
3527
- // ../shared/src/time/timeframeRegistry.ts
3528
- var TIMEFRAMES = {
3529
- // ── Sub-hour ──────────────────────────────────────────────────────────────
3530
- "1m": { key: "1m", kind: "fixed", ms: 6e4 },
3531
- "2m": { key: "2m", kind: "fixed", ms: 12e4 },
3532
- "3m": { key: "3m", kind: "fixed", ms: 18e4 },
3533
- "4m": { key: "4m", kind: "fixed", ms: 24e4 },
3534
- "5m": { key: "5m", kind: "fixed", ms: 3e5 },
3535
- "10m": { key: "10m", kind: "fixed", ms: 6e5 },
3536
- "15m": { key: "15m", kind: "fixed", ms: 9e5 },
3537
- "20m": { key: "20m", kind: "fixed", ms: 12e5 },
3538
- "30m": { key: "30m", kind: "fixed", ms: 18e5 },
3539
- "45m": { key: "45m", kind: "fixed", ms: 27e5 },
3540
- // ── Hourly ────────────────────────────────────────────────────────────────
3541
- "1h": { key: "1h", kind: "fixed", ms: 36e5 },
3542
- "2h": { key: "2h", kind: "fixed", ms: 72e5 },
3543
- "4h": { key: "4h", kind: "fixed", ms: 144e5 },
3544
- "6h": { key: "6h", kind: "fixed", ms: 216e5 },
3545
- "12h": { key: "12h", kind: "fixed", ms: 432e5 },
3546
- // ── Daily ─────────────────────────────────────────────────────────────────
3547
- "1d": { key: "1d", kind: "fixed", ms: 864e5 },
3548
- "3d": { key: "3d", kind: "fixed", ms: 2592e5 },
3549
- // ── Calendar — MUST NOT have a fixed ms value ──────────────────────────
3550
- "1w": { key: "1w", kind: "calendar_week" },
3551
- "1M": { key: "1M", kind: "calendar_month" }
3552
- };
3553
-
3554
- // ../shared/src/time/timeUtils.ts
3555
- var MONDAY_EPOCH_OFFSET_MS = 3456e5;
3556
- var WEEK_MS = 6048e5;
3557
- function timeframeToMs(tf) {
3558
- const def = TIMEFRAMES[tf];
3559
- if (!def || def.kind !== "fixed") return null;
3560
- return def.ms;
3561
- }
3562
- function getBucketStart(timeMs, timeframe) {
3563
- const def = TIMEFRAMES[timeframe];
3564
- if (!def) {
3565
- return Math.floor(timeMs / 6e4) * 6e4;
3566
- }
3567
- if (def.kind === "calendar_week") return getWeekBucketStart(timeMs);
3568
- if (def.kind === "calendar_month") return getMonthBucketStart(timeMs);
3569
- return Math.floor(timeMs / def.ms) * def.ms;
3570
- }
3571
- function getWeekBucketStart(timeMs) {
3572
- return Math.floor((timeMs - MONDAY_EPOCH_OFFSET_MS) / WEEK_MS) * WEEK_MS + MONDAY_EPOCH_OFFSET_MS;
3573
- }
3574
- function getMonthBucketStart(timeMs) {
3575
- const d = new Date(timeMs);
3576
- return Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), 1);
3577
- }
3578
-
3579
3718
  // src/engine/timeframeUtils.ts
3580
3719
  function timeframeToMs2(timeframe) {
3581
3720
  return timeframeToMs(timeframe) ?? 6e4;
@@ -3926,51 +4065,12 @@ var CandleEngine = class {
3926
4065
  function toOHLCV(bar) {
3927
4066
  return { time: bar.time, open: bar.open, high: bar.high, low: bar.low, close: bar.close, volume: bar.volume };
3928
4067
  }
3929
- var HISTORY_WINDOWS = {
3930
- // Sub-hour: small display window; DB fills 12 months in background for scrollback.
3931
- "1m": 3 * 24 * 3600,
3932
- // 3 days → ~4 320 bars
3933
- "3m": 7 * 24 * 3600,
3934
- // 7 days → ~3 360 bars
3935
- "5m": 10 * 24 * 3600,
3936
- // 10 days → ~2 880 bars
3937
- "15m": 30 * 24 * 3600,
3938
- // 30 days → ~2 880 bars
3939
- "30m": 60 * 24 * 3600,
3940
- // 60 days → ~2 880 bars
3941
- "1h": 90 * 24 * 3600,
3942
- // 90 days → ~2 160 bars
3943
- // 4h and above: 12 months — bar counts are small enough to load in full.
3944
- "2h": 365 * 86400,
3945
- // 12 months → ~4 380 bars
3946
- "4h": 365 * 86400,
3947
- // 12 months → ~2 190 bars
3948
- "6h": 365 * 86400,
3949
- // 12 months → ~1 460 bars
3950
- "12h": 365 * 86400,
3951
- // 12 months → ~730 bars
3952
- "1d": 365 * 86400,
3953
- // 12 months → ~365 bars
3954
- "3d": 365 * 86400,
3955
- // 12 months → ~122 bars
3956
- "1w": 365 * 86400,
3957
- // 12 months → ~52 bars
3958
- "1M": 365 * 86400
3959
- // 12 months → ~12 bars
3960
- };
3961
- var DEFAULT_HISTORY_WINDOW = 90 * 24 * 3600;
3962
- var PAGE_SIZES = {
3963
- "1m": 500 * 60,
3964
- "5m": 500 * 300,
3965
- "15m": 500 * 900,
3966
- "30m": 500 * 1800,
3967
- "1h": 500 * 3600,
3968
- "4h": 500 * 4 * 3600,
3969
- "1d": 500 * 86400,
3970
- "1w": 500 * 7 * 86400,
3971
- "1M": 500 * 30 * 86400
3972
- };
3973
- var DEFAULT_PAGE_SIZE = 500 * 3600;
4068
+ function _historyWindow(tf) {
4069
+ return 500 * (parseTfSeconds(tf) ?? 3600);
4070
+ }
4071
+ function _pageSize(tf) {
4072
+ return 500 * (parseTfSeconds(tf) ?? 3600);
4073
+ }
3974
4074
  var _uidCounter = 0;
3975
4075
  var DatafeedConnector = class {
3976
4076
  _datafeed;
@@ -4000,12 +4100,13 @@ var DatafeedConnector = class {
4000
4100
  */
4001
4101
  connect(symbol, timeframe) {
4002
4102
  if (this._destroyed) return;
4103
+ if (!symbol) return;
4003
4104
  this._cancelActive();
4004
4105
  const uid = `forgecharts-${symbol}-${timeframe}-${++_uidCounter}`;
4005
4106
  this._activeUID = uid;
4006
4107
  this._cb.onLoading(symbol, timeframe);
4007
4108
  const to = Math.floor(Date.now() / 1e3);
4008
- const from = to - (HISTORY_WINDOWS[timeframe] ?? DEFAULT_HISTORY_WINDOW);
4109
+ const from = to - _historyWindow(timeframe);
4009
4110
  const engine = new CandleEngine({
4010
4111
  // Every live tick mutates the engine's bar array and the chart series.
4011
4112
  onBarUpdated: (bar) => {
@@ -4092,7 +4193,7 @@ var DatafeedConnector = class {
4092
4193
  if (!oldest) return;
4093
4194
  const uid = this._activeUID;
4094
4195
  const to = Math.floor(oldest.timeMs / 1e3);
4095
- const page = PAGE_SIZES[timeframe] ?? DEFAULT_PAGE_SIZE;
4196
+ const page = _pageSize(timeframe);
4096
4197
  const from = to - page;
4097
4198
  this._isLoadingMore = true;
4098
4199
  this._datafeed.getHistoricalBars(symbol, timeframe, from, to - 1).then(({ bars }) => {
@@ -4337,6 +4438,9 @@ var LicenseManager = class _LicenseManager {
4337
4438
  }
4338
4439
  };
4339
4440
 
4441
+ // src/licensing/licenseTypes.ts
4442
+ var TRADING_TIERS = /* @__PURE__ */ new Set(["trading", "dom"]);
4443
+
4340
4444
  // src/licensing/ChartRuntimeResolver.ts
4341
4445
  var ChartRuntimeResolver = class _ChartRuntimeResolver {
4342
4446
  static instance = null;
@@ -4388,33 +4492,45 @@ var ChartRuntimeResolver = class _ChartRuntimeResolver {
4388
4492
  /**
4389
4493
  * Built-in order entry UI hooks.
4390
4494
  * Requires managed mode AND the `orderEntry` feature flag (or no flag restriction).
4495
+ * When a tier is present on the license, the tier must be 'trading' or 'dom'.
4391
4496
  */
4392
4497
  canUseOrderEntry() {
4393
4498
  if (this.isUnmanagedMode()) return false;
4499
+ const tier = this.lm.getLicense()?.tier;
4500
+ if (tier && !TRADING_TIERS.has(tier)) return false;
4394
4501
  return this.#featureOrDefault("orderEntry", true);
4395
4502
  }
4396
4503
  /**
4397
4504
  * Managed trading service hooks (broker execution, position management).
4398
4505
  * Requires managed mode AND the `managedTrading` feature flag (or no flag restriction).
4506
+ * When a tier is present on the license, the tier must be 'trading' or 'dom'.
4399
4507
  */
4400
4508
  canUseManagedTrading() {
4401
4509
  if (this.isUnmanagedMode()) return false;
4510
+ const tier = this.lm.getLicense()?.tier;
4511
+ if (tier && !TRADING_TIERS.has(tier)) return false;
4402
4512
  return this.#featureOrDefault("managedTrading", true);
4403
4513
  }
4404
4514
  /**
4405
4515
  * Drag-to-price order placement.
4406
4516
  * Requires managed mode AND the `draggableOrders` feature flag (or no flag restriction).
4517
+ * When a tier is present on the license, the tier must be 'trading' or 'dom'.
4407
4518
  */
4408
4519
  canUseDraggableOrders() {
4409
4520
  if (this.isUnmanagedMode()) return false;
4521
+ const tier = this.lm.getLicense()?.tier;
4522
+ if (tier && !TRADING_TIERS.has(tier)) return false;
4410
4523
  return this.#featureOrDefault("draggableOrders", true);
4411
4524
  }
4412
4525
  /**
4413
4526
  * Bracket / OCO order support.
4414
4527
  * Requires managed mode AND the `bracketOrders` feature flag (or no flag restriction).
4528
+ * When a tier is present on the license, the tier must be 'trading' or 'dom'.
4415
4529
  */
4416
4530
  canUseBracketOrders() {
4417
4531
  if (this.isUnmanagedMode()) return false;
4532
+ const tier = this.lm.getLicense()?.tier;
4533
+ if (tier && !TRADING_TIERS.has(tier)) return false;
4418
4534
  return this.#featureOrDefault("bracketOrders", true);
4419
4535
  }
4420
4536
  // ── UI render capability checks ──────────────────────────────────────────────
@@ -4872,26 +4988,6 @@ var ManagedTradingController = class {
4872
4988
  };
4873
4989
 
4874
4990
  // src/api/TChart.ts
4875
- var _TF_SECS = {
4876
- "1s": 1,
4877
- "5s": 5,
4878
- "10s": 10,
4879
- "30s": 30,
4880
- "1m": 60,
4881
- "3m": 180,
4882
- "5m": 300,
4883
- "15m": 900,
4884
- "30m": 1800,
4885
- "1h": 3600,
4886
- "2h": 7200,
4887
- "4h": 14400,
4888
- "6h": 21600,
4889
- "12h": 43200,
4890
- "1d": 86400,
4891
- "3d": 259200,
4892
- "1w": 604800,
4893
- "1M": 2592e3
4894
- };
4895
4991
  var TChart = class {
4896
4992
  _core;
4897
4993
  _bus;
@@ -4969,9 +5065,14 @@ var TChart = class {
4969
5065
  };
4970
5066
  this._series = this._core.addSeries({ type: "candlestick" });
4971
5067
  this._core.setInterval(this._interval);
5068
+ this._core.setTradingOrders(this._overlayStore.getOrders());
5069
+ this._overlayStore.onChange(() => {
5070
+ this._core.setTradingOrders(this._overlayStore.getOrders());
5071
+ this._core.markDirty();
5072
+ });
4972
5073
  if (config.datafeed !== void 0) {
4973
5074
  this._connector = this._buildConnector(config.datafeed);
4974
- this._connector.connect(this._symbol, this._interval);
5075
+ if (this._symbol) this._connector.connect(this._symbol, this._interval);
4975
5076
  }
4976
5077
  this._core.onViewportChanged = () => {
4977
5078
  const connector = this._connector;
@@ -4979,9 +5080,9 @@ var TChart = class {
4979
5080
  const bars = this._series.data();
4980
5081
  if (bars.length === 0) return;
4981
5082
  const oldestBarTime = bars[0].time;
4982
- const { from } = this._core.getViewport().timeRange;
4983
- const tfSec = _TF_SECS[this._interval] ?? 3600;
4984
- if (from < oldestBarTime + tfSec * 30) {
5083
+ const { from, to } = this._core.getViewport().timeRange;
5084
+ const visibleSpan = to - from;
5085
+ if (from <= oldestBarTime + visibleSpan * 0.75) {
4985
5086
  connector.loadMoreHistory(this._symbol, this._interval);
4986
5087
  }
4987
5088
  };
@@ -5063,7 +5164,7 @@ var TChart = class {
5063
5164
  if (symbol === this._symbol) return;
5064
5165
  this._symbol = symbol;
5065
5166
  this._bus.emit("symbolChanged", { symbol });
5066
- this._connector?.reconnect(symbol, this._interval);
5167
+ if (symbol) this._connector?.reconnect(symbol, this._interval);
5067
5168
  }
5068
5169
  /**
5069
5170
  * Changes the active interval.
@@ -5238,6 +5339,16 @@ var TChart = class {
5238
5339
  this._assertAlive();
5239
5340
  this._core.setNativeCursorHidden(hidden);
5240
5341
  }
5342
+ /** Feed overlay indicator polylines for hit-testing. */
5343
+ setIndicatorLines(lines) {
5344
+ this._assertAlive();
5345
+ this._core.setIndicatorLines(lines);
5346
+ }
5347
+ /** Snap a pixel x-coordinate to the nearest bar center (for pointer overlay snapping). */
5348
+ snapXToBar(x) {
5349
+ this._assertAlive();
5350
+ return this._core.snapXToBar(x);
5351
+ }
5241
5352
  /** Deletes the currently selected drawing (if any). */
5242
5353
  deleteSelectedDrawing() {
5243
5354
  this._assertAlive();
@@ -5710,7 +5821,7 @@ function computeWMAFromSeries(input, period) {
5710
5821
  return result;
5711
5822
  }
5712
5823
 
5713
- // src/tscript/series.ts
5824
+ // src/forgescript/series.ts
5714
5825
  var Series2 = class {
5715
5826
  _buf;
5716
5827
  _cap;
@@ -5776,6 +5887,51 @@ var Lexer = class {
5776
5887
  this._src = src;
5777
5888
  }
5778
5889
  tokenize() {
5890
+ const raw = this._rawTokenize();
5891
+ return this._injectIndents(raw);
5892
+ }
5893
+ // ─── INDENT/DEDENT injection ──────────────────────────────────────────────
5894
+ /** After each NEWLINE, inject INDENT/DEDENT tokens based on the indentation
5895
+ * of the following non-blank line. Enables indented if/else blocks. */
5896
+ _injectIndents(raw) {
5897
+ const out = [];
5898
+ const indentStack = [0];
5899
+ let i = 0;
5900
+ while (i < raw.length) {
5901
+ const tok = raw[i];
5902
+ if (tok.kind !== "NEWLINE") {
5903
+ out.push(tok);
5904
+ i++;
5905
+ continue;
5906
+ }
5907
+ out.push(tok);
5908
+ i++;
5909
+ while (i < raw.length && raw[i].kind === "NEWLINE") {
5910
+ out.push(raw[i]);
5911
+ i++;
5912
+ }
5913
+ if (i >= raw.length || raw[i].kind === "EOF") break;
5914
+ const nextTok = raw[i];
5915
+ const spaces = nextTok.col - 1;
5916
+ const currentIndent = indentStack[indentStack.length - 1];
5917
+ if (spaces > currentIndent) {
5918
+ indentStack.push(spaces);
5919
+ out.push({ kind: "INDENT", value: "", line: nextTok.line, col: 1 });
5920
+ } else {
5921
+ while (spaces < indentStack[indentStack.length - 1]) {
5922
+ indentStack.pop();
5923
+ out.push({ kind: "DEDENT", value: "", line: nextTok.line, col: 1 });
5924
+ }
5925
+ }
5926
+ }
5927
+ while (indentStack.length > 1) {
5928
+ indentStack.pop();
5929
+ out.push({ kind: "DEDENT", value: "", line: 0, col: 0 });
5930
+ }
5931
+ out.push({ kind: "EOF", value: "", line: this._line, col: this._col });
5932
+ return out;
5933
+ }
5934
+ _rawTokenize() {
5779
5935
  const tokens = [];
5780
5936
  while (this._pos < this._src.length) {
5781
5937
  this._skipWhitespaceAndComments();
@@ -5800,7 +5956,23 @@ var Lexer = class {
5800
5956
  tokens.push(this._readIdent());
5801
5957
  continue;
5802
5958
  }
5959
+ if (ch === "#") {
5960
+ tokens.push(this._readColor());
5961
+ continue;
5962
+ }
5803
5963
  const two = this._src.slice(this._pos, this._pos + 2);
5964
+ if (two === ":=") {
5965
+ tokens.push(this._make("COLONEQ", two));
5966
+ this._pos += 2;
5967
+ this._col += 2;
5968
+ continue;
5969
+ }
5970
+ if (two === "=>") {
5971
+ tokens.push(this._make("ARROW", two));
5972
+ this._pos += 2;
5973
+ this._col += 2;
5974
+ continue;
5975
+ }
5804
5976
  if (two === "<=") {
5805
5977
  tokens.push(this._make("LTE", two));
5806
5978
  this._pos += 2;
@@ -5835,6 +6007,7 @@ var Lexer = class {
5835
6007
  ">": "GT",
5836
6008
  "?": "QMARK",
5837
6009
  ":": "COLON",
6010
+ ".": "DOT",
5838
6011
  "(": "LPAREN",
5839
6012
  ")": "RPAREN",
5840
6013
  "[": "LBRACKET",
@@ -5848,7 +6021,7 @@ var Lexer = class {
5848
6021
  this._col++;
5849
6022
  continue;
5850
6023
  }
5851
- throw new SyntaxError(`[TScript] Unexpected character '${ch}' at ${this._line}:${this._col}`);
6024
+ throw new SyntaxError(`[ForgeScript] Unexpected character '${ch}' at ${this._line}:${this._col}`);
5852
6025
  }
5853
6026
  tokens.push(this._make("EOF", ""));
5854
6027
  return tokens;
@@ -5931,14 +6104,31 @@ var Lexer = class {
5931
6104
  }
5932
6105
  return { kind: "IDENT", value: name, line: this._line, col: startCol };
5933
6106
  }
6107
+ _readColor() {
6108
+ const startCol = this._col;
6109
+ this._pos++;
6110
+ this._col++;
6111
+ let hex = "#";
6112
+ while (this._pos < this._src.length && this._isHex(this._src[this._pos])) {
6113
+ hex += this._src[this._pos];
6114
+ this._pos++;
6115
+ this._col++;
6116
+ }
6117
+ if (hex.length !== 4 && hex.length !== 7 && hex.length !== 9) {
6118
+ throw new SyntaxError(`[ForgeScript] Invalid color literal '${hex}' at ${this._line}:${startCol}`);
6119
+ }
6120
+ return { kind: "COLOR", value: hex, line: this._line, col: startCol };
6121
+ }
6122
+ _isHex(ch) {
6123
+ return ch >= "0" && ch <= "9" || ch >= "a" && ch <= "f" || ch >= "A" && ch <= "F";
6124
+ }
5934
6125
  };
5935
6126
 
5936
- // src/tscript/parser.ts
6127
+ // src/forgescript/parser.ts
5937
6128
  var Parser = class {
5938
6129
  _tokens;
5939
6130
  _pos = 0;
5940
6131
  constructor(src) {
5941
- this._tokens = new Lexer(src).tokenize().filter((t) => t.kind !== "NEWLINE" || this._isStatementBoundary(t));
5942
6132
  this._tokens = this._collapseNewlines(new Lexer(src).tokenize());
5943
6133
  }
5944
6134
  parse() {
@@ -5952,21 +6142,83 @@ var Parser = class {
5952
6142
  }
5953
6143
  // ─── Statements ─────────────────────────────────────────────────────────────
5954
6144
  _stmt() {
6145
+ if (this._checkIdent("if")) {
6146
+ return this._ifStmt();
6147
+ }
6148
+ if (this._checkIdent("while")) {
6149
+ return this._whileStmt();
6150
+ }
6151
+ if (this._checkIdent("for")) {
6152
+ return this._forStmt();
6153
+ }
6154
+ if (this._checkIdent("var") || this._checkIdent("varip")) {
6155
+ return this._varDeclStmt();
6156
+ }
5955
6157
  if (this._checkIdent("indicator")) {
5956
6158
  return this._indicatorDecl();
5957
6159
  }
6160
+ if (this._check("IDENT") && this._peekKind(1) === "LPAREN" && this._isFnDecl()) {
6161
+ return this._fnDeclStmt();
6162
+ }
5958
6163
  if (this._check("IDENT") && this._peekKind(1) === "EQ") {
5959
6164
  return this._assign();
5960
6165
  }
6166
+ if (this._check("IDENT") && this._peekKind(1) === "COLONEQ") {
6167
+ return this._reassign();
6168
+ }
5961
6169
  return this._exprStmt();
5962
6170
  }
5963
6171
  _indicatorDecl() {
5964
6172
  this._consumeIdent("indicator");
5965
6173
  this._consume("LPAREN", "Expected '(' after 'indicator'");
5966
6174
  const title = this._consume("STRING", "Expected string title in indicator()").value;
5967
- this._consume("RPAREN", "Expected ')' after indicator title");
6175
+ const namedArgs = /* @__PURE__ */ new Map();
6176
+ while (this._match("COMMA")) {
6177
+ if (this._check("IDENT") && this._peekKind(1) === "EQ") {
6178
+ const key = this._consume("IDENT", "Expected named arg").value;
6179
+ this._consume("EQ", "Expected '='");
6180
+ namedArgs.set(key, this._expr());
6181
+ } else {
6182
+ this._expr();
6183
+ }
6184
+ }
6185
+ this._consume("RPAREN", "Expected ')' after indicator args");
5968
6186
  this._consumeNewlineOrEOF();
5969
- return { kind: "IndicatorDecl", title };
6187
+ return { kind: "IndicatorDecl", title, namedArgs };
6188
+ }
6189
+ _varDeclStmt() {
6190
+ const modifier = this._consume("IDENT", "Expected 'var' or 'varip'").value;
6191
+ if (this._check("IDENT") && this._peekKind(1) === "IDENT") {
6192
+ this._advance();
6193
+ }
6194
+ const name = this._consume("IDENT", "Expected variable name after var/varip").value;
6195
+ this._consume("EQ", "Expected '=' in var declaration");
6196
+ const value = this._expr();
6197
+ this._consumeNewlineOrEOF();
6198
+ return { kind: "VarDeclStmt", modifier, name, value };
6199
+ }
6200
+ _reassign() {
6201
+ const name = this._consume("IDENT", "Expected identifier").value;
6202
+ this._consume("COLONEQ", "Expected ':='");
6203
+ const value = this._expr();
6204
+ this._consumeNewlineOrEOF();
6205
+ return { kind: "ReassignStmt", name, value };
6206
+ }
6207
+ _forStmt() {
6208
+ this._consumeIdent("for");
6209
+ const name = this._consume("IDENT", "Expected loop variable after 'for'").value;
6210
+ this._consume("EQ", "Expected '=' after loop variable");
6211
+ const start = this._expr();
6212
+ this._consumeIdent("to");
6213
+ const end = this._expr();
6214
+ let step = null;
6215
+ if (this._checkIdent("by")) {
6216
+ this._advance();
6217
+ step = this._expr();
6218
+ }
6219
+ this._consumeNewlineOrEOF();
6220
+ const body = this._indentedBlock();
6221
+ return { kind: "ForStmt", name, start, end, step, body };
5970
6222
  }
5971
6223
  _assign() {
5972
6224
  const name = this._consume("IDENT", "Expected identifier").value;
@@ -5980,6 +6232,75 @@ var Parser = class {
5980
6232
  this._consumeNewlineOrEOF();
5981
6233
  return { kind: "ExprStmt", expr };
5982
6234
  }
6235
+ _ifStmt() {
6236
+ this._consumeIdent("if");
6237
+ const condition = this._expr();
6238
+ this._consumeNewlineOrEOF();
6239
+ const then = this._indentedBlock();
6240
+ let else_ = [];
6241
+ if (this._checkIdent("else")) {
6242
+ this._advance();
6243
+ this._consumeNewlineOrEOF();
6244
+ else_ = this._indentedBlock();
6245
+ }
6246
+ return { kind: "IfStmt", condition, then, else_ };
6247
+ }
6248
+ _whileStmt() {
6249
+ this._consumeIdent("while");
6250
+ const condition = this._expr();
6251
+ this._consumeNewlineOrEOF();
6252
+ const body = this._indentedBlock();
6253
+ return { kind: "WhileStmt", condition, body };
6254
+ }
6255
+ /** Lookahead: is this IDENT '(' ... ')' '=>'? Scans without consuming. */
6256
+ _isFnDecl() {
6257
+ let depth = 0;
6258
+ let i = this._pos + 1;
6259
+ while (i < this._tokens.length) {
6260
+ const k = this._tokens[i].kind;
6261
+ if (k === "LPAREN") depth++;
6262
+ else if (k === "RPAREN") {
6263
+ depth--;
6264
+ if (depth === 0) break;
6265
+ } else if (k === "EOF" || k === "NEWLINE") return false;
6266
+ i++;
6267
+ }
6268
+ return i + 1 < this._tokens.length && this._tokens[i + 1].kind === "ARROW";
6269
+ }
6270
+ /** Parse: name(p1, p2, ...) => expr OR name(p1, p2, ...) =>\n block */
6271
+ _fnDeclStmt() {
6272
+ const name = this._consume("IDENT", "Expected function name").value;
6273
+ this._consume("LPAREN", "Expected '(' after function name");
6274
+ const params = [];
6275
+ if (!this._check("RPAREN")) {
6276
+ params.push(this._consume("IDENT", "Expected parameter name").value);
6277
+ while (this._match("COMMA")) {
6278
+ params.push(this._consume("IDENT", "Expected parameter name").value);
6279
+ }
6280
+ }
6281
+ this._consume("RPAREN", "Expected ')' after parameters");
6282
+ this._consume("ARROW", "Expected '=>'");
6283
+ if (this._check("NEWLINE") || this._check("EOF")) {
6284
+ this._consumeNewlineOrEOF();
6285
+ const body = this._indentedBlock();
6286
+ return { kind: "FnDeclStmt", name, params, body };
6287
+ }
6288
+ const expr = this._expr();
6289
+ this._consumeNewlineOrEOF();
6290
+ return { kind: "FnDeclStmt", name, params, body: [{ kind: "ExprStmt", expr }] };
6291
+ }
6292
+ _indentedBlock() {
6293
+ const stmts = [];
6294
+ if (!this._check("INDENT")) return stmts;
6295
+ this._advance();
6296
+ this._skipNewlines();
6297
+ while (!this._check("DEDENT") && !this._check("EOF")) {
6298
+ stmts.push(this._stmt());
6299
+ this._skipNewlines();
6300
+ }
6301
+ if (this._check("DEDENT")) this._advance();
6302
+ return stmts;
6303
+ }
5983
6304
  // ─── Expressions ────────────────────────────────────────────────────────────
5984
6305
  _expr() {
5985
6306
  return this._ternary();
@@ -6114,11 +6435,19 @@ var Parser = class {
6114
6435
  const node = { kind: "BoolLit", value: tok.value === "true" };
6115
6436
  return node;
6116
6437
  }
6438
+ if (tok.kind === "COLOR") {
6439
+ this._advance();
6440
+ const node = { kind: "ColorLit", value: tok.value };
6441
+ return node;
6442
+ }
6117
6443
  if (tok.kind === "IDENT" && tok.value === "na") {
6118
6444
  this._advance();
6119
6445
  const node = { kind: "NumberLit", value: NaN };
6120
6446
  return node;
6121
6447
  }
6448
+ if (tok.kind === "IDENT" && this._peekKind(1) === "DOT") {
6449
+ return this._nsCallOrMember();
6450
+ }
6122
6451
  if (tok.kind === "IDENT" && this._peekKind(1) === "LPAREN") {
6123
6452
  return this._callExpr();
6124
6453
  }
@@ -6141,21 +6470,57 @@ var Parser = class {
6141
6470
  return inner;
6142
6471
  }
6143
6472
  throw new SyntaxError(
6144
- `[TScript] Unexpected token '${tok.value}' (${tok.kind}) at ${tok.line}:${tok.col}`
6473
+ `[ForgeScript] Unexpected token '${tok.value}' (${tok.kind}) at ${tok.line}:${tok.col}`
6145
6474
  );
6146
6475
  }
6476
+ /** Parse IDENT.IDENT or IDENT.IDENT(args) */
6477
+ _nsCallOrMember() {
6478
+ const ns = this._consume("IDENT", "Expected namespace").value;
6479
+ this._consume("DOT", "Expected '.'");
6480
+ const member = this._consume("IDENT", "Expected member name").value;
6481
+ if (this._check("LPAREN")) {
6482
+ this._advance();
6483
+ const { posArgs, namedArgs } = this._argList();
6484
+ this._consume("RPAREN", "Expected ')'");
6485
+ const node2 = { kind: "NsCallExpr", namespace: ns, fn: member, args: posArgs, namedArgs };
6486
+ return node2;
6487
+ }
6488
+ let node = { kind: "MemberExpr", object: ns, prop: member };
6489
+ while (this._check("LBRACKET")) {
6490
+ this._advance();
6491
+ const index = this._expr();
6492
+ this._consume("RBRACKET", "Expected ']'");
6493
+ const indexNode = { kind: "IndexExpr", series: node, index };
6494
+ node = indexNode;
6495
+ }
6496
+ return node;
6497
+ }
6147
6498
  _callExpr() {
6148
6499
  const callee = this._consume("IDENT", "Expected function name").value;
6149
6500
  this._consume("LPAREN", "Expected '('");
6150
- const args = [];
6151
- if (!this._check("RPAREN")) {
6152
- args.push(this._expr());
6153
- while (this._match("COMMA")) {
6154
- args.push(this._expr());
6501
+ const { posArgs, namedArgs } = this._argList();
6502
+ this._consume("RPAREN", "Expected ')'");
6503
+ return { kind: "CallExpr", callee, args: posArgs, namedArgs };
6504
+ }
6505
+ /** Parse a mixed positional + named argument list. */
6506
+ _argList() {
6507
+ const posArgs = [];
6508
+ const namedArgs = /* @__PURE__ */ new Map();
6509
+ if (this._check("RPAREN")) return { posArgs, namedArgs };
6510
+ const parseArg = () => {
6511
+ if (this._check("IDENT") && this._peekKind(1) === "EQ") {
6512
+ const key = this._consume("IDENT", "Expected named arg").value;
6513
+ this._consume("EQ", "Expected '='");
6514
+ namedArgs.set(key, this._expr());
6515
+ } else {
6516
+ posArgs.push(this._expr());
6155
6517
  }
6518
+ };
6519
+ parseArg();
6520
+ while (this._match("COMMA")) {
6521
+ parseArg();
6156
6522
  }
6157
- this._consume("RPAREN", "Expected ')'");
6158
- return { kind: "CallExpr", callee, args };
6523
+ return { posArgs, namedArgs };
6159
6524
  }
6160
6525
  // ─── Utility helpers ────────────────────────────────────────────────────────
6161
6526
  _binop(op, left, right) {
@@ -6187,12 +6552,12 @@ var Parser = class {
6187
6552
  _consume(kind, msg) {
6188
6553
  if (this._check(kind)) return this._advance();
6189
6554
  const t = this._current();
6190
- throw new SyntaxError(`[TScript] ${msg} \u2014 got '${t.value}' at ${t.line}:${t.col}`);
6555
+ throw new SyntaxError(`[ForgeScript] ${msg} \u2014 got '${t.value}' at ${t.line}:${t.col}`);
6191
6556
  }
6192
6557
  _consumeIdent(name) {
6193
6558
  if (this._checkIdent(name)) return this._advance();
6194
6559
  const t = this._current();
6195
- throw new SyntaxError(`[TScript] Expected '${name}' \u2014 got '${t.value}' at ${t.line}:${t.col}`);
6560
+ throw new SyntaxError(`[ForgeScript] Expected '${name}' \u2014 got '${t.value}' at ${t.line}:${t.col}`);
6196
6561
  }
6197
6562
  _consumeNewlineOrEOF() {
6198
6563
  if (this._check("NEWLINE") || this._check("EOF")) {
@@ -6222,12 +6587,52 @@ var Parser = class {
6222
6587
  }
6223
6588
  };
6224
6589
 
6225
- // src/tscript/runtime.ts
6590
+ // src/forgescript/runtime.ts
6591
+ var TArray = class _TArray {
6592
+ items;
6593
+ constructor(items = []) {
6594
+ this.items = items;
6595
+ }
6596
+ size() {
6597
+ return this.items.length;
6598
+ }
6599
+ get(i) {
6600
+ return i >= 0 && i < this.items.length ? this.items[i] : NaN;
6601
+ }
6602
+ set(i, v) {
6603
+ if (i >= 0 && i < this.items.length) this.items[i] = v;
6604
+ }
6605
+ push(v) {
6606
+ this.items.push(v);
6607
+ }
6608
+ pop() {
6609
+ return this.items.length > 0 ? this.items.pop() : NaN;
6610
+ }
6611
+ remove(i) {
6612
+ if (i < 0 || i >= this.items.length) return NaN;
6613
+ return this.items.splice(i, 1)[0];
6614
+ }
6615
+ clear() {
6616
+ this.items.length = 0;
6617
+ }
6618
+ includes(v) {
6619
+ return this.items.includes(v);
6620
+ }
6621
+ indexOf(v) {
6622
+ return this.items.indexOf(v);
6623
+ }
6624
+ slice(start, end) {
6625
+ return new _TArray(this.items.slice(start, end));
6626
+ }
6627
+ join(sep) {
6628
+ return this.items.map(String).join(sep);
6629
+ }
6630
+ };
6226
6631
  function _num(v, ctx) {
6227
6632
  if (v instanceof Series2) return v.value;
6228
6633
  if (typeof v === "number") return v;
6229
6634
  if (typeof v === "boolean") return v ? 1 : 0;
6230
- throw new TypeError(`[TScript] ${ctx}: expected number, got ${typeof v}`);
6635
+ throw new TypeError(`[ForgeScript] ${ctx}: expected number, got ${typeof v}`);
6231
6636
  }
6232
6637
  function _bool(v) {
6233
6638
  if (v instanceof Series2) return !isNaN(v.value) && v.value !== 0;
@@ -6235,11 +6640,31 @@ function _bool(v) {
6235
6640
  if (typeof v === "boolean") return v;
6236
6641
  return Boolean(v);
6237
6642
  }
6238
- var TScriptRuntime = class {
6643
+ var ForgeScriptRuntime = class _ForgeScriptRuntime {
6239
6644
  _program;
6240
6645
  _scope = /* @__PURE__ */ new Map();
6241
6646
  _plotSeries = [];
6242
6647
  // one entry per plot() call order
6648
+ _plotMetas = [];
6649
+ _hlines = [];
6650
+ _fills = [];
6651
+ _bgcolors = [];
6652
+ _barcolors = [];
6653
+ _shapes = [];
6654
+ _tables = [];
6655
+ _functions = /* @__PURE__ */ new Map();
6656
+ /** Tracks which vars have been initialized (for var/varip init-once semantics). */
6657
+ _varInitialized = /* @__PURE__ */ new Set();
6658
+ /** varip variables update intra-bar; we track their names to handle them differently. */
6659
+ _varipNames = /* @__PURE__ */ new Set();
6660
+ /** Current bar index for use in output functions. */
6661
+ _barIndex = 0;
6662
+ /** Total number of bars in the current run (for barstate). */
6663
+ _totalBars = 0;
6664
+ /** Current bar for output timestamp. */
6665
+ _currentBar = null;
6666
+ /** Whether this indicator is an overlay. */
6667
+ _overlay = false;
6243
6668
  // Public metadata set by indicator() declaration
6244
6669
  title = "Script";
6245
6670
  constructor(src) {
@@ -6247,6 +6672,8 @@ var TScriptRuntime = class {
6247
6672
  for (const stmt of this._program.stmts) {
6248
6673
  if (stmt.kind === "IndicatorDecl") {
6249
6674
  this.title = stmt.title;
6675
+ const ov = stmt.namedArgs.get("overlay");
6676
+ if (ov && ov.kind === "BoolLit") this._overlay = ov.value;
6250
6677
  break;
6251
6678
  }
6252
6679
  }
@@ -6261,6 +6688,8 @@ var TScriptRuntime = class {
6261
6688
  * @returns the current values of all `plot()` series after this bar.
6262
6689
  */
6263
6690
  execBar(bar, barIndex) {
6691
+ this._currentBar = bar;
6692
+ this._barIndex = barIndex;
6264
6693
  this._pushBuiltin("open", bar.open);
6265
6694
  this._pushBuiltin("high", bar.high);
6266
6695
  this._pushBuiltin("low", bar.low);
@@ -6283,11 +6712,16 @@ var TScriptRuntime = class {
6283
6712
  */
6284
6713
  run(bars) {
6285
6714
  this.reset();
6715
+ this._totalBars = bars.length;
6286
6716
  for (let i = 0; i < bars.length; i++) {
6287
6717
  this.execBar(bars[i], i);
6288
6718
  }
6289
6719
  return this.getPlots(bars);
6290
6720
  }
6721
+ /** Set total bar count for barstate properties (call before execBar loop). */
6722
+ setTotalBars(n) {
6723
+ this._totalBars = n;
6724
+ }
6291
6725
  /**
6292
6726
  * Snapshot the current plot series as IndicatorPoint arrays.
6293
6727
  * Must be called after `run()` or after the last `execBar()`.
@@ -6303,6 +6737,21 @@ var TScriptRuntime = class {
6303
6737
  })).filter((p) => !isNaN(p.value));
6304
6738
  });
6305
6739
  }
6740
+ /** Return the full script result including all output types. */
6741
+ getResult(bars) {
6742
+ return {
6743
+ title: this.title,
6744
+ plots: this.getPlots(bars),
6745
+ plotMetas: this._plotMetas,
6746
+ hlines: this._hlines,
6747
+ fills: this._fills,
6748
+ bgcolors: this._bgcolors,
6749
+ barcolors: this._barcolors,
6750
+ shapes: this._shapes,
6751
+ tables: this._tables,
6752
+ overlay: this._overlay
6753
+ };
6754
+ }
6306
6755
  /** Reset all persistent series state (call on symbol/timeframe change). */
6307
6756
  reset() {
6308
6757
  for (const v of this._scope.values()) {
@@ -6310,6 +6759,17 @@ var TScriptRuntime = class {
6310
6759
  }
6311
6760
  for (const s of this._plotSeries) s.reset();
6312
6761
  this._plotIdx = 0;
6762
+ this._varInitialized.clear();
6763
+ this._internalState.clear();
6764
+ this._hlines.length = 0;
6765
+ this._fills.length = 0;
6766
+ this._bgcolors.length = 0;
6767
+ this._barcolors.length = 0;
6768
+ this._shapes.length = 0;
6769
+ this._tables.length = 0;
6770
+ this._plotMetas.length = 0;
6771
+ this._functions.clear();
6772
+ this._tableCounter = 0;
6313
6773
  }
6314
6774
  // ─── Private: statement execution ───────────────────────────────────────────
6315
6775
  _execStmt(stmt) {
@@ -6317,20 +6777,67 @@ var TScriptRuntime = class {
6317
6777
  case "IndicatorDecl":
6318
6778
  break;
6319
6779
  // handled at construction
6780
+ case "VarDeclStmt":
6781
+ this._execVarDecl(stmt);
6782
+ break;
6320
6783
  case "AssignStmt":
6321
6784
  this._execAssign(stmt);
6322
6785
  break;
6786
+ case "ReassignStmt":
6787
+ this._execReassign(stmt);
6788
+ break;
6323
6789
  case "ExprStmt":
6324
6790
  this._evalExpr(stmt.expr);
6325
6791
  break;
6792
+ case "IfStmt":
6793
+ this._execIf(stmt);
6794
+ break;
6795
+ case "WhileStmt":
6796
+ this._execWhile(stmt);
6797
+ break;
6798
+ case "ForStmt":
6799
+ this._execFor(stmt);
6800
+ break;
6801
+ case "FnDeclStmt":
6802
+ this._functions.set(stmt.name, stmt);
6803
+ break;
6326
6804
  }
6327
6805
  }
6328
- _execAssign(stmt) {
6329
- const rhs = this._evalExpr(stmt.value);
6330
- const existing = this._scope.get(stmt.name);
6331
- if (existing instanceof Series2) {
6332
- existing.push(_num(rhs, `assignment to '${stmt.name}'`));
6333
- } else {
6806
+ _execIf(stmt) {
6807
+ const branch = _bool(this._evalExpr(stmt.condition)) ? stmt.then : stmt.else_;
6808
+ for (const s of branch) this._execStmt(s);
6809
+ }
6810
+ _execWhile(stmt) {
6811
+ const MAX_ITER = 1e4;
6812
+ let count = 0;
6813
+ while (_bool(this._evalExpr(stmt.condition))) {
6814
+ if (++count > MAX_ITER) {
6815
+ throw new RangeError("[ForgeScript] while loop exceeded 10,000 iterations \u2014 possible infinite loop");
6816
+ }
6817
+ for (const s of stmt.body) this._execStmt(s);
6818
+ }
6819
+ }
6820
+ _execFor(stmt) {
6821
+ const startVal = _num(this._evalExpr(stmt.start), "for start");
6822
+ const endVal = _num(this._evalExpr(stmt.end), "for end");
6823
+ const stepVal = stmt.step ? _num(this._evalExpr(stmt.step), "for step") : 1;
6824
+ if (stepVal === 0) throw new RangeError("[ForgeScript] for loop step cannot be 0");
6825
+ const MAX_ITER = 1e5;
6826
+ let count = 0;
6827
+ for (let i = startVal; stepVal > 0 ? i <= endVal : i >= endVal; i += stepVal) {
6828
+ if (++count > MAX_ITER) {
6829
+ throw new RangeError("[ForgeScript] for loop exceeded 100,000 iterations");
6830
+ }
6831
+ this._scope.set(stmt.name, i);
6832
+ for (const s of stmt.body) this._execStmt(s);
6833
+ }
6834
+ }
6835
+ /** var / varip — only initialize on the first bar (bar_index 0). */
6836
+ _execVarDecl(stmt) {
6837
+ if (stmt.modifier === "varip") this._varipNames.add(stmt.name);
6838
+ if (!this._varInitialized.has(stmt.name)) {
6839
+ this._varInitialized.add(stmt.name);
6840
+ const rhs = this._evalExpr(stmt.value);
6334
6841
  if (typeof rhs === "number" || rhs instanceof Series2) {
6335
6842
  const series = new Series2(1e3);
6336
6843
  series.push(rhs instanceof Series2 ? rhs.value : rhs);
@@ -6338,24 +6845,68 @@ var TScriptRuntime = class {
6338
6845
  } else {
6339
6846
  this._scope.set(stmt.name, rhs);
6340
6847
  }
6848
+ } else {
6849
+ const existing = this._scope.get(stmt.name);
6850
+ if (existing instanceof Series2) {
6851
+ existing.push(existing.value);
6852
+ }
6341
6853
  }
6342
6854
  }
6343
- // ─── Private: expression evaluation ─────────────────────────────────────────
6344
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
6345
- _evalExpr(expr) {
6346
- switch (expr.kind) {
6347
- case "NumberLit":
6348
- return expr.value;
6349
- case "StringLit":
6350
- return expr.value;
6855
+ /** := reassignment variable must already exist. */
6856
+ _execReassign(stmt) {
6857
+ const rhs = this._evalExpr(stmt.value);
6858
+ const existing = this._scope.get(stmt.name);
6859
+ if (existing === void 0) {
6860
+ throw new ReferenceError(`[ForgeScript] Cannot reassign undefined variable '${stmt.name}'`);
6861
+ }
6862
+ if (existing instanceof Series2) {
6863
+ const numVal = _num(rhs, `reassignment to '${stmt.name}'`);
6864
+ existing.push(numVal);
6865
+ } else {
6866
+ this._scope.set(stmt.name, rhs);
6867
+ }
6868
+ }
6869
+ _execAssign(stmt) {
6870
+ const rhs = this._evalExpr(stmt.value);
6871
+ const existing = this._scope.get(stmt.name);
6872
+ if (rhs instanceof TArray || typeof rhs === "string" && rhs.startsWith("__table_")) {
6873
+ this._scope.set(stmt.name, rhs);
6874
+ return;
6875
+ }
6876
+ if (existing instanceof Series2) {
6877
+ existing.push(_num(rhs, `assignment to '${stmt.name}'`));
6878
+ } else {
6879
+ if (typeof rhs === "number" || rhs instanceof Series2) {
6880
+ const series = new Series2(1e3);
6881
+ series.push(rhs instanceof Series2 ? rhs.value : rhs);
6882
+ this._scope.set(stmt.name, series);
6883
+ } else {
6884
+ this._scope.set(stmt.name, rhs);
6885
+ }
6886
+ }
6887
+ }
6888
+ // ─── Private: expression evaluation ─────────────────────────────────────────
6889
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
6890
+ _evalExpr(expr) {
6891
+ switch (expr.kind) {
6892
+ case "NumberLit":
6893
+ return expr.value;
6894
+ case "StringLit":
6895
+ return expr.value;
6351
6896
  case "BoolLit":
6352
6897
  return expr.value;
6898
+ case "ColorLit":
6899
+ return expr.value;
6353
6900
  case "Identifier":
6354
6901
  return this._evalIdent(expr);
6355
6902
  case "IndexExpr":
6356
6903
  return this._evalIndex(expr);
6357
6904
  case "CallExpr":
6358
6905
  return this._evalCall(expr);
6906
+ case "NsCallExpr":
6907
+ return this._evalNsCall(expr);
6908
+ case "MemberExpr":
6909
+ return this._evalMember(expr);
6359
6910
  case "BinaryExpr":
6360
6911
  return this._evalBinary(expr);
6361
6912
  case "UnaryExpr":
@@ -6369,7 +6920,7 @@ var TScriptRuntime = class {
6369
6920
  _evalIdent(expr) {
6370
6921
  const v = this._scope.get(expr.name);
6371
6922
  if (v === void 0) {
6372
- throw new ReferenceError(`[TScript] Undefined variable '${expr.name}'`);
6923
+ throw new ReferenceError(`[ForgeScript] Undefined variable '${expr.name}'`);
6373
6924
  }
6374
6925
  return v;
6375
6926
  }
@@ -6377,13 +6928,20 @@ var TScriptRuntime = class {
6377
6928
  const series = this._evalExpr(expr.series);
6378
6929
  const idx = _num(this._evalExpr(expr.index), "series index");
6379
6930
  if (!(series instanceof Series2)) {
6380
- throw new TypeError("[TScript] Index operator [] can only be applied to a series");
6931
+ throw new TypeError("[ForgeScript] Index operator [] can only be applied to a series");
6381
6932
  }
6382
6933
  return series.get(Math.round(idx));
6383
6934
  }
6384
6935
  _evalBinary(expr) {
6385
- const l = _num(this._evalExpr(expr.left), `left of '${expr.op}'`);
6386
- const r = _num(this._evalExpr(expr.right), `right of '${expr.op}'`);
6936
+ const rawL = this._evalExpr(expr.left);
6937
+ const rawR = this._evalExpr(expr.right);
6938
+ if (expr.op === "+" && (typeof rawL === "string" || typeof rawR === "string")) {
6939
+ const ls = rawL instanceof Series2 ? String(rawL.value) : String(rawL);
6940
+ const rs = rawR instanceof Series2 ? String(rawR.value) : String(rawR);
6941
+ return ls + rs;
6942
+ }
6943
+ const l = _num(rawL, `left of '${expr.op}'`);
6944
+ const r = _num(rawR, `right of '${expr.op}'`);
6387
6945
  switch (expr.op) {
6388
6946
  case "+":
6389
6947
  return l + r;
@@ -6421,6 +6979,349 @@ var TScriptRuntime = class {
6421
6979
  if (expr.op === "and") return l ? _bool(this._evalExpr(expr.right)) : false;
6422
6980
  return l ? true : _bool(this._evalExpr(expr.right));
6423
6981
  }
6982
+ // ─── Namespace call dispatch ───────────────────────────────────────────────
6983
+ _evalNsCall(expr) {
6984
+ const { namespace, fn, args } = expr;
6985
+ switch (namespace) {
6986
+ case "ta":
6987
+ return this._evalTaCall(fn, args);
6988
+ case "math":
6989
+ return this._evalMathCall(fn, args);
6990
+ case "color":
6991
+ return this._evalColorCall(fn, args);
6992
+ case "str":
6993
+ return this._evalStrCall(fn, args);
6994
+ case "array":
6995
+ return this._evalArrayCall(fn, args);
6996
+ case "table":
6997
+ return this._evalTableCall(fn, args, expr.namedArgs);
6998
+ case "input":
6999
+ return this._evalInputNsCall(fn, args);
7000
+ default:
7001
+ throw new ReferenceError(`[ForgeScript] Unknown namespace '${namespace}'`);
7002
+ }
7003
+ }
7004
+ _evalTaCall(method, args) {
7005
+ switch (method) {
7006
+ case "sma":
7007
+ return this._ta_sma(args);
7008
+ case "ema":
7009
+ return this._ta_ema(args);
7010
+ case "wma":
7011
+ return this._ta_wma(args);
7012
+ case "rma":
7013
+ return this._ta_rma(args);
7014
+ case "stdev":
7015
+ return this._ta_stdev(args);
7016
+ case "highest":
7017
+ return this._ta_rolling(args, Math.max);
7018
+ case "lowest":
7019
+ return this._ta_rolling(args, Math.min);
7020
+ case "change":
7021
+ return this._ta_change(args);
7022
+ case "mom":
7023
+ return this._ta_change(args);
7024
+ case "atr":
7025
+ return this._ta_atr(args);
7026
+ case "rsi":
7027
+ return this._ta_rsi(args);
7028
+ case "crossover":
7029
+ return this._ta_crossover(args);
7030
+ case "crossunder":
7031
+ return this._ta_crossunder(args);
7032
+ case "cross":
7033
+ return this._ta_cross(args);
7034
+ case "barssince":
7035
+ return this._ta_barssince(args);
7036
+ case "macd":
7037
+ return this._ta_macd(args);
7038
+ case "bb":
7039
+ return this._ta_bb(args);
7040
+ case "stoch":
7041
+ return this._ta_stoch(args);
7042
+ case "obv":
7043
+ return this._ta_obv(args);
7044
+ case "correlation":
7045
+ return this._ta_correlation(args);
7046
+ case "dev":
7047
+ return this._ta_dev(args);
7048
+ case "variance":
7049
+ return this._ta_variance(args);
7050
+ case "cum":
7051
+ return this._ta_cum(args);
7052
+ case "sum":
7053
+ return this._ta_sum(args);
7054
+ case "valuewhen":
7055
+ return this._ta_valuewhen(args);
7056
+ case "pivothigh":
7057
+ return this._ta_pivothigh(args);
7058
+ case "pivotlow":
7059
+ return this._ta_pivotlow(args);
7060
+ case "tr":
7061
+ return this._ta_tr(args);
7062
+ case "swma":
7063
+ return this._ta_swma(args);
7064
+ case "vwma":
7065
+ return this._ta_vwma(args);
7066
+ case "rising":
7067
+ return this._ta_rising(args);
7068
+ case "falling":
7069
+ return this._ta_falling(args);
7070
+ default:
7071
+ throw new ReferenceError(`[ForgeScript] Unknown ta function 'ta.${method}'`);
7072
+ }
7073
+ }
7074
+ _evalMathCall(method, args) {
7075
+ switch (method) {
7076
+ case "abs":
7077
+ return Math.abs(_num(this._evalExpr(args[0]), "math.abs()"));
7078
+ case "round":
7079
+ return Math.round(_num(this._evalExpr(args[0]), "math.round()"));
7080
+ case "floor":
7081
+ return Math.floor(_num(this._evalExpr(args[0]), "math.floor()"));
7082
+ case "ceil":
7083
+ return Math.ceil(_num(this._evalExpr(args[0]), "math.ceil()"));
7084
+ case "sqrt":
7085
+ return Math.sqrt(_num(this._evalExpr(args[0]), "math.sqrt()"));
7086
+ case "log":
7087
+ return Math.log(_num(this._evalExpr(args[0]), "math.log()"));
7088
+ case "log10":
7089
+ return Math.log10(_num(this._evalExpr(args[0]), "math.log10()"));
7090
+ case "sign":
7091
+ return Math.sign(_num(this._evalExpr(args[0]), "math.sign()"));
7092
+ case "exp":
7093
+ return Math.exp(_num(this._evalExpr(args[0]), "math.exp()"));
7094
+ case "pow": {
7095
+ const base = _num(this._evalExpr(args[0]), "math.pow() base");
7096
+ const exp = _num(this._evalExpr(args[1]), "math.pow() exponent");
7097
+ return Math.pow(base, exp);
7098
+ }
7099
+ case "max":
7100
+ return Math.max(...args.map((a) => _num(this._evalExpr(a), "math.max()")));
7101
+ case "min":
7102
+ return Math.min(...args.map((a) => _num(this._evalExpr(a), "math.min()")));
7103
+ case "avg": {
7104
+ const vals = args.map((a) => _num(this._evalExpr(a), "math.avg()"));
7105
+ return vals.reduce((s, v) => s + v, 0) / vals.length;
7106
+ }
7107
+ case "sum": {
7108
+ return args.map((a) => _num(this._evalExpr(a), "math.sum()")).reduce((s, v) => s + v, 0);
7109
+ }
7110
+ case "todegrees":
7111
+ return _num(this._evalExpr(args[0]), "math.todegrees()") * (180 / Math.PI);
7112
+ case "toradians":
7113
+ return _num(this._evalExpr(args[0]), "math.toradians()") * (Math.PI / 180);
7114
+ case "sin":
7115
+ return Math.sin(_num(this._evalExpr(args[0]), "math.sin()"));
7116
+ case "cos":
7117
+ return Math.cos(_num(this._evalExpr(args[0]), "math.cos()"));
7118
+ case "tan":
7119
+ return Math.tan(_num(this._evalExpr(args[0]), "math.tan()"));
7120
+ case "asin":
7121
+ return Math.asin(_num(this._evalExpr(args[0]), "math.asin()"));
7122
+ case "acos":
7123
+ return Math.acos(_num(this._evalExpr(args[0]), "math.acos()"));
7124
+ case "atan":
7125
+ return Math.atan(_num(this._evalExpr(args[0]), "math.atan()"));
7126
+ case "random":
7127
+ return Math.random();
7128
+ default:
7129
+ throw new ReferenceError(`[ForgeScript] Unknown math function 'math.${method}'`);
7130
+ }
7131
+ }
7132
+ _evalColorCall(method, args) {
7133
+ if (method === "new") {
7134
+ const base = String(this._evalExpr(args[0]));
7135
+ const transp = _num(this._evalExpr(args[1]), "color.new() transparency");
7136
+ const alpha = Math.round(255 * (1 - transp / 100));
7137
+ const hex = base.startsWith("#") ? base.slice(0, 7) : base;
7138
+ return hex + alpha.toString(16).padStart(2, "0");
7139
+ }
7140
+ if (method === "rgb") {
7141
+ const r = Math.round(_num(this._evalExpr(args[0]), "color.rgb() r"));
7142
+ const g = Math.round(_num(this._evalExpr(args[1]), "color.rgb() g"));
7143
+ const b = Math.round(_num(this._evalExpr(args[2]), "color.rgb() b"));
7144
+ const t = args.length > 3 ? _num(this._evalExpr(args[3]), "color.rgb() transp") : 0;
7145
+ const a = Math.round(255 * (1 - t / 100));
7146
+ return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}${a.toString(16).padStart(2, "0")}`;
7147
+ }
7148
+ throw new ReferenceError(`[ForgeScript] Unknown color function 'color.${method}'`);
7149
+ }
7150
+ _evalStrCall(method, args) {
7151
+ switch (method) {
7152
+ case "tostring":
7153
+ return String(this._evalExpr(args[0]));
7154
+ case "tonumber": {
7155
+ const v = Number(this._evalExpr(args[0]));
7156
+ return isNaN(v) ? NaN : v;
7157
+ }
7158
+ case "format": {
7159
+ let fmt = String(this._evalExpr(args[0]));
7160
+ for (let i = 1; i < args.length; i++) {
7161
+ fmt = fmt.replace(`{${i - 1}}`, String(this._evalExpr(args[i])));
7162
+ }
7163
+ return fmt;
7164
+ }
7165
+ case "length":
7166
+ return String(this._evalExpr(args[0])).length;
7167
+ case "trim":
7168
+ return String(this._evalExpr(args[0])).trim();
7169
+ case "contains": {
7170
+ const s = String(this._evalExpr(args[0]));
7171
+ const sub = String(this._evalExpr(args[1]));
7172
+ return s.includes(sub);
7173
+ }
7174
+ case "substring": {
7175
+ const s = String(this._evalExpr(args[0]));
7176
+ const start = Math.trunc(Number(this._evalExpr(args[1])));
7177
+ const end = args.length > 2 ? Math.trunc(Number(this._evalExpr(args[2]))) : void 0;
7178
+ return s.substring(start, end);
7179
+ }
7180
+ case "replace_all": {
7181
+ const s = String(this._evalExpr(args[0]));
7182
+ const target = String(this._evalExpr(args[1]));
7183
+ const replacement = String(this._evalExpr(args[2]));
7184
+ return s.split(target).join(replacement);
7185
+ }
7186
+ case "upper":
7187
+ return String(this._evalExpr(args[0])).toUpperCase();
7188
+ case "lower":
7189
+ return String(this._evalExpr(args[0])).toLowerCase();
7190
+ case "split": {
7191
+ const s = String(this._evalExpr(args[0]));
7192
+ const sep = args.length > 1 ? String(this._evalExpr(args[1])) : ",";
7193
+ return new TArray(s.split(sep));
7194
+ }
7195
+ default:
7196
+ throw new ReferenceError(`[ForgeScript] Unknown str function 'str.${method}'`);
7197
+ }
7198
+ }
7199
+ // ─── Member access dispatch ──────────────────────────────────────────────────
7200
+ static _COLOR_MAP = {
7201
+ red: "#FF0000",
7202
+ green: "#00FF00",
7203
+ blue: "#0000FF",
7204
+ white: "#FFFFFF",
7205
+ black: "#000000",
7206
+ yellow: "#FFFF00",
7207
+ orange: "#FF9800",
7208
+ purple: "#9C27B0",
7209
+ aqua: "#00BCD4",
7210
+ lime: "#8BC34A",
7211
+ teal: "#009688",
7212
+ fuchsia: "#E040FB",
7213
+ silver: "#9E9E9E",
7214
+ gray: "#787B86",
7215
+ olive: "#808000",
7216
+ maroon: "#880E4F",
7217
+ navy: "#311B92"
7218
+ };
7219
+ _evalMember(expr) {
7220
+ const { object, prop } = expr;
7221
+ switch (object) {
7222
+ case "color":
7223
+ return _ForgeScriptRuntime._COLOR_MAP[prop] ?? NaN;
7224
+ case "syminfo": {
7225
+ switch (prop) {
7226
+ case "tickerid":
7227
+ return this._scope.get("__syminfo_tickerid") ?? "";
7228
+ case "ticker":
7229
+ return this._scope.get("__syminfo_ticker") ?? "";
7230
+ case "mintick":
7231
+ return this._scope.get("__syminfo_mintick") ?? 0.01;
7232
+ case "pointvalue":
7233
+ return this._scope.get("__syminfo_pointvalue") ?? 1;
7234
+ case "currency":
7235
+ return "USD";
7236
+ case "type":
7237
+ return "stock";
7238
+ default:
7239
+ return NaN;
7240
+ }
7241
+ }
7242
+ case "timeframe": {
7243
+ switch (prop) {
7244
+ case "period":
7245
+ return this._scope.get("__timeframe_period") ?? "1";
7246
+ case "multiplier":
7247
+ return this._scope.get("__timeframe_multiplier") ?? 1;
7248
+ case "isintraday":
7249
+ return true;
7250
+ case "isdaily":
7251
+ return false;
7252
+ case "isweekly":
7253
+ return false;
7254
+ case "ismonthly":
7255
+ return false;
7256
+ default:
7257
+ return NaN;
7258
+ }
7259
+ }
7260
+ // Pine v6 enum-like namespaces — return property name as string
7261
+ case "shape":
7262
+ return prop;
7263
+ // shape.circle → "circle"
7264
+ case "location":
7265
+ return prop;
7266
+ // location.abovebar → "abovebar"
7267
+ case "plot":
7268
+ return prop.replace(/^style_/, "");
7269
+ // plot.style_line → "line"
7270
+ case "math": {
7271
+ switch (prop) {
7272
+ case "pi":
7273
+ return Math.PI;
7274
+ case "e":
7275
+ return Math.E;
7276
+ default:
7277
+ return NaN;
7278
+ }
7279
+ }
7280
+ case "barstate": {
7281
+ switch (prop) {
7282
+ case "islast":
7283
+ return this._totalBars > 0 && this._barIndex === this._totalBars - 1;
7284
+ case "isfirst":
7285
+ return this._barIndex === 0;
7286
+ case "isconfirmed":
7287
+ return true;
7288
+ // historical bars are always confirmed
7289
+ case "isnew":
7290
+ return true;
7291
+ // each bar is "new" in per-bar execution
7292
+ case "isrealtime":
7293
+ return false;
7294
+ // ForgeScript runs on historical data
7295
+ case "ishistory":
7296
+ return true;
7297
+ default:
7298
+ return false;
7299
+ }
7300
+ }
7301
+ case "position":
7302
+ return prop;
7303
+ // position.top_right → "top_right"
7304
+ case "size":
7305
+ return prop;
7306
+ // size.large → "large"
7307
+ case "text": {
7308
+ return prop.replace(/^align_/, "");
7309
+ }
7310
+ case "bar_index":
7311
+ return this._barIndex;
7312
+ case "close":
7313
+ case "open":
7314
+ case "high":
7315
+ case "low":
7316
+ case "volume":
7317
+ case "time": {
7318
+ const v = this._scope.get(object);
7319
+ return v === void 0 ? NaN : v;
7320
+ }
7321
+ default:
7322
+ throw new ReferenceError(`[ForgeScript] Unknown member access '${object}.${prop}'`);
7323
+ }
7324
+ }
6424
7325
  // ─── Built-in function dispatch ──────────────────────────────────────────────
6425
7326
  _plotIdx = 0;
6426
7327
  _evalCall(expr) {
@@ -6428,16 +7329,32 @@ var TScriptRuntime = class {
6428
7329
  const args = expr.args;
6429
7330
  switch (fn) {
6430
7331
  // ── Utility ────────────────────────────────────────────────────────────
6431
- case "input": {
7332
+ case "input":
7333
+ case "input.text_area":
7334
+ case "input.string": {
6432
7335
  if (args.length === 0) return 0;
6433
7336
  const def = this._evalExpr(args[0]);
6434
- return typeof def === "string" ? parseFloat(def) || 0 : _num(def, "input()");
7337
+ if (fn === "input.text_area" || fn === "input.string") return typeof def === "string" ? def : String(def);
7338
+ if (typeof def === "string") {
7339
+ const n = parseFloat(def);
7340
+ return isNaN(n) ? def : n;
7341
+ }
7342
+ return _num(def, "input()");
6435
7343
  }
6436
7344
  case "plot": {
6437
7345
  const value = _num(this._evalExpr(args[0]), "plot()");
6438
7346
  const idx = this._plotIdx++;
6439
7347
  if (idx >= this._plotSeries.length) {
6440
7348
  this._plotSeries.push(new Series2(1e3));
7349
+ const na = expr.namedArgs;
7350
+ const title = na?.get("title") ? String(this._evalExpr(na.get("title"))) : `Plot ${idx}`;
7351
+ const color = na?.get("color") ? String(this._evalExpr(na.get("color"))) : void 0;
7352
+ const linewidth = na?.get("linewidth") ? _num(this._evalExpr(na.get("linewidth")), "plot linewidth") : void 0;
7353
+ const style = na?.get("style") ? String(this._evalExpr(na.get("style"))) : "line";
7354
+ const meta = { title, style };
7355
+ if (color !== void 0) meta.color = color;
7356
+ if (linewidth !== void 0) meta.linewidth = linewidth;
7357
+ this._plotMetas.push(meta);
6441
7358
  }
6442
7359
  this._plotSeries[idx].push(value);
6443
7360
  return value;
@@ -6498,14 +7415,74 @@ var TScriptRuntime = class {
6498
7415
  return this._ta_change(args);
6499
7416
  case "atr":
6500
7417
  return this._ta_atr(args);
7418
+ case "rsi":
7419
+ return this._ta_rsi(args);
6501
7420
  case "crossover":
6502
7421
  return this._ta_crossover(args);
6503
7422
  case "crossunder":
6504
7423
  return this._ta_crossunder(args);
7424
+ case "cross":
7425
+ return this._ta_cross(args);
6505
7426
  case "barssince":
6506
7427
  return this._ta_barssince(args);
6507
- default:
6508
- throw new ReferenceError(`[TScript] Unknown function '${fn}'`);
7428
+ // New TA functions (flat-call form)
7429
+ case "macd":
7430
+ return this._ta_macd(args);
7431
+ case "bb":
7432
+ return this._ta_bb(args);
7433
+ case "stoch":
7434
+ return this._ta_stoch(args);
7435
+ case "obv":
7436
+ return this._ta_obv(args);
7437
+ case "correlation":
7438
+ return this._ta_correlation(args);
7439
+ case "dev":
7440
+ return this._ta_dev(args);
7441
+ case "variance":
7442
+ return this._ta_variance(args);
7443
+ case "cum":
7444
+ return this._ta_cum(args);
7445
+ case "sum":
7446
+ return this._ta_sum(args);
7447
+ case "valuewhen":
7448
+ return this._ta_valuewhen(args);
7449
+ case "pivothigh":
7450
+ return this._ta_pivothigh(args);
7451
+ case "pivotlow":
7452
+ return this._ta_pivotlow(args);
7453
+ case "tr":
7454
+ return this._ta_tr(args);
7455
+ case "swma":
7456
+ return this._ta_swma(args);
7457
+ case "vwma":
7458
+ return this._ta_vwma(args);
7459
+ case "rising":
7460
+ return this._ta_rising(args);
7461
+ case "falling":
7462
+ return this._ta_falling(args);
7463
+ case "exp":
7464
+ return Math.exp(_num(this._evalExpr(args[0]), "exp()"));
7465
+ // ── Output functions ──────────────────────────────────────────────────
7466
+ case "plotshape":
7467
+ return this._callPlotshape(expr);
7468
+ case "plotchar":
7469
+ return this._callPlotshape(expr);
7470
+ // same output type
7471
+ case "plotarrow":
7472
+ return this._callPlotarrow(expr);
7473
+ case "hline":
7474
+ return this._callHline(expr);
7475
+ case "fill":
7476
+ return this._callFill(expr);
7477
+ case "bgcolor":
7478
+ return this._callBgcolor(expr);
7479
+ case "barcolor":
7480
+ return this._callBarcolor(expr);
7481
+ default: {
7482
+ const fnDecl = this._functions.get(fn);
7483
+ if (fnDecl) return this._callUserFunction(fnDecl, args);
7484
+ throw new ReferenceError(`[ForgeScript] Unknown function '${fn}'`);
7485
+ }
6509
7486
  }
6510
7487
  }
6511
7488
  // ─── TA helpers ──────────────────────────────────────────────────────────────
@@ -6518,11 +7495,42 @@ var TScriptRuntime = class {
6518
7495
  s.push(v);
6519
7496
  return s;
6520
7497
  }
6521
- throw new TypeError(`[TScript] ${ctx}: expected a series`);
7498
+ throw new TypeError(`[ForgeScript] ${ctx}: expected a series`);
6522
7499
  }
6523
7500
  _argInt(arg, ctx) {
6524
7501
  return Math.round(_num(this._evalExpr(arg), ctx));
6525
7502
  }
7503
+ _ta_rsi(args) {
7504
+ const src = this._argSeries(args[0], "rsi(src)");
7505
+ const period = this._argInt(args[1], "rsi(length)");
7506
+ if (src.length <= period) return NaN;
7507
+ const gainKey = `__rsi_gain_${args[0].kind}_${period}`;
7508
+ const lossKey = `__rsi_loss_${args[0].kind}_${period}`;
7509
+ let prevGain = this._getInternalNum(gainKey);
7510
+ let prevLoss = this._getInternalNum(lossKey);
7511
+ const change = src.get(0) - src.get(1);
7512
+ const gain = change > 0 ? change : 0;
7513
+ const loss = change < 0 ? -change : 0;
7514
+ let avgGain;
7515
+ let avgLoss;
7516
+ if (isNaN(prevGain)) {
7517
+ let gSum = 0, lSum = 0;
7518
+ for (let i = 0; i < period; i++) {
7519
+ const d = src.get(i) - src.get(i + 1);
7520
+ if (d > 0) gSum += d;
7521
+ else lSum -= d;
7522
+ }
7523
+ avgGain = gSum / period;
7524
+ avgLoss = lSum / period;
7525
+ } else {
7526
+ avgGain = (prevGain * (period - 1) + gain) / period;
7527
+ avgLoss = (prevLoss * (period - 1) + loss) / period;
7528
+ }
7529
+ this._setInternal(gainKey, avgGain);
7530
+ this._setInternal(lossKey, avgLoss);
7531
+ if (avgLoss === 0) return 100;
7532
+ return 100 - 100 / (1 + avgGain / avgLoss);
7533
+ }
6526
7534
  _ta_sma(args) {
6527
7535
  const src = this._argSeries(args[0], "sma(src)");
6528
7536
  const period = this._argInt(args[1], "sma(length)");
@@ -6627,6 +7635,541 @@ var TScriptRuntime = class {
6627
7635
  }
6628
7636
  return NaN;
6629
7637
  }
7638
+ // ─── Additional TA functions ─────────────────────────────────────────────────
7639
+ /** ta.cross(a, b) — true when a crosses b in either direction */
7640
+ _ta_cross(args) {
7641
+ const a = this._argSeries(args[0], "cross(a)");
7642
+ const b = this._argSeries(args[1], "cross(b)");
7643
+ return a.get(0) > b.get(0) && a.get(1) <= b.get(1) || a.get(0) < b.get(0) && a.get(1) >= b.get(1);
7644
+ }
7645
+ /** ta.macd(src, fast, slow, signal) — returns [macd, signal, hist] via internal series */
7646
+ _ta_macd(args) {
7647
+ const src = this._argSeries(args[0], "macd(src)");
7648
+ const fast = this._argInt(args[1], "macd(fastlen)");
7649
+ const slow = this._argInt(args[2], "macd(slowlen)");
7650
+ const signal = this._argInt(args[3], "macd(signallen)");
7651
+ const fastKey = `__macd_fast_${args[0].kind}_${fast}`;
7652
+ const slowKey = `__macd_slow_${args[0].kind}_${slow}`;
7653
+ const sigKey = `__macd_sig_${args[0].kind}_${fast}_${slow}_${signal}`;
7654
+ const cur = src.get(0);
7655
+ const kFast = 2 / (fast + 1);
7656
+ const kSlow = 2 / (slow + 1);
7657
+ const kSig = 2 / (signal + 1);
7658
+ let prevFast = this._getInternalNum(fastKey);
7659
+ let prevSlow = this._getInternalNum(slowKey);
7660
+ let prevSig = this._getInternalNum(sigKey);
7661
+ const fastEMA = isNaN(prevFast) ? src.length >= fast ? this._smaOfSeries(src, fast) : NaN : cur * kFast + prevFast * (1 - kFast);
7662
+ const slowEMA = isNaN(prevSlow) ? src.length >= slow ? this._smaOfSeries(src, slow) : NaN : cur * kSlow + prevSlow * (1 - kSlow);
7663
+ this._setInternal(fastKey, fastEMA);
7664
+ this._setInternal(slowKey, slowEMA);
7665
+ const macdLine = fastEMA - slowEMA;
7666
+ const sigLine = isNaN(prevSig) ? macdLine : macdLine * kSig + prevSig * (1 - kSig);
7667
+ this._setInternal(sigKey, sigLine);
7668
+ const hist = macdLine - sigLine;
7669
+ this._pushBuiltin("__macd_line", macdLine);
7670
+ this._pushBuiltin("__macd_signal", sigLine);
7671
+ this._pushBuiltin("__macd_hist", hist);
7672
+ return macdLine;
7673
+ }
7674
+ /** ta.bb(src, length, mult) — returns middle band; stores upper/lower in named series */
7675
+ _ta_bb(args) {
7676
+ const src = this._argSeries(args[0], "bb(src)");
7677
+ const period = this._argInt(args[1], "bb(length)");
7678
+ const mult = _num(this._evalExpr(args[2]), "bb(mult)");
7679
+ if (src.length < period) return NaN;
7680
+ const middle = this._smaOfSeries(src, period);
7681
+ let variance = 0;
7682
+ for (let i = 0; i < period; i++) {
7683
+ const d = src.get(i) - middle;
7684
+ variance += d * d;
7685
+ }
7686
+ const stdevVal = Math.sqrt(variance / period);
7687
+ const upper = middle + mult * stdevVal;
7688
+ const lower = middle - mult * stdevVal;
7689
+ this._pushBuiltin("__bb_upper", upper);
7690
+ this._pushBuiltin("__bb_middle", middle);
7691
+ this._pushBuiltin("__bb_lower", lower);
7692
+ return middle;
7693
+ }
7694
+ /** ta.stoch(source, high, low, length) — %K */
7695
+ _ta_stoch(args) {
7696
+ const src = this._argSeries(args[0], "stoch(src)");
7697
+ const hi = this._argSeries(args[1], "stoch(high)");
7698
+ const lo = this._argSeries(args[2], "stoch(low)");
7699
+ const period = this._argInt(args[3], "stoch(length)");
7700
+ if (hi.length < period || lo.length < period) return NaN;
7701
+ let hh = -Infinity, ll = Infinity;
7702
+ for (let i = 0; i < period; i++) {
7703
+ hh = Math.max(hh, hi.get(i));
7704
+ ll = Math.min(ll, lo.get(i));
7705
+ }
7706
+ const denom = hh - ll;
7707
+ return denom === 0 ? 50 : 100 * (src.get(0) - ll) / denom;
7708
+ }
7709
+ /** ta.obv — On Balance Volume */
7710
+ _ta_obv(_args) {
7711
+ const close = this._scope.get("close");
7712
+ const volume = this._scope.get("volume");
7713
+ if (!(close instanceof Series2) || !(volume instanceof Series2)) return NaN;
7714
+ if (close.length < 2) return volume.get(0);
7715
+ const stateKey = "__obv";
7716
+ const prev = this._getInternalNum(stateKey);
7717
+ const prevOBV = isNaN(prev) ? 0 : prev;
7718
+ const diff = close.get(0) - close.get(1);
7719
+ const result = diff > 0 ? prevOBV + volume.get(0) : diff < 0 ? prevOBV - volume.get(0) : prevOBV;
7720
+ this._setInternal(stateKey, result);
7721
+ return result;
7722
+ }
7723
+ /** ta.correlation(src1, src2, length) — Pearson r */
7724
+ _ta_correlation(args) {
7725
+ const a = this._argSeries(args[0], "correlation(src1)");
7726
+ const b = this._argSeries(args[1], "correlation(src2)");
7727
+ const period = this._argInt(args[2], "correlation(length)");
7728
+ if (a.length < period || b.length < period) return NaN;
7729
+ let sumA = 0, sumB = 0, sumAB = 0, sumA2 = 0, sumB2 = 0;
7730
+ for (let i = 0; i < period; i++) {
7731
+ const av = a.get(i), bv = b.get(i);
7732
+ sumA += av;
7733
+ sumB += bv;
7734
+ sumAB += av * bv;
7735
+ sumA2 += av * av;
7736
+ sumB2 += bv * bv;
7737
+ }
7738
+ const n = period;
7739
+ const denom = Math.sqrt((n * sumA2 - sumA * sumA) * (n * sumB2 - sumB * sumB));
7740
+ return denom === 0 ? 0 : (n * sumAB - sumA * sumB) / denom;
7741
+ }
7742
+ /** ta.dev(src, length) — mean absolute deviation */
7743
+ _ta_dev(args) {
7744
+ const src = this._argSeries(args[0], "dev(src)");
7745
+ const period = this._argInt(args[1], "dev(length)");
7746
+ if (src.length < period) return NaN;
7747
+ const mean = this._smaOfSeries(src, period);
7748
+ let sum = 0;
7749
+ for (let i = 0; i < period; i++) sum += Math.abs(src.get(i) - mean);
7750
+ return sum / period;
7751
+ }
7752
+ /** ta.variance(src, length) */
7753
+ _ta_variance(args) {
7754
+ const src = this._argSeries(args[0], "variance(src)");
7755
+ const period = this._argInt(args[1], "variance(length)");
7756
+ if (src.length < period) return NaN;
7757
+ const mean = this._smaOfSeries(src, period);
7758
+ let sum = 0;
7759
+ for (let i = 0; i < period; i++) {
7760
+ const d = src.get(i) - mean;
7761
+ sum += d * d;
7762
+ }
7763
+ return sum / period;
7764
+ }
7765
+ /** ta.cum(src) — cumulative sum */
7766
+ _ta_cum(args) {
7767
+ const v = _num(this._evalExpr(args[0]), "cum(src)");
7768
+ const key = "__cum";
7769
+ const prev = this._getInternalNum(key);
7770
+ const result = (isNaN(prev) ? 0 : prev) + (isNaN(v) ? 0 : v);
7771
+ this._setInternal(key, result);
7772
+ return result;
7773
+ }
7774
+ /** ta.sum(src, length) — rolling sum */
7775
+ _ta_sum(args) {
7776
+ const src = this._argSeries(args[0], "sum(src)");
7777
+ const period = this._argInt(args[1], "sum(length)");
7778
+ if (src.length < period) return NaN;
7779
+ let sum = 0;
7780
+ for (let i = 0; i < period; i++) sum += src.get(i);
7781
+ return sum;
7782
+ }
7783
+ /** ta.valuewhen(condition, source, occurrence) */
7784
+ _ta_valuewhen(args) {
7785
+ const cond = this._argSeries(args[0], "valuewhen(cond)");
7786
+ const src = this._argSeries(args[1], "valuewhen(src)");
7787
+ const occ = args.length > 2 ? this._argInt(args[2], "valuewhen(occurrence)") : 0;
7788
+ let count = 0;
7789
+ for (let i = 0; i < cond.length; i++) {
7790
+ if (_bool(cond.get(i))) {
7791
+ if (count === occ) return src.get(i);
7792
+ count++;
7793
+ }
7794
+ }
7795
+ return NaN;
7796
+ }
7797
+ /** ta.pivothigh(src, leftbars, rightbars) */
7798
+ _ta_pivothigh(args) {
7799
+ const src = this._argSeries(args[0], "pivothigh(src)");
7800
+ const left = this._argInt(args[1], "pivothigh(leftbars)");
7801
+ const right = this._argInt(args[2], "pivothigh(rightbars)");
7802
+ if (src.length < left + right + 1) return NaN;
7803
+ const pivotIdx = right;
7804
+ const pivotVal = src.get(pivotIdx);
7805
+ for (let i = 1; i <= left; i++) {
7806
+ if (src.get(pivotIdx + i) >= pivotVal) return NaN;
7807
+ }
7808
+ for (let i = 1; i <= right; i++) {
7809
+ if (src.get(pivotIdx - i) >= pivotVal) return NaN;
7810
+ }
7811
+ return pivotVal;
7812
+ }
7813
+ /** ta.pivotlow(src, leftbars, rightbars) */
7814
+ _ta_pivotlow(args) {
7815
+ const src = this._argSeries(args[0], "pivotlow(src)");
7816
+ const left = this._argInt(args[1], "pivotlow(leftbars)");
7817
+ const right = this._argInt(args[2], "pivotlow(rightbars)");
7818
+ if (src.length < left + right + 1) return NaN;
7819
+ const pivotIdx = right;
7820
+ const pivotVal = src.get(pivotIdx);
7821
+ for (let i = 1; i <= left; i++) {
7822
+ if (src.get(pivotIdx + i) <= pivotVal) return NaN;
7823
+ }
7824
+ for (let i = 1; i <= right; i++) {
7825
+ if (src.get(pivotIdx - i) <= pivotVal) return NaN;
7826
+ }
7827
+ return pivotVal;
7828
+ }
7829
+ /** ta.tr(handleNa?) — True Range */
7830
+ _ta_tr(_args) {
7831
+ const high = this._scope.get("high");
7832
+ const low = this._scope.get("low");
7833
+ const close = this._scope.get("close");
7834
+ if (!(high instanceof Series2) || !(low instanceof Series2) || !(close instanceof Series2)) return NaN;
7835
+ if (close.length < 2) return high.get(0) - low.get(0);
7836
+ const h = high.get(0);
7837
+ const l = low.get(0);
7838
+ const pc = close.get(1);
7839
+ return Math.max(h - l, Math.abs(h - pc), Math.abs(l - pc));
7840
+ }
7841
+ /** ta.swma(src) — Symmetrically Weighted MA with fixed period 4 */
7842
+ _ta_swma(args) {
7843
+ const src = this._argSeries(args[0], "swma(src)");
7844
+ if (src.length < 4) return NaN;
7845
+ return (src.get(3) * 1 + src.get(2) * 3 + src.get(1) * 3 + src.get(0) * 1) / 8;
7846
+ }
7847
+ /** ta.vwma(src, length) — Volume Weighted MA */
7848
+ _ta_vwma(args) {
7849
+ const src = this._argSeries(args[0], "vwma(src)");
7850
+ const period = this._argInt(args[1], "vwma(length)");
7851
+ const volume = this._scope.get("volume");
7852
+ if (!(volume instanceof Series2)) return this._smaOfSeries(src, period);
7853
+ if (src.length < period || volume.length < period) return NaN;
7854
+ let sumSV = 0, sumV = 0;
7855
+ for (let i = 0; i < period; i++) {
7856
+ const v = volume.get(i);
7857
+ sumSV += src.get(i) * v;
7858
+ sumV += v;
7859
+ }
7860
+ return sumV === 0 ? NaN : sumSV / sumV;
7861
+ }
7862
+ /** ta.rising(src, length) */
7863
+ _ta_rising(args) {
7864
+ const src = this._argSeries(args[0], "rising(src)");
7865
+ const period = this._argInt(args[1], "rising(length)");
7866
+ if (src.length <= period) return false;
7867
+ for (let i = 0; i < period; i++) {
7868
+ if (src.get(i) <= src.get(i + 1)) return false;
7869
+ }
7870
+ return true;
7871
+ }
7872
+ /** ta.falling(src, length) */
7873
+ _ta_falling(args) {
7874
+ const src = this._argSeries(args[0], "falling(src)");
7875
+ const period = this._argInt(args[1], "falling(length)");
7876
+ if (src.length <= period) return false;
7877
+ for (let i = 0; i < period; i++) {
7878
+ if (src.get(i) >= src.get(i + 1)) return false;
7879
+ }
7880
+ return true;
7881
+ }
7882
+ // ─── Output function implementations ─────────────────────────────────────────
7883
+ _callPlotshape(expr) {
7884
+ const args = expr.args;
7885
+ const na = expr.namedArgs;
7886
+ const cond = args.length > 0 ? _bool(this._evalExpr(args[0])) : true;
7887
+ if (!cond || !this._currentBar) return NaN;
7888
+ const style = na?.get("style") ? String(this._evalExpr(na.get("style"))) : "xcross";
7889
+ const location = na?.get("location") ? String(this._evalExpr(na.get("location"))) : "abovebar";
7890
+ const color = na?.get("color") ? String(this._evalExpr(na.get("color"))) : "#FF0000";
7891
+ const text = na?.get("text") ? String(this._evalExpr(na.get("text"))) : void 0;
7892
+ const title = na?.get("title") ? String(this._evalExpr(na.get("title"))) : void 0;
7893
+ const shape = { time: this._currentBar.time, style, location, color };
7894
+ if (text !== void 0) shape.text = text;
7895
+ if (title !== void 0) shape.title = title;
7896
+ this._shapes.push(shape);
7897
+ return NaN;
7898
+ }
7899
+ _callPlotarrow(expr) {
7900
+ const args = expr.args;
7901
+ const na = expr.namedArgs;
7902
+ const value = _num(this._evalExpr(args[0]), "plotarrow()");
7903
+ if (isNaN(value) || !this._currentBar) return NaN;
7904
+ const colorUp = na?.get("colorup") ? String(this._evalExpr(na.get("colorup"))) : "#00FF00";
7905
+ const colorDown = na?.get("colordown") ? String(this._evalExpr(na.get("colordown"))) : "#FF0000";
7906
+ this._shapes.push({
7907
+ time: this._currentBar.time,
7908
+ style: value > 0 ? "arrowup" : "arrowdown",
7909
+ location: value > 0 ? "abovebar" : "belowbar",
7910
+ color: value > 0 ? colorUp : colorDown
7911
+ });
7912
+ return value;
7913
+ }
7914
+ _callHline(expr) {
7915
+ const args = expr.args;
7916
+ const na = expr.namedArgs;
7917
+ const price = _num(this._evalExpr(args[0]), "hline()");
7918
+ const title = na?.get("title") ? String(this._evalExpr(na.get("title"))) : void 0;
7919
+ const color = na?.get("color") ? String(this._evalExpr(na.get("color"))) : "#787B86";
7920
+ const lineStyle = na?.get("linestyle") ? String(this._evalExpr(na.get("linestyle"))) : "dashed";
7921
+ const lineWidth = na?.get("linewidth") ? _num(this._evalExpr(na.get("linewidth")), "hline linewidth") : 1;
7922
+ const id = `hline_${price}`;
7923
+ if (!this._hlines.some((h) => h.price === price)) {
7924
+ const hline = { id, price, color, lineStyle, lineWidth };
7925
+ if (title !== void 0) hline.title = title;
7926
+ this._hlines.push(hline);
7927
+ }
7928
+ return price;
7929
+ }
7930
+ _callFill(expr) {
7931
+ const args = expr.args;
7932
+ const na = expr.namedArgs;
7933
+ const id1 = String(this._evalExpr(args[0]));
7934
+ const id2 = String(this._evalExpr(args[1]));
7935
+ const color = na?.get("color") ? String(this._evalExpr(na.get("color"))) : args.length > 2 ? String(this._evalExpr(args[2])) : "#00000020";
7936
+ const title = na?.get("title") ? String(this._evalExpr(na.get("title"))) : void 0;
7937
+ const fill = { id1, id2, color };
7938
+ if (title !== void 0) fill.title = title;
7939
+ this._fills.push(fill);
7940
+ return NaN;
7941
+ }
7942
+ _callBgcolor(expr) {
7943
+ const args = expr.args;
7944
+ const color = String(this._evalExpr(args[0]));
7945
+ if (!this._currentBar) return NaN;
7946
+ this._bgcolors.push({ time: this._currentBar.time, color });
7947
+ return NaN;
7948
+ }
7949
+ _callBarcolor(expr) {
7950
+ const args = expr.args;
7951
+ const color = String(this._evalExpr(args[0]));
7952
+ if (!this._currentBar) return NaN;
7953
+ this._barcolors.push({ time: this._currentBar.time, color });
7954
+ return NaN;
7955
+ }
7956
+ // ─── Array namespace ──────────────────────────────────────────────────────
7957
+ _evalArrayCall(method, args) {
7958
+ switch (method) {
7959
+ case "new_float":
7960
+ case "new_int":
7961
+ case "new_bool":
7962
+ case "new_string":
7963
+ case "new": {
7964
+ const size = args.length > 0 ? Math.trunc(_num(this._evalExpr(args[0]), "array.new() size")) : 0;
7965
+ const fill = args.length > 1 ? this._evalExpr(args[1]) : NaN;
7966
+ return new TArray(Array(size).fill(fill));
7967
+ }
7968
+ case "from": {
7969
+ return new TArray(args.map((a) => this._evalExpr(a)));
7970
+ }
7971
+ case "size": {
7972
+ const arr = this._evalExpr(args[0]);
7973
+ if (!(arr instanceof TArray)) throw new TypeError("[ForgeScript] array.size() expects an array");
7974
+ return arr.size();
7975
+ }
7976
+ case "get": {
7977
+ const arr = this._evalExpr(args[0]);
7978
+ if (!(arr instanceof TArray)) throw new TypeError("[ForgeScript] array.get() expects an array");
7979
+ return arr.get(Math.trunc(_num(this._evalExpr(args[1]), "array.get() index")));
7980
+ }
7981
+ case "set": {
7982
+ const arr = this._evalExpr(args[0]);
7983
+ if (!(arr instanceof TArray)) throw new TypeError("[ForgeScript] array.set() expects an array");
7984
+ arr.set(
7985
+ Math.trunc(_num(this._evalExpr(args[1]), "array.set() index")),
7986
+ this._evalExpr(args[2])
7987
+ );
7988
+ return NaN;
7989
+ }
7990
+ case "push": {
7991
+ const arr = this._evalExpr(args[0]);
7992
+ if (!(arr instanceof TArray)) throw new TypeError("[ForgeScript] array.push() expects an array");
7993
+ arr.push(this._evalExpr(args[1]));
7994
+ return NaN;
7995
+ }
7996
+ case "pop": {
7997
+ const arr = this._evalExpr(args[0]);
7998
+ if (!(arr instanceof TArray)) throw new TypeError("[ForgeScript] array.pop() expects an array");
7999
+ return arr.pop();
8000
+ }
8001
+ case "remove": {
8002
+ const arr = this._evalExpr(args[0]);
8003
+ if (!(arr instanceof TArray)) throw new TypeError("[ForgeScript] array.remove() expects an array");
8004
+ return arr.remove(Math.trunc(_num(this._evalExpr(args[1]), "array.remove() index")));
8005
+ }
8006
+ case "clear": {
8007
+ const arr = this._evalExpr(args[0]);
8008
+ if (!(arr instanceof TArray)) throw new TypeError("[ForgeScript] array.clear() expects an array");
8009
+ arr.clear();
8010
+ return NaN;
8011
+ }
8012
+ case "includes": {
8013
+ const arr = this._evalExpr(args[0]);
8014
+ if (!(arr instanceof TArray)) throw new TypeError("[ForgeScript] array.includes() expects an array");
8015
+ return arr.includes(this._evalExpr(args[1]));
8016
+ }
8017
+ case "indexof": {
8018
+ const arr = this._evalExpr(args[0]);
8019
+ if (!(arr instanceof TArray)) throw new TypeError("[ForgeScript] array.indexof() expects an array");
8020
+ return arr.indexOf(this._evalExpr(args[1]));
8021
+ }
8022
+ case "slice": {
8023
+ const arr = this._evalExpr(args[0]);
8024
+ if (!(arr instanceof TArray)) throw new TypeError("[ForgeScript] array.slice() expects an array");
8025
+ const start = Math.trunc(_num(this._evalExpr(args[1]), "array.slice() start"));
8026
+ const end = args.length > 2 ? Math.trunc(_num(this._evalExpr(args[2]), "array.slice() end")) : void 0;
8027
+ return arr.slice(start, end);
8028
+ }
8029
+ case "join": {
8030
+ const arr = this._evalExpr(args[0]);
8031
+ if (!(arr instanceof TArray)) throw new TypeError("[ForgeScript] array.join() expects an array");
8032
+ const sep = args.length > 1 ? String(this._evalExpr(args[1])) : ",";
8033
+ return arr.join(sep);
8034
+ }
8035
+ case "sort": {
8036
+ const arr = this._evalExpr(args[0]);
8037
+ if (!(arr instanceof TArray)) throw new TypeError("[ForgeScript] array.sort() expects an array");
8038
+ arr.items.sort((a, b) => _num(a, "sort") - _num(b, "sort"));
8039
+ return NaN;
8040
+ }
8041
+ case "reverse": {
8042
+ const arr = this._evalExpr(args[0]);
8043
+ if (!(arr instanceof TArray)) throw new TypeError("[ForgeScript] array.reverse() expects an array");
8044
+ arr.items.reverse();
8045
+ return NaN;
8046
+ }
8047
+ case "avg": {
8048
+ const arr = this._evalExpr(args[0]);
8049
+ if (!(arr instanceof TArray)) throw new TypeError("[ForgeScript] array.avg() expects an array");
8050
+ if (arr.size() === 0) return NaN;
8051
+ let sum = 0;
8052
+ for (const v of arr.items) sum += _num(v, "array.avg()");
8053
+ return sum / arr.size();
8054
+ }
8055
+ case "sum": {
8056
+ const arr = this._evalExpr(args[0]);
8057
+ if (!(arr instanceof TArray)) throw new TypeError("[ForgeScript] array.sum() expects an array");
8058
+ let sum = 0;
8059
+ for (const v of arr.items) sum += _num(v, "array.sum()");
8060
+ return sum;
8061
+ }
8062
+ case "min": {
8063
+ const arr = this._evalExpr(args[0]);
8064
+ if (!(arr instanceof TArray)) throw new TypeError("[ForgeScript] array.min() expects an array");
8065
+ if (arr.size() === 0) return NaN;
8066
+ let m = Infinity;
8067
+ for (const v of arr.items) m = Math.min(m, _num(v, "array.min()"));
8068
+ return m;
8069
+ }
8070
+ case "max": {
8071
+ const arr = this._evalExpr(args[0]);
8072
+ if (!(arr instanceof TArray)) throw new TypeError("[ForgeScript] array.max() expects an array");
8073
+ if (arr.size() === 0) return NaN;
8074
+ let m = -Infinity;
8075
+ for (const v of arr.items) m = Math.max(m, _num(v, "array.max()"));
8076
+ return m;
8077
+ }
8078
+ default:
8079
+ throw new ReferenceError(`[ForgeScript] Unknown array function 'array.${method}'`);
8080
+ }
8081
+ }
8082
+ // ─── Input namespace (input.text_area, input.string) ─────────────────────
8083
+ _evalInputNsCall(fn, args) {
8084
+ if (args.length === 0) return fn === "text_area" || fn === "string" ? "" : 0;
8085
+ const def = this._evalExpr(args[0]);
8086
+ if (fn === "text_area" || fn === "string") return typeof def === "string" ? def : String(def);
8087
+ if (typeof def === "boolean") return def;
8088
+ if (typeof def === "string") {
8089
+ const n = parseFloat(def);
8090
+ return isNaN(n) ? def : n;
8091
+ }
8092
+ return _num(def, `input.${fn}()`);
8093
+ }
8094
+ // ─── Table namespace ────────────────────────────────────────────────────────
8095
+ _tableCounter = 0;
8096
+ _evalTableCall(method, args, namedArgs) {
8097
+ switch (method) {
8098
+ case "new": {
8099
+ const position = args.length > 0 ? String(this._evalExpr(args[0])) : "top_right";
8100
+ const cols = args.length > 1 ? Math.trunc(_num(this._evalExpr(args[1]), "table.new() columns")) : 1;
8101
+ const rows = args.length > 2 ? Math.trunc(_num(this._evalExpr(args[2]), "table.new() rows")) : 1;
8102
+ const bgColor = args.length > 3 ? String(this._evalExpr(args[3])) : namedArgs?.get("bgcolor") ? String(this._evalExpr(namedArgs.get("bgcolor"))) : "transparent";
8103
+ const borderColor = args.length > 4 ? String(this._evalExpr(args[4])) : namedArgs?.get("border_color") ? String(this._evalExpr(namedArgs.get("border_color"))) : "transparent";
8104
+ const borderWidth = namedArgs?.get("border_width") ? _num(this._evalExpr(namedArgs.get("border_width")), "table.new() border_width") : 0;
8105
+ const frameColor = namedArgs?.get("frame_color") ? String(this._evalExpr(namedArgs.get("frame_color"))) : args.length > 5 ? String(this._evalExpr(args[5])) : "transparent";
8106
+ const frameWidth = namedArgs?.get("frame_width") ? _num(this._evalExpr(namedArgs.get("frame_width")), "table.new() frame_width") : args.length > 6 ? Math.trunc(_num(this._evalExpr(args[6]), "table.new() frame_width")) : 0;
8107
+ const id = `__table_${this._tableCounter++}`;
8108
+ this._tables.push({ id, position, rows, cols, cells: [], bgColor, borderColor, borderWidth, frameColor, frameWidth });
8109
+ return id;
8110
+ }
8111
+ case "cell": {
8112
+ const tableId = String(this._evalExpr(args[0]));
8113
+ const col = Math.trunc(_num(this._evalExpr(args[1]), "table.cell() column"));
8114
+ const row = Math.trunc(_num(this._evalExpr(args[2]), "table.cell() row"));
8115
+ const text = args.length > 3 ? String(this._evalExpr(args[3])) : namedArgs?.get("text") ? String(this._evalExpr(namedArgs.get("text"))) : "";
8116
+ const textColor = namedArgs?.get("text_color") ? String(this._evalExpr(namedArgs.get("text_color"))) : "#FFFFFF";
8117
+ const bgColor = namedArgs?.get("bgcolor") ? String(this._evalExpr(namedArgs.get("bgcolor"))) : "transparent";
8118
+ const textSize = namedArgs?.get("text_size") ? String(this._evalExpr(namedArgs.get("text_size"))) : "normal";
8119
+ const textHAlign = namedArgs?.get("text_halign") ? String(this._evalExpr(namedArgs.get("text_halign"))) : "center";
8120
+ const textVAlign = namedArgs?.get("text_valign") ? String(this._evalExpr(namedArgs.get("text_valign"))) : "center";
8121
+ const table = this._tables.find((t) => t.id === tableId);
8122
+ if (table) {
8123
+ const existing = table.cells.findIndex((c) => c.row === row && c.col === col);
8124
+ const cell = { row, col, text, textColor, bgColor, textSize, textHAlign, textVAlign };
8125
+ if (existing >= 0) table.cells[existing] = cell;
8126
+ else table.cells.push(cell);
8127
+ }
8128
+ return NaN;
8129
+ }
8130
+ case "clear": {
8131
+ const tableId = String(this._evalExpr(args[0]));
8132
+ const col = Math.trunc(_num(this._evalExpr(args[1]), "table.clear() column"));
8133
+ const row = Math.trunc(_num(this._evalExpr(args[2]), "table.clear() row"));
8134
+ const table = this._tables.find((t) => t.id === tableId);
8135
+ if (table) {
8136
+ const idx = table.cells.findIndex((c) => c.row === row && c.col === col);
8137
+ if (idx >= 0) table.cells.splice(idx, 1);
8138
+ }
8139
+ return NaN;
8140
+ }
8141
+ case "delete": {
8142
+ const tableId = String(this._evalExpr(args[0]));
8143
+ const idx = this._tables.findIndex((t) => t.id === tableId);
8144
+ if (idx >= 0) this._tables.splice(idx, 1);
8145
+ return NaN;
8146
+ }
8147
+ default:
8148
+ throw new ReferenceError(`[ForgeScript] Unknown table function 'table.${method}'`);
8149
+ }
8150
+ }
8151
+ // ─── User-defined function calls ────────────────────────────────────────────
8152
+ _callUserFunction(fnDecl, callArgs) {
8153
+ const savedParams = fnDecl.params.map((p) => this._scope.get(p));
8154
+ for (let i = 0; i < fnDecl.params.length; i++) {
8155
+ const val = i < callArgs.length ? this._evalExpr(callArgs[i]) : NaN;
8156
+ this._scope.set(fnDecl.params[i], val);
8157
+ }
8158
+ let result = NaN;
8159
+ for (const stmt of fnDecl.body) {
8160
+ if (stmt.kind === "ExprStmt") {
8161
+ result = this._evalExpr(stmt.expr);
8162
+ } else {
8163
+ this._execStmt(stmt);
8164
+ }
8165
+ }
8166
+ for (let i = 0; i < fnDecl.params.length; i++) {
8167
+ const saved = savedParams[i];
8168
+ if (saved === void 0) this._scope.delete(fnDecl.params[i]);
8169
+ else this._scope.set(fnDecl.params[i], saved);
8170
+ }
8171
+ return result;
8172
+ }
6630
8173
  // ─── Internal state helpers ──────────────────────────────────────────────────
6631
8174
  /** Persistent scalar state for stateful TA functions (EMA, RMA, ATR). */
6632
8175
  _internalState = /* @__PURE__ */ new Map();
@@ -6653,15 +8196,24 @@ var TScriptRuntime = class {
6653
8196
  }
6654
8197
  };
6655
8198
 
6656
- // src/tscript/TScriptIndicator.ts
6657
- var TScriptIndicator = class {
8199
+ // src/forgescript/ForgeScriptIndicator.ts
8200
+ var ForgeScriptIndicator = class _ForgeScriptIndicator {
6658
8201
  _runtime;
6659
8202
  /** Title from the `indicator("...")` declaration, or `'Script'` if absent. */
6660
8203
  title;
6661
8204
  constructor(src) {
6662
- this._runtime = new TScriptRuntime(src);
8205
+ this._runtime = new ForgeScriptRuntime(_ForgeScriptIndicator._dedent(src));
6663
8206
  this.title = this._runtime.title;
6664
8207
  }
8208
+ /** Strip common leading whitespace from template-literal source. */
8209
+ static _dedent(s) {
8210
+ const lines = s.split("\n");
8211
+ while (lines.length && !lines[0].trim()) lines.shift();
8212
+ while (lines.length && !lines[lines.length - 1].trim()) lines.pop();
8213
+ const min = lines.filter((l) => l.trim().length > 0).reduce((m, l) => Math.min(m, l.search(/\S/)), Infinity);
8214
+ if (!isFinite(min) || min === 0) return lines.join("\n");
8215
+ return lines.map((l) => l.slice(min)).join("\n");
8216
+ }
6665
8217
  /**
6666
8218
  * Execute the script over the full bar history.
6667
8219
  *
@@ -6674,6 +8226,25 @@ var TScriptIndicator = class {
6674
8226
  run(bars) {
6675
8227
  return this._runtime.run(bars);
6676
8228
  }
8229
+ /**
8230
+ * Return the full ScriptResult from the most recent `run()` or `runFull()`.
8231
+ * Must be called after `run()` — otherwise returns empty data.
8232
+ */
8233
+ getLastResult(bars) {
8234
+ return this._runtime.getResult(bars);
8235
+ }
8236
+ /**
8237
+ * Execute the script and return the full result including shapes, hlines,
8238
+ * fills, bgcolors, barcolors, and plot metadata.
8239
+ */
8240
+ runFull(bars) {
8241
+ this._runtime.reset();
8242
+ this._runtime.setTotalBars(bars.length);
8243
+ for (let i = 0; i < bars.length; i++) {
8244
+ this._runtime.execBar(bars[i], i);
8245
+ }
8246
+ return this._runtime.getResult(bars);
8247
+ }
6677
8248
  /**
6678
8249
  * Reset all persistent series state (ring buffers) without re-parsing.
6679
8250
  * Call when the chart symbol or timeframe changes.
@@ -6892,10 +8463,11 @@ var IndicatorDAG = class {
6892
8463
  const src = node.config.script;
6893
8464
  if (!src) return { kind: "series", points: [] };
6894
8465
  if (!node.scriptRuntime) {
6895
- node.scriptRuntime = new TScriptIndicator(src);
8466
+ node.scriptRuntime = new ForgeScriptIndicator(src);
6896
8467
  }
6897
8468
  const plots = node.scriptRuntime.run(bars);
6898
- return { kind: "series", points: plots[0] ?? [] };
8469
+ const scriptResult = node.scriptRuntime.getLastResult(bars);
8470
+ return { kind: "series", points: plots[0] ?? [], scriptResult };
6899
8471
  }
6900
8472
  default:
6901
8473
  return { kind: "series", points: [...input] };
@@ -6952,6 +8524,12 @@ var DEMONSTRATION_RADIUS = 44;
6952
8524
  var DEMONSTRATION_FILL = "rgba(255, 200, 50, 0.18)";
6953
8525
  var DEMONSTRATION_STROKE = "rgba(255, 200, 50, 0.7)";
6954
8526
  var DEMONSTRATION_COLOR = "var(--crosshair-overlay, rgba(255,255,255,0.75))";
8527
+
8528
+ // src/react/assets/logo_dark.png
8529
+ var logo_dark_default = "../logo_dark-B2KCSRPJ.png";
8530
+
8531
+ // src/react/assets/logo_light.png
8532
+ var logo_light_default = "../logo_light-NAWNBY4G.png";
6955
8533
  function _scriptTitle(src) {
6956
8534
  const m = src.match(/indicator\s*\(\s*["']([^"']+)["']/);
6957
8535
  return m ? m[1] : "Script";
@@ -7039,7 +8617,7 @@ function IndicatorLabel({
7039
8617
  height: 18,
7040
8618
  borderRadius: 3,
7041
8619
  overflow: "hidden",
7042
- background: "rgba(0,0,0,0.5)",
8620
+ background: "transparent",
7043
8621
  cursor: "default",
7044
8622
  userSelect: "none"
7045
8623
  },
@@ -8290,15 +9868,28 @@ function FibGannGroup({
8290
9868
  function FavoritesFloatingToolbar({
8291
9869
  favorites,
8292
9870
  activeTool,
8293
- onSelectTool
9871
+ onSelectTool,
9872
+ containerRef
8294
9873
  }) {
8295
9874
  const [pos, setPos] = useState({ x: 60, y: 60 });
8296
9875
  const [dragging, setDragging] = useState(false);
8297
9876
  const dragOffset = useRef({ x: 0, y: 0 });
9877
+ const toolbarRef = useRef(null);
8298
9878
  useEffect(() => {
8299
9879
  if (!dragging) return;
8300
9880
  const onMove = (e) => {
8301
- setPos({ x: e.clientX - dragOffset.current.x, y: e.clientY - dragOffset.current.y });
9881
+ let x = e.clientX - dragOffset.current.x;
9882
+ let y = e.clientY - dragOffset.current.y;
9883
+ const container = containerRef?.current;
9884
+ const toolbar = toolbarRef.current;
9885
+ if (container && toolbar) {
9886
+ const cr = container.getBoundingClientRect();
9887
+ const tw = toolbar.offsetWidth;
9888
+ const th = toolbar.offsetHeight;
9889
+ x = Math.max(cr.left, Math.min(cr.right - tw, x));
9890
+ y = Math.max(cr.top, Math.min(cr.bottom - th, y));
9891
+ }
9892
+ setPos({ x, y });
8302
9893
  };
8303
9894
  const onUp = () => setDragging(false);
8304
9895
  window.addEventListener("mousemove", onMove);
@@ -8307,11 +9898,11 @@ function FavoritesFloatingToolbar({
8307
9898
  window.removeEventListener("mousemove", onMove);
8308
9899
  window.removeEventListener("mouseup", onUp);
8309
9900
  };
8310
- }, [dragging]);
9901
+ }, [dragging, containerRef]);
8311
9902
  const validFavs = favorites.filter((f) => ALL_DRAWING_ITEM_MAP.has(f));
8312
9903
  if (validFavs.length === 0) return null;
8313
9904
  return ReactDOM.createPortal(
8314
- /* @__PURE__ */ jsxs("div", { className: "dt-favorites-toolbar", style: { left: pos.x, top: pos.y }, children: [
9905
+ /* @__PURE__ */ jsxs("div", { ref: toolbarRef, className: "dt-favorites-toolbar", style: { left: pos.x, top: pos.y }, children: [
8315
9906
  /* @__PURE__ */ jsx(
8316
9907
  "div",
8317
9908
  {
@@ -8420,8 +10011,151 @@ function VisibilityTool({
8420
10011
  )) })
8421
10012
  ] });
8422
10013
  }
8423
- function LeftToolbar({ activeTool, onSelectTool, drawingFavorites, onDrawingFavoritesChange, onVisibilityAction, visibilityActiveAction, onVisibilityDeactivate, onLinkClick, onDeleteClick }) {
10014
+ function LeftToolbar({ activeTool, onSelectTool, drawingFavorites, onDrawingFavoritesChange, onVisibilityAction, visibilityActiveAction, onVisibilityDeactivate, onLinkClick, onDeleteClick, onLogout, user, getAuthToken, apiUrl, containerRef }) {
8424
10015
  const [favorites, setFavorites] = useState(drawingFavorites ?? []);
10016
+ const [profileOpen, setProfileOpen] = useState(false);
10017
+ const [profileTab, setProfileTab] = useState("profile");
10018
+ const [emailInput, setEmailInput] = useState("");
10019
+ const [currentPwd, setCurrentPwd] = useState("");
10020
+ const [newPwd, setNewPwd] = useState("");
10021
+ const [confirmPwd, setConfirmPwd] = useState("");
10022
+ const [emailMsg, setEmailMsg] = useState(null);
10023
+ const [pwdMsg, setPwdMsg] = useState(null);
10024
+ const [emailBusy, setEmailBusy] = useState(false);
10025
+ const [pwdBusy, setPwdBusy] = useState(false);
10026
+ const [plans, setPlans] = useState([]);
10027
+ const [plansLoading, setPlansLoading] = useState(false);
10028
+ const [subscribeMsg, setSubscribeMsg] = useState(null);
10029
+ const fetchPlans = useCallback(async () => {
10030
+ if (plansLoading) return;
10031
+ setPlansLoading(true);
10032
+ try {
10033
+ const base = apiUrl ?? "";
10034
+ const headers = {};
10035
+ if (getAuthToken) {
10036
+ try {
10037
+ const tok = await getAuthToken();
10038
+ if (tok) headers["Authorization"] = `Bearer ${tok}`;
10039
+ } catch {
10040
+ }
10041
+ }
10042
+ const res = await fetch(`${base}/api/subscriptions`, { headers });
10043
+ if (res.ok) setPlans((await res.json()).filter((p) => p.active));
10044
+ } catch {
10045
+ } finally {
10046
+ setPlansLoading(false);
10047
+ }
10048
+ }, [apiUrl, getAuthToken, plansLoading]);
10049
+ const handleOpenProfile = useCallback(() => {
10050
+ setEmailInput(user?.email ?? "");
10051
+ setEmailMsg(null);
10052
+ setPwdMsg(null);
10053
+ setCurrentPwd("");
10054
+ setNewPwd("");
10055
+ setConfirmPwd("");
10056
+ setProfileTab("profile");
10057
+ setProfileOpen(true);
10058
+ }, [user]);
10059
+ const handleSubscriptionTab = useCallback(() => {
10060
+ setProfileTab("subscription");
10061
+ setSubscribeMsg(null);
10062
+ void fetchPlans();
10063
+ }, [fetchPlans]);
10064
+ const handleEmailUpdate = useCallback(async () => {
10065
+ if (!emailInput.trim()) return;
10066
+ setEmailBusy(true);
10067
+ setEmailMsg(null);
10068
+ try {
10069
+ const base = apiUrl ?? "";
10070
+ const headers = { "Content-Type": "application/json" };
10071
+ if (getAuthToken) {
10072
+ try {
10073
+ const tok = await getAuthToken();
10074
+ if (tok) headers["Authorization"] = `Bearer ${tok}`;
10075
+ } catch {
10076
+ }
10077
+ }
10078
+ const res = await fetch(`${base}/api/auth/update-email`, {
10079
+ method: "PATCH",
10080
+ headers,
10081
+ body: JSON.stringify({ email: emailInput.trim() })
10082
+ });
10083
+ if (res.ok) {
10084
+ setEmailMsg({ type: "success", text: "Email updated successfully." });
10085
+ } else {
10086
+ const d = await res.json().catch(() => ({}));
10087
+ setEmailMsg({ type: "error", text: d.error ?? "Failed to update email." });
10088
+ }
10089
+ } catch {
10090
+ setEmailMsg({ type: "error", text: "Network error. Please try again." });
10091
+ } finally {
10092
+ setEmailBusy(false);
10093
+ }
10094
+ }, [apiUrl, getAuthToken, emailInput]);
10095
+ const handlePasswordUpdate = useCallback(async () => {
10096
+ if (newPwd !== confirmPwd) {
10097
+ setPwdMsg({ type: "error", text: "New passwords do not match." });
10098
+ return;
10099
+ }
10100
+ if (newPwd.length < 8) {
10101
+ setPwdMsg({ type: "error", text: "Password must be at least 8 characters." });
10102
+ return;
10103
+ }
10104
+ setPwdBusy(true);
10105
+ setPwdMsg(null);
10106
+ try {
10107
+ const base = apiUrl ?? "";
10108
+ const headers = { "Content-Type": "application/json" };
10109
+ if (getAuthToken) {
10110
+ try {
10111
+ const tok = await getAuthToken();
10112
+ if (tok) headers["Authorization"] = `Bearer ${tok}`;
10113
+ } catch {
10114
+ }
10115
+ }
10116
+ const res = await fetch(`${base}/api/auth/update-password`, {
10117
+ method: "PATCH",
10118
+ headers,
10119
+ body: JSON.stringify({ currentPassword: currentPwd, newPassword: newPwd })
10120
+ });
10121
+ if (res.ok) {
10122
+ setPwdMsg({ type: "success", text: "Password updated successfully." });
10123
+ setCurrentPwd("");
10124
+ setNewPwd("");
10125
+ setConfirmPwd("");
10126
+ } else {
10127
+ const d = await res.json().catch(() => ({}));
10128
+ setPwdMsg({ type: "error", text: d.error ?? "Failed to update password." });
10129
+ }
10130
+ } catch {
10131
+ setPwdMsg({ type: "error", text: "Network error. Please try again." });
10132
+ } finally {
10133
+ setPwdBusy(false);
10134
+ }
10135
+ }, [apiUrl, getAuthToken, currentPwd, newPwd, confirmPwd]);
10136
+ const handleSubscribe = useCallback(async (planId) => {
10137
+ setSubscribeMsg(null);
10138
+ try {
10139
+ const base = apiUrl ?? "";
10140
+ const headers = { "Content-Type": "application/json" };
10141
+ if (getAuthToken) {
10142
+ try {
10143
+ const tok = await getAuthToken();
10144
+ if (tok) headers["Authorization"] = `Bearer ${tok}`;
10145
+ } catch {
10146
+ }
10147
+ }
10148
+ const res = await fetch(`${base}/api/subscriptions/${planId}/checkout`, { method: "POST", headers });
10149
+ if (res.ok) {
10150
+ setSubscribeMsg({ id: planId, type: "success", text: "Subscribed successfully." });
10151
+ } else {
10152
+ const d = await res.json().catch(() => ({}));
10153
+ setSubscribeMsg({ id: planId, type: "error", text: d.error ?? "Failed to subscribe." });
10154
+ }
10155
+ } catch {
10156
+ setSubscribeMsg({ id: planId, type: "error", text: "Network error. Please try again." });
10157
+ }
10158
+ }, [apiUrl, getAuthToken]);
8425
10159
  const handleFavoritesChange = (favs) => {
8426
10160
  setFavorites(favs);
8427
10161
  onDrawingFavoritesChange?.(favs);
@@ -8440,7 +10174,8 @@ function LeftToolbar({ activeTool, onSelectTool, drawingFavorites, onDrawingFavo
8440
10174
  borderRadius: 8,
8441
10175
  width: 40,
8442
10176
  flexShrink: 0,
8443
- userSelect: "none"
10177
+ userSelect: "none",
10178
+ height: "100%"
8444
10179
  },
8445
10180
  children: [
8446
10181
  /* @__PURE__ */ jsx(PointerGroup, { activeTool, onSelectTool }),
@@ -8521,6 +10256,29 @@ function LeftToolbar({ activeTool, onSelectTool, drawingFavorites, onDrawingFavo
8521
10256
  /* @__PURE__ */ jsx("line", { x1: "9", y1: "6.5", x2: "9", y2: "11.5" })
8522
10257
  ] })
8523
10258
  }
10259
+ ),
10260
+ /* @__PURE__ */ jsx("div", { style: { flex: 1 } }),
10261
+ (user || onLogout) && /* @__PURE__ */ jsx(
10262
+ "button",
10263
+ {
10264
+ className: "drawing-tool-btn profile-avatar-btn",
10265
+ title: "Profile",
10266
+ onClick: handleOpenProfile,
10267
+ children: (user?.username?.[0] ?? user?.email?.[0] ?? "?").toUpperCase()
10268
+ }
10269
+ ),
10270
+ onLogout && /* @__PURE__ */ jsx(
10271
+ "button",
10272
+ {
10273
+ className: "drawing-tool-btn",
10274
+ title: "Log out",
10275
+ onClick: onLogout,
10276
+ children: /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 16 16", width: "15", height: "15", stroke: "currentColor", fill: "none", strokeWidth: "1.6", strokeLinecap: "round", strokeLinejoin: "round", children: [
10277
+ /* @__PURE__ */ jsx("path", { d: "M6 2H3a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h3" }),
10278
+ /* @__PURE__ */ jsx("polyline", { points: "11 11 14 8 11 5" }),
10279
+ /* @__PURE__ */ jsx("line", { x1: "14", y1: "8", x2: "6", y2: "8" })
10280
+ ] })
10281
+ }
8524
10282
  )
8525
10283
  ]
8526
10284
  }
@@ -8530,8 +10288,177 @@ function LeftToolbar({ activeTool, onSelectTool, drawingFavorites, onDrawingFavo
8530
10288
  {
8531
10289
  favorites,
8532
10290
  activeTool,
8533
- onSelectTool
10291
+ onSelectTool,
10292
+ containerRef
8534
10293
  }
10294
+ ),
10295
+ profileOpen && ReactDOM.createPortal(
10296
+ /* @__PURE__ */ jsx("div", { className: "profile-drawer-overlay", onClick: () => setProfileOpen(false), children: /* @__PURE__ */ jsxs("div", { className: "profile-drawer", onClick: (e) => e.stopPropagation(), children: [
10297
+ /* @__PURE__ */ jsxs("div", { className: "profile-drawer-header", children: [
10298
+ /* @__PURE__ */ jsx("span", { children: "Account" }),
10299
+ /* @__PURE__ */ jsx(
10300
+ "button",
10301
+ {
10302
+ className: "profile-drawer-close-btn",
10303
+ onClick: () => setProfileOpen(false),
10304
+ title: "Close",
10305
+ children: /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 14 14", width: "12", height: "12", stroke: "currentColor", fill: "none", strokeWidth: "2", strokeLinecap: "round", children: [
10306
+ /* @__PURE__ */ jsx("line", { x1: "2", y1: "2", x2: "12", y2: "12" }),
10307
+ /* @__PURE__ */ jsx("line", { x1: "12", y1: "2", x2: "2", y2: "12" })
10308
+ ] })
10309
+ }
10310
+ )
10311
+ ] }),
10312
+ /* @__PURE__ */ jsxs("div", { className: "profile-drawer-tabs", children: [
10313
+ /* @__PURE__ */ jsx(
10314
+ "button",
10315
+ {
10316
+ className: `profile-drawer-tab-btn${profileTab === "profile" ? " active" : ""}`,
10317
+ onClick: () => setProfileTab("profile"),
10318
+ children: "Profile"
10319
+ }
10320
+ ),
10321
+ /* @__PURE__ */ jsx(
10322
+ "button",
10323
+ {
10324
+ className: `profile-drawer-tab-btn${profileTab === "subscription" ? " active" : ""}`,
10325
+ onClick: handleSubscriptionTab,
10326
+ children: "Subscription"
10327
+ }
10328
+ )
10329
+ ] }),
10330
+ /* @__PURE__ */ jsxs("div", { className: "profile-drawer-tab-content", children: [
10331
+ profileTab === "profile" && /* @__PURE__ */ jsxs(Fragment, { children: [
10332
+ /* @__PURE__ */ jsxs("div", { className: "profile-info-section", children: [
10333
+ /* @__PURE__ */ jsx("div", { className: "profile-drawer-avatar", children: (user?.username?.[0] ?? user?.email?.[0] ?? "?").toUpperCase() }),
10334
+ user?.username && /* @__PURE__ */ jsx("div", { className: "profile-drawer-name", children: user.username }),
10335
+ user?.email && /* @__PURE__ */ jsx("div", { className: "profile-drawer-email", children: user.email }),
10336
+ user?.role && /* @__PURE__ */ jsx("div", { className: "profile-drawer-role", style: { marginTop: 10 }, children: /* @__PURE__ */ jsx("span", { className: "profile-drawer-role-badge", children: user.role }) })
10337
+ ] }),
10338
+ /* @__PURE__ */ jsxs("div", { className: "profile-form-section", children: [
10339
+ /* @__PURE__ */ jsx("h3", { children: "Change Email" }),
10340
+ /* @__PURE__ */ jsxs("div", { className: "profile-form-group", children: [
10341
+ /* @__PURE__ */ jsx("label", { className: "profile-form-label", children: "New email address" }),
10342
+ /* @__PURE__ */ jsx(
10343
+ "input",
10344
+ {
10345
+ className: "profile-form-input",
10346
+ type: "email",
10347
+ value: emailInput,
10348
+ onChange: (e) => setEmailInput(e.target.value),
10349
+ placeholder: "you@example.com",
10350
+ autoComplete: "email"
10351
+ }
10352
+ )
10353
+ ] }),
10354
+ /* @__PURE__ */ jsx(
10355
+ "button",
10356
+ {
10357
+ className: "profile-form-submit",
10358
+ disabled: emailBusy || !emailInput.trim(),
10359
+ onClick: () => void handleEmailUpdate(),
10360
+ children: emailBusy ? "Updating\u2026" : "Update Email"
10361
+ }
10362
+ ),
10363
+ emailMsg && /* @__PURE__ */ jsx("div", { className: `profile-form-msg ${emailMsg.type}`, children: emailMsg.text })
10364
+ ] }),
10365
+ /* @__PURE__ */ jsxs("div", { className: "profile-form-section", children: [
10366
+ /* @__PURE__ */ jsx("h3", { children: "Change Password" }),
10367
+ /* @__PURE__ */ jsxs("div", { className: "profile-form-group", children: [
10368
+ /* @__PURE__ */ jsx("label", { className: "profile-form-label", children: "Current password" }),
10369
+ /* @__PURE__ */ jsx(
10370
+ "input",
10371
+ {
10372
+ className: "profile-form-input",
10373
+ type: "password",
10374
+ value: currentPwd,
10375
+ onChange: (e) => setCurrentPwd(e.target.value),
10376
+ autoComplete: "current-password"
10377
+ }
10378
+ )
10379
+ ] }),
10380
+ /* @__PURE__ */ jsxs("div", { className: "profile-form-group", children: [
10381
+ /* @__PURE__ */ jsx("label", { className: "profile-form-label", children: "New password" }),
10382
+ /* @__PURE__ */ jsx(
10383
+ "input",
10384
+ {
10385
+ className: "profile-form-input",
10386
+ type: "password",
10387
+ value: newPwd,
10388
+ onChange: (e) => setNewPwd(e.target.value),
10389
+ autoComplete: "new-password"
10390
+ }
10391
+ )
10392
+ ] }),
10393
+ /* @__PURE__ */ jsxs("div", { className: "profile-form-group", children: [
10394
+ /* @__PURE__ */ jsx("label", { className: "profile-form-label", children: "Confirm new password" }),
10395
+ /* @__PURE__ */ jsx(
10396
+ "input",
10397
+ {
10398
+ className: "profile-form-input",
10399
+ type: "password",
10400
+ value: confirmPwd,
10401
+ onChange: (e) => setConfirmPwd(e.target.value),
10402
+ autoComplete: "new-password"
10403
+ }
10404
+ )
10405
+ ] }),
10406
+ /* @__PURE__ */ jsx(
10407
+ "button",
10408
+ {
10409
+ className: "profile-form-submit",
10410
+ disabled: pwdBusy || !currentPwd || !newPwd || !confirmPwd,
10411
+ onClick: () => void handlePasswordUpdate(),
10412
+ children: pwdBusy ? "Updating\u2026" : "Update Password"
10413
+ }
10414
+ ),
10415
+ pwdMsg && /* @__PURE__ */ jsx("div", { className: `profile-form-msg ${pwdMsg.type}`, children: pwdMsg.text })
10416
+ ] })
10417
+ ] }),
10418
+ profileTab === "subscription" && /* @__PURE__ */ jsxs(Fragment, { children: [
10419
+ plansLoading && /* @__PURE__ */ jsx("p", { className: "profile-subs-empty", children: "Loading plans\u2026" }),
10420
+ !plansLoading && plans.length === 0 && /* @__PURE__ */ jsx("p", { className: "profile-subs-empty", children: "No subscription plans available." }),
10421
+ !plansLoading && plans.length > 0 && /* @__PURE__ */ jsx("div", { className: "profile-subs-list", children: plans.map((plan) => /* @__PURE__ */ jsxs("div", { className: "profile-sub-card", children: [
10422
+ /* @__PURE__ */ jsx("div", { className: "profile-sub-card-name", children: plan.name }),
10423
+ plan.description && /* @__PURE__ */ jsx("div", { className: "profile-sub-card-desc", children: plan.description }),
10424
+ /* @__PURE__ */ jsxs("div", { className: "profile-sub-card-price", children: [
10425
+ "$",
10426
+ plan.priceMonthly.toFixed(2),
10427
+ " / month"
10428
+ ] }),
10429
+ plan.permissions.length > 0 && /* @__PURE__ */ jsx("div", { className: "profile-sub-card-perms", children: plan.permissions.map((p) => /* @__PURE__ */ jsx("span", { className: "profile-sub-perm-badge", children: p }, p)) }),
10430
+ /* @__PURE__ */ jsx("div", { className: "profile-sub-card-footer", children: /* @__PURE__ */ jsx(
10431
+ "button",
10432
+ {
10433
+ className: "profile-sub-subscribe-btn",
10434
+ onClick: () => void handleSubscribe(plan.id),
10435
+ children: "Subscribe"
10436
+ }
10437
+ ) }),
10438
+ subscribeMsg?.id === plan.id && /* @__PURE__ */ jsx("div", { className: `profile-form-msg ${subscribeMsg.type}`, children: subscribeMsg.text })
10439
+ ] }, plan.id)) })
10440
+ ] })
10441
+ ] }),
10442
+ onLogout && /* @__PURE__ */ jsx("div", { className: "profile-drawer-footer", children: /* @__PURE__ */ jsxs(
10443
+ "button",
10444
+ {
10445
+ className: "profile-drawer-logout-btn",
10446
+ onClick: () => {
10447
+ setProfileOpen(false);
10448
+ onLogout();
10449
+ },
10450
+ children: [
10451
+ /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 16 16", width: "14", height: "14", stroke: "currentColor", fill: "none", strokeWidth: "1.6", strokeLinecap: "round", strokeLinejoin: "round", children: [
10452
+ /* @__PURE__ */ jsx("path", { d: "M6 2H3a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h3" }),
10453
+ /* @__PURE__ */ jsx("polyline", { points: "11 11 14 8 11 5" }),
10454
+ /* @__PURE__ */ jsx("line", { x1: "14", y1: "8", x2: "6", y2: "8" })
10455
+ ] }),
10456
+ "Log out"
10457
+ ]
10458
+ }
10459
+ ) })
10460
+ ] }) }),
10461
+ document.body
8535
10462
  )
8536
10463
  ] });
8537
10464
  }
@@ -8679,11 +10606,13 @@ function ChartSettingsDialog({ initialSettings, onApply, onClose }) {
8679
10606
  ] }) });
8680
10607
  }
8681
10608
  var CROSSHAIR_COLOR = DEMONSTRATION_COLOR;
8682
- function PointerOverlay({ mode, width, height }) {
10609
+ function PointerOverlay({ mode, width, height, snapX }) {
8683
10610
  const svgRef = useRef(null);
8684
10611
  const hGroupRef = useRef(null);
8685
10612
  const vGroupRef = useRef(null);
8686
10613
  const cursorGroup = useRef(null);
10614
+ const snapXRef = useRef(snapX);
10615
+ snapXRef.current = snapX;
8687
10616
  useEffect(() => {
8688
10617
  const svg = svgRef.current;
8689
10618
  if (!svg) return;
@@ -8701,10 +10630,19 @@ function PointerOverlay({ mode, width, height }) {
8701
10630
  svg.style.opacity = "0";
8702
10631
  return;
8703
10632
  }
10633
+ const target = e.target;
10634
+ if (target && target.tagName === "CANVAS" && target.style.cursor) {
10635
+ const c = target.style.cursor;
10636
+ if (c.includes("resize")) {
10637
+ svg.style.opacity = "0";
10638
+ return;
10639
+ }
10640
+ }
8704
10641
  svg.style.opacity = "1";
10642
+ const sx = snapXRef.current ? snapXRef.current(x) : x;
8705
10643
  if (hGroupRef.current) hGroupRef.current.style.transform = `translateY(${y}px)`;
8706
- if (vGroupRef.current) vGroupRef.current.style.transform = `translateX(${x}px)`;
8707
- if (cursorGroup.current) cursorGroup.current.style.transform = `translate(${x}px,${y}px)`;
10644
+ if (vGroupRef.current) vGroupRef.current.style.transform = `translateX(${sx}px)`;
10645
+ if (cursorGroup.current) cursorGroup.current.style.transform = `translate(${sx}px,${y}px)`;
8708
10646
  };
8709
10647
  const hide = () => {
8710
10648
  svg.style.opacity = "0";
@@ -8729,7 +10667,7 @@ function PointerOverlay({ mode, width, height }) {
8729
10667
  top: 0,
8730
10668
  left: 0,
8731
10669
  pointerEvents: "none",
8732
- zIndex: 5,
10670
+ zIndex: 4,
8733
10671
  opacity: 0,
8734
10672
  overflow: "visible",
8735
10673
  willChange: "opacity"
@@ -8775,27 +10713,97 @@ function PointerOverlay({ mode, width, height }) {
8775
10713
  }
8776
10714
  );
8777
10715
  }
8778
- var OVERLAY_COLORS = ["#2196f3", "#4caf50", "#ff9800", "#e040fb", "#00bcd4", "#f44336"];
8779
- var _TF_SECONDS2 = {
8780
- "1s": 1,
8781
- "5s": 5,
8782
- "10s": 10,
8783
- "30s": 30,
8784
- "1m": 60,
8785
- "3m": 180,
8786
- "5m": 300,
8787
- "15m": 900,
8788
- "30m": 1800,
8789
- "1h": 3600,
8790
- "2h": 7200,
8791
- "4h": 14400,
8792
- "6h": 21600,
8793
- "12h": 43200,
8794
- "1d": 86400,
8795
- "3d": 259200,
8796
- "1w": 604800,
8797
- "1M": 2592e3
10716
+ var POS_STYLES = {
10717
+ top_left: { top: 8, left: 8 },
10718
+ top_center: { top: 8, left: "50%", transform: "translateX(-50%)" },
10719
+ top_right: { top: 8, right: 78 },
10720
+ // offset for price axis
10721
+ middle_left: { top: "50%", left: 8, transform: "translateY(-50%)" },
10722
+ middle_center: { top: "50%", left: "50%", transform: "translate(-50%, -50%)" },
10723
+ middle_right: { top: "50%", right: 78, transform: "translateY(-50%)" },
10724
+ bottom_left: { bottom: 8, left: 8 },
10725
+ bottom_center: { bottom: 8, left: "50%", transform: "translateX(-50%)" },
10726
+ bottom_right: { bottom: 8, right: 78 }
10727
+ };
10728
+ var SIZE_MAP = {
10729
+ tiny: 9,
10730
+ small: 10,
10731
+ normal: 11,
10732
+ large: 14,
10733
+ huge: 18,
10734
+ auto: 11
10735
+ };
10736
+ var HALIGN_MAP = {
10737
+ left: "left",
10738
+ center: "center",
10739
+ right: "right"
10740
+ };
10741
+ var VALIGN_MAP = {
10742
+ top: "top",
10743
+ center: "middle",
10744
+ bottom: "bottom"
8798
10745
  };
10746
+ function TableOverlay({ tables }) {
10747
+ if (!tables || tables.length === 0) return null;
10748
+ return /* @__PURE__ */ jsx(Fragment, { children: tables.map((table) => {
10749
+ const posStyle = POS_STYLES[table.position] ?? POS_STYLES["top_right"];
10750
+ const grid = [];
10751
+ for (let r = 0; r < table.rows; r++) {
10752
+ const row = [];
10753
+ for (let c = 0; c < table.cols; c++) {
10754
+ row.push(table.cells.find((cell) => cell.row === r && cell.col === c) ?? null);
10755
+ }
10756
+ grid.push(row);
10757
+ }
10758
+ const frameStyle = table.frameWidth > 0 ? `${table.frameWidth}px solid ${table.frameColor}` : void 0;
10759
+ const cellBorderStyle = table.borderWidth > 0 ? `${table.borderWidth}px solid ${table.borderColor}` : void 0;
10760
+ return /* @__PURE__ */ jsx(
10761
+ "div",
10762
+ {
10763
+ style: {
10764
+ position: "absolute",
10765
+ ...posStyle,
10766
+ zIndex: 15,
10767
+ pointerEvents: "none"
10768
+ },
10769
+ children: /* @__PURE__ */ jsx(
10770
+ "table",
10771
+ {
10772
+ style: {
10773
+ borderCollapse: "collapse",
10774
+ background: table.bgColor === "transparent" ? "rgba(19,23,34,0.85)" : table.bgColor,
10775
+ border: frameStyle ?? (cellBorderStyle ?? "none"),
10776
+ borderRadius: 3,
10777
+ overflow: "hidden"
10778
+ },
10779
+ children: /* @__PURE__ */ jsx("tbody", { children: grid.map((row, rIdx) => /* @__PURE__ */ jsx("tr", { children: row.map((cell, cIdx) => /* @__PURE__ */ jsx(
10780
+ "td",
10781
+ {
10782
+ style: {
10783
+ padding: "2px 6px",
10784
+ fontSize: SIZE_MAP[cell?.textSize ?? "normal"] ?? 11,
10785
+ fontFamily: "-apple-system, BlinkMacSystemFont, sans-serif",
10786
+ color: cell?.textColor ?? "#d1d4dc",
10787
+ background: cell?.bgColor === "transparent" ? "transparent" : cell?.bgColor ?? "transparent",
10788
+ whiteSpace: "nowrap",
10789
+ textAlign: HALIGN_MAP[cell?.textHAlign ?? "center"] ?? "center",
10790
+ verticalAlign: VALIGN_MAP[cell?.textVAlign ?? "center"] ?? "middle",
10791
+ lineHeight: 1.4,
10792
+ border: cellBorderStyle
10793
+ },
10794
+ children: cell?.text ?? ""
10795
+ },
10796
+ cIdx
10797
+ )) }, rIdx)) })
10798
+ }
10799
+ )
10800
+ },
10801
+ table.id
10802
+ );
10803
+ }) });
10804
+ }
10805
+ var OVERLAY_COLORS = ["#2196f3", "#4caf50", "#ff9800", "#e040fb", "#00bcd4", "#f44336"];
10806
+ var _TF_SECONDS = (tf) => parseTfSeconds(tf) ?? 3600;
8799
10807
  function _isLightBg(hex) {
8800
10808
  const c = hex.replace("#", "");
8801
10809
  if (c.length < 6) return false;
@@ -8844,6 +10852,11 @@ function _fmtPrice(v) {
8844
10852
  const dec = abs >= 100 ? 2 : abs >= 1 ? 2 : abs >= 1e-4 ? 4 : 6;
8845
10853
  return v.toLocaleString("en-US", { minimumFractionDigits: dec, maximumFractionDigits: dec });
8846
10854
  }
10855
+ function _fmtVol(v) {
10856
+ if (v >= 1e6) return `${(v / 1e6).toFixed(2)}M`;
10857
+ if (v >= 1e3) return `${(v / 1e3).toFixed(1)}K`;
10858
+ return v.toFixed(0);
10859
+ }
8847
10860
  function PricePaneHeader({
8848
10861
  symbol,
8849
10862
  timeframe,
@@ -8865,6 +10878,17 @@ function PricePaneHeader({
8865
10878
  const muted = theme === "dark" ? "rgba(255,255,255,0.42)" : "rgba(0,0,0,0.38)";
8866
10879
  const textColor = theme === "dark" ? "rgba(255,255,255,0.82)" : "rgba(0,0,0,0.82)";
8867
10880
  const overlayInds = indicators.filter((ind) => ind.config.overlay === true);
10881
+ const dailyVolume = (() => {
10882
+ if (!lastBar) return 0;
10883
+ const lastDate = new Date(lastBar.time * 1e3);
10884
+ const dayStart = new Date(lastDate.getFullYear(), lastDate.getMonth(), lastDate.getDate()).getTime() / 1e3;
10885
+ let total = 0;
10886
+ for (let i = bars.length - 1; i >= 0; i--) {
10887
+ if (bars[i].time < dayStart) break;
10888
+ total += bars[i].volume;
10889
+ }
10890
+ return total;
10891
+ })();
8868
10892
  return /* @__PURE__ */ jsxs(
8869
10893
  "div",
8870
10894
  {
@@ -8932,6 +10956,11 @@ function PricePaneHeader({
8932
10956
  isUp ? "+" : "",
8933
10957
  pctChange.toFixed(2),
8934
10958
  "%)"
10959
+ ] }),
10960
+ /* @__PURE__ */ jsxs("span", { children: [
10961
+ /* @__PURE__ */ jsx("span", { style: { color: muted }, children: "V" }),
10962
+ /* @__PURE__ */ jsx("span", { style: { color: muted }, children: " " }),
10963
+ /* @__PURE__ */ jsx("span", { style: { color: textColor }, children: _fmtVol(dailyVolume) })
8935
10964
  ] })
8936
10965
  ] })
8937
10966
  ] }),
@@ -8988,7 +11017,15 @@ var ChartCanvas = forwardRef(
8988
11017
  priceFraction: priceFractionProp,
8989
11018
  onPriceFractionChange,
8990
11019
  getExchangeLogoUrl,
8991
- tradingBridge
11020
+ tradingBridge,
11021
+ onLogout,
11022
+ user,
11023
+ getAuthToken,
11024
+ apiUrl,
11025
+ initialActiveTool,
11026
+ onPointerToolChange,
11027
+ drawingFavorites: drawingFavoritesProp,
11028
+ onDrawingFavoritesChange
8992
11029
  }, ref) {
8993
11030
  const outerRef = useRef(null);
8994
11031
  const priceRef = useRef(null);
@@ -9002,7 +11039,8 @@ var ChartCanvas = forwardRef(
9002
11039
  const [panes, setPanes] = useState([]);
9003
11040
  const [indicators, setIndicators] = useState([]);
9004
11041
  const [totalHeight, setTotalHeight] = useState(600);
9005
- const [activeTool, setActiveTool] = useState("cursor");
11042
+ const [activeTool, setActiveTool] = useState(initialActiveTool ?? "cursor");
11043
+ const [drawingFavorites, setDrawingFavorites] = useState(drawingFavoritesProp ?? []);
9006
11044
  const [ctxMenu, setCtxMenu] = useState(null);
9007
11045
  const [chartCtxMenu, setChartCtxMenu] = useState(null);
9008
11046
  const [settingsOpen, setSettingsOpen] = useState(false);
@@ -9019,6 +11057,8 @@ var ChartCanvas = forwardRef(
9019
11057
  );
9020
11058
  const drawOverlayRef = useRef(null);
9021
11059
  const [priceFraction, setPriceFraction] = useState(priceFractionProp ?? 0.6);
11060
+ const selectPointerToolRef = useRef(() => {
11061
+ });
9022
11062
  useImperativeHandle(ref, () => ({
9023
11063
  getLayoutSnapshot: () => chartRef.current?.saveLayout() ?? null,
9024
11064
  loadLayoutSnapshot: (layout) => {
@@ -9026,6 +11066,7 @@ var ChartCanvas = forwardRef(
9026
11066
  },
9027
11067
  addIndicator: (config) => chartRef.current?.addIndicator(config) ?? null,
9028
11068
  getOverlayStore: () => chartRef.current?.getTradingOverlayStore() ?? null,
11069
+ selectPointerTool: (tool) => selectPointerToolRef.current(tool),
9029
11070
  captureScreenshot: async () => {
9030
11071
  const container = outerRef.current;
9031
11072
  if (!container) return null;
@@ -9082,7 +11123,7 @@ var ChartCanvas = forwardRef(
9082
11123
  chart.on("dataLoaded", ({ interval: loadedInterval }) => {
9083
11124
  setLoading(false);
9084
11125
  syncChartState();
9085
- const tfSec = _TF_SECONDS2[loadedInterval] ?? 3600;
11126
+ const tfSec = _TF_SECONDS(loadedInterval);
9086
11127
  const savedSpan = layoutToRestore?.viewport ? layoutToRestore.viewport.timeRange.to - layoutToRestore.viewport.timeRange.from : 0;
9087
11128
  const hasValidViewport = !!layoutToRestore?.viewport && layoutToRestore.interval === loadedInterval && savedSpan >= tfSec * 50;
9088
11129
  if (hasValidViewport) {
@@ -9126,6 +11167,9 @@ var ChartCanvas = forwardRef(
9126
11167
  }
9127
11168
  }
9128
11169
  chartRef.current = chart;
11170
+ const initTool = initialActiveTool ?? "cursor";
11171
+ chart.setCrosshairEnabled(initTool === "crosshair");
11172
+ chart.setNativeCursorHidden(initTool !== "cursor");
9129
11173
  syncChartState();
9130
11174
  return () => {
9131
11175
  chart.destroy();
@@ -9175,14 +11219,19 @@ var ChartCanvas = forwardRef(
9175
11219
  if (!ctx) return;
9176
11220
  ctx.clearRect(0, 0, physW, physH);
9177
11221
  const overlayInds = indicators.filter((ind) => ind.config.overlay === true);
9178
- if (overlayInds.length === 0) return;
11222
+ if (overlayInds.length === 0) {
11223
+ chartRef.current?.setIndicatorLines([]);
11224
+ return;
11225
+ }
9179
11226
  ctx.save();
9180
11227
  ctx.scale(dpr, dpr);
9181
11228
  const plotW = transform.plotWidth;
9182
11229
  let colorIdx = 0;
11230
+ const indicatorLines = [];
9183
11231
  for (const ind of overlayInds) {
9184
11232
  const result = computedResults.get(ind.id);
9185
11233
  if (!result || result.kind !== "series" || result.points.length === 0) continue;
11234
+ indicatorLines.push({ id: ind.id, points: result.points });
9186
11235
  const color = ind.config.color ?? OVERLAY_COLORS[colorIdx++ % OVERLAY_COLORS.length];
9187
11236
  ctx.strokeStyle = color;
9188
11237
  ctx.lineWidth = 1.5;
@@ -9204,6 +11253,7 @@ var ChartCanvas = forwardRef(
9204
11253
  }
9205
11254
  ctx.stroke();
9206
11255
  }
11256
+ chartRef.current?.setIndicatorLines(indicatorLines);
9207
11257
  ctx.restore();
9208
11258
  };
9209
11259
  drawOverlayRef.current();
@@ -9316,11 +11366,15 @@ var ChartCanvas = forwardRef(
9316
11366
  chart.cancelDrawingTool();
9317
11367
  chart.setCrosshairEnabled(tool === "crosshair");
9318
11368
  chart.setNativeCursorHidden(tool !== "cursor");
11369
+ onPointerToolChange?.(tool);
9319
11370
  } else {
9320
11371
  chart.setCrosshairEnabled(false);
9321
11372
  chart.startDrawingTool(tool);
9322
11373
  }
9323
- }, []);
11374
+ }, [onPointerToolChange]);
11375
+ const handleToolSelectRef = useRef(handleToolSelect);
11376
+ handleToolSelectRef.current = handleToolSelect;
11377
+ selectPointerToolRef.current = handleToolSelect;
9324
11378
  const handlePriceDividerDrag = useCallback(
9325
11379
  (dy) => {
9326
11380
  if (totalHeight === 0) return;
@@ -9359,7 +11413,7 @@ var ChartCanvas = forwardRef(
9359
11413
  width: "100%",
9360
11414
  height: "100%",
9361
11415
  display: "flex",
9362
- gap: 8,
11416
+ gap: 4,
9363
11417
  overflow: "hidden"
9364
11418
  },
9365
11419
  children: [
@@ -9371,13 +11425,23 @@ var ChartCanvas = forwardRef(
9371
11425
  onVisibilityAction: handleVisibilityAction,
9372
11426
  visibilityActiveAction,
9373
11427
  onVisibilityDeactivate: handleVisibilityDeactivate,
9374
- onDeleteClick: () => chartRef.current?.deleteSelectedDrawing()
11428
+ onDeleteClick: () => chartRef.current?.deleteSelectedDrawing(),
11429
+ drawingFavorites,
11430
+ onDrawingFavoritesChange: (favs) => {
11431
+ setDrawingFavorites(favs);
11432
+ onDrawingFavoritesChange?.(favs);
11433
+ },
11434
+ containerRef: outerRef,
11435
+ ...onLogout ? { onLogout } : {},
11436
+ ...user ? { user } : {},
11437
+ ...getAuthToken ? { getAuthToken } : {},
11438
+ ...apiUrl ? { apiUrl } : {}
9375
11439
  }
9376
11440
  ),
9377
11441
  /* @__PURE__ */ jsxs(
9378
11442
  "div",
9379
11443
  {
9380
- className: activeTool === "crosshair" ? "chart-crosshair-mode" : void 0,
11444
+ className: activeTool === "crosshair" || activeTool === "dot" || activeTool === "demonstration" ? "chart-crosshair-mode" : void 0,
9381
11445
  style: {
9382
11446
  flex: 1,
9383
11447
  display: "flex",
@@ -9396,13 +11460,41 @@ var ChartCanvas = forwardRef(
9396
11460
  crosshairXRef.current = null;
9397
11461
  },
9398
11462
  children: [
11463
+ /* @__PURE__ */ jsx(
11464
+ "a",
11465
+ {
11466
+ href: "https://forgecharts.com",
11467
+ target: "_blank",
11468
+ rel: "noopener noreferrer",
11469
+ style: {
11470
+ position: "absolute",
11471
+ bottom: 38,
11472
+ left: 38,
11473
+ zIndex: 5,
11474
+ lineHeight: 0
11475
+ },
11476
+ children: /* @__PURE__ */ jsx(
11477
+ "img",
11478
+ {
11479
+ src: theme === "light" ? logo_light_default : logo_dark_default,
11480
+ alt: "ForgeCharts",
11481
+ style: {
11482
+ height: 48,
11483
+ width: "auto",
11484
+ opacity: 0.55,
11485
+ userSelect: "none"
11486
+ }
11487
+ }
11488
+ )
11489
+ }
11490
+ ),
9399
11491
  /* @__PURE__ */ jsxs(
9400
11492
  "div",
9401
11493
  {
9402
11494
  style: { position: "relative", height: `${priceH}px`, flexShrink: 0 },
9403
11495
  onContextMenu: handleCanvasContextMenu,
9404
11496
  children: [
9405
- /* @__PURE__ */ jsx("div", { ref: priceRef, style: { width: "100%", height: "100%" } }),
11497
+ /* @__PURE__ */ jsx("div", { ref: priceRef, style: { width: "100%", height: "100%", filter: loading ? "blur(4px)" : "none", transition: "filter 0.3s ease", willChange: "filter" } }),
9406
11498
  /* @__PURE__ */ jsx(
9407
11499
  "canvas",
9408
11500
  {
@@ -9424,10 +11516,15 @@ var ChartCanvas = forwardRef(
9424
11516
  {
9425
11517
  mode: activeTool,
9426
11518
  width: priceRef.current?.clientWidth ?? 0,
9427
- height: priceH
11519
+ height: priceH,
11520
+ snapX: (x) => chartRef.current?.snapXToBar(x).x ?? x
9428
11521
  }
9429
11522
  ),
9430
11523
  " ",
11524
+ (() => {
11525
+ const allTables = Array.from(computedResults.values()).filter((r) => r.kind === "series" && !!r.scriptResult?.tables?.length).flatMap((r) => r.scriptResult.tables);
11526
+ return allTables.length > 0 ? /* @__PURE__ */ jsx(TableOverlay, { tables: allTables }) : null;
11527
+ })(),
9431
11528
  /* @__PURE__ */ jsx(
9432
11529
  PricePaneHeader,
9433
11530
  {
@@ -9445,22 +11542,7 @@ var ChartCanvas = forwardRef(
9445
11542
  ]
9446
11543
  }
9447
11544
  ),
9448
- loading && /* @__PURE__ */ jsx(
9449
- "div",
9450
- {
9451
- style: {
9452
- position: "absolute",
9453
- top: priceH / 2 - 12,
9454
- left: "50%",
9455
- transform: "translateX(-50%)",
9456
- color: "var(--text-muted, #787b86)",
9457
- fontSize: 13,
9458
- pointerEvents: "none",
9459
- zIndex: 10
9460
- },
9461
- children: "Loading\u2026"
9462
- }
9463
- ),
11545
+ loading && /* @__PURE__ */ jsx(CandleLoadingSpinner, {}),
9464
11546
  error && /* @__PURE__ */ jsx(
9465
11547
  "div",
9466
11548
  {
@@ -9577,10 +11659,86 @@ var ChartCanvas = forwardRef(
9577
11659
  );
9578
11660
  }
9579
11661
  );
11662
+ function CandleLoadingSpinner() {
11663
+ return /* @__PURE__ */ jsx(
11664
+ "div",
11665
+ {
11666
+ style: {
11667
+ position: "absolute",
11668
+ top: 0,
11669
+ left: 0,
11670
+ right: 0,
11671
+ bottom: 0,
11672
+ display: "flex",
11673
+ alignItems: "center",
11674
+ justifyContent: "center",
11675
+ pointerEvents: "none",
11676
+ zIndex: 10
11677
+ },
11678
+ children: /* @__PURE__ */ jsxs("svg", { width: "56", height: "112", viewBox: "0 0 28 56", fill: "none", xmlns: "http://www.w3.org/2000/svg", children: [
11679
+ /* @__PURE__ */ jsx("style", { children: `
11680
+ @keyframes fc-wick-grow {
11681
+ 0% { transform: scaleY(0); opacity: 0; }
11682
+ 20% { transform: scaleY(1); opacity: 1; }
11683
+ 80% { transform: scaleY(1); opacity: 1; }
11684
+ 100% { transform: scaleY(0); opacity: 0; }
11685
+ }
11686
+ @keyframes fc-body-grow {
11687
+ 0% { transform: scaleY(0); opacity: 0; }
11688
+ 15% { transform: scaleY(0); opacity: 0; }
11689
+ 55% { transform: scaleY(1); opacity: 1; }
11690
+ 80% { transform: scaleY(1); opacity: 1; }
11691
+ 100% { transform: scaleY(0); opacity: 0; }
11692
+ }
11693
+ .fc-wick {
11694
+ transform-box: fill-box;
11695
+ transform-origin: bottom center;
11696
+ animation: fc-wick-grow 1.6s cubic-bezier(0.4,0,0.2,1) infinite;
11697
+ }
11698
+ .fc-body {
11699
+ transform-box: fill-box;
11700
+ transform-origin: bottom center;
11701
+ animation: fc-body-grow 1.6s cubic-bezier(0.4,0,0.2,1) infinite;
11702
+ }
11703
+ ` }),
11704
+ /* @__PURE__ */ jsx("rect", { className: "fc-wick", x: "13", y: "44", width: "2", height: "10", rx: "1", fill: "#26a69a" }),
11705
+ /* @__PURE__ */ jsx("rect", { className: "fc-body", x: "6", y: "18", width: "16", height: "28", rx: "2", fill: "#26a69a" }),
11706
+ /* @__PURE__ */ jsx("rect", { className: "fc-wick", x: "13", y: "4", width: "2", height: "14", rx: "1", fill: "#26a69a" })
11707
+ ] })
11708
+ }
11709
+ );
11710
+ }
9580
11711
  function TabBar({ tabs, activeId, onSelect, onAdd, onClose, onRename }) {
9581
11712
  const [editingId, setEditingId] = useState(null);
9582
11713
  const [editValue, setEditValue] = useState("");
9583
11714
  const inputRef = useRef(null);
11715
+ const scrollRef = useRef(null);
11716
+ const [canScrollLeft, setCanScrollLeft] = useState(false);
11717
+ const [canScrollRight, setCanScrollRight] = useState(false);
11718
+ const updateArrows = useCallback(() => {
11719
+ const el = scrollRef.current;
11720
+ if (!el) return;
11721
+ setCanScrollLeft(el.scrollLeft > 0);
11722
+ setCanScrollRight(el.scrollLeft + el.clientWidth < el.scrollWidth - 1);
11723
+ }, []);
11724
+ useEffect(() => {
11725
+ updateArrows();
11726
+ const el = scrollRef.current;
11727
+ if (!el) return;
11728
+ const ro = new ResizeObserver(updateArrows);
11729
+ ro.observe(el);
11730
+ return () => ro.disconnect();
11731
+ }, [tabs, updateArrows]);
11732
+ useEffect(() => {
11733
+ const el = scrollRef.current;
11734
+ if (!el) return;
11735
+ const activeEl = el.querySelector(".tab-item.active");
11736
+ activeEl?.scrollIntoView({ block: "nearest", inline: "nearest" });
11737
+ requestAnimationFrame(updateArrows);
11738
+ }, [activeId, updateArrows]);
11739
+ function scrollTabs(dir) {
11740
+ scrollRef.current?.scrollBy({ left: dir * 150, behavior: "smooth" });
11741
+ }
9584
11742
  function startEdit(tab, e) {
9585
11743
  e.stopPropagation();
9586
11744
  setEditingId(tab.id);
@@ -9594,49 +11752,77 @@ function TabBar({ tabs, activeId, onSelect, onAdd, onClose, onRename }) {
9594
11752
  setEditingId(null);
9595
11753
  }
9596
11754
  return /* @__PURE__ */ jsxs("div", { className: "tab-bar", children: [
9597
- tabs.map((tab) => /* @__PURE__ */ jsxs(
11755
+ /* @__PURE__ */ jsx(
11756
+ "button",
11757
+ {
11758
+ className: `tab-scroll-btn tab-scroll-left${canScrollLeft ? " visible" : ""}`,
11759
+ onClick: () => scrollTabs(-1),
11760
+ tabIndex: -1,
11761
+ title: "Scroll tabs left",
11762
+ children: /* @__PURE__ */ jsx("svg", { viewBox: "0 0 10 14", width: "8", height: "12", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ jsx("polyline", { points: "8,2 2,7 8,12" }) })
11763
+ }
11764
+ ),
11765
+ /* @__PURE__ */ jsx(
9598
11766
  "div",
9599
11767
  {
9600
- className: `tab-item${tab.id === activeId ? " active" : ""}`,
9601
- onClick: () => onSelect(tab.id),
9602
- onDoubleClick: (e) => startEdit(tab, e),
9603
- title: "Double-click to rename",
9604
- children: [
9605
- editingId === tab.id ? /* @__PURE__ */ jsx(
9606
- "input",
9607
- {
9608
- ref: inputRef,
9609
- className: "tab-rename-input",
9610
- value: editValue,
9611
- onChange: (e) => setEditValue(e.target.value),
9612
- onBlur: commitEdit,
9613
- onKeyDown: (e) => {
9614
- if (e.key === "Enter") commitEdit();
9615
- if (e.key === "Escape") setEditingId(null);
9616
- },
9617
- onClick: (e) => e.stopPropagation(),
9618
- autoFocus: true
9619
- }
9620
- ) : /* @__PURE__ */ jsxs("span", { className: "tab-label", children: [
9621
- tab.label,
9622
- tab.isSaved && /* @__PURE__ */ jsx("span", { className: "tab-saved-dot", title: "Saved layout" })
9623
- ] }),
9624
- tabs.length > 1 && /* @__PURE__ */ jsx(
9625
- "button",
9626
- {
9627
- className: "tab-close",
9628
- title: "Close tab",
9629
- onClick: (e) => {
9630
- e.stopPropagation();
9631
- onClose(tab.id);
9632
- },
9633
- children: "\xD7"
9634
- }
9635
- )
9636
- ]
9637
- },
9638
- tab.id
9639
- )),
11768
+ className: "tab-bar-scroll",
11769
+ ref: scrollRef,
11770
+ onScroll: updateArrows,
11771
+ children: tabs.map((tab) => /* @__PURE__ */ jsxs(
11772
+ "div",
11773
+ {
11774
+ className: `tab-item${tab.id === activeId ? " active" : ""}`,
11775
+ onClick: () => onSelect(tab.id),
11776
+ onDoubleClick: (e) => startEdit(tab, e),
11777
+ title: "Double-click to rename",
11778
+ children: [
11779
+ editingId === tab.id ? /* @__PURE__ */ jsx(
11780
+ "input",
11781
+ {
11782
+ ref: inputRef,
11783
+ className: "tab-rename-input",
11784
+ value: editValue,
11785
+ onChange: (e) => setEditValue(e.target.value),
11786
+ onBlur: commitEdit,
11787
+ onKeyDown: (e) => {
11788
+ if (e.key === "Enter") commitEdit();
11789
+ if (e.key === "Escape") setEditingId(null);
11790
+ },
11791
+ onClick: (e) => e.stopPropagation(),
11792
+ autoFocus: true
11793
+ }
11794
+ ) : /* @__PURE__ */ jsxs("span", { className: "tab-label", children: [
11795
+ tab.label,
11796
+ tab.isSaved && /* @__PURE__ */ jsx("span", { className: "tab-saved-dot", title: "Saved layout" })
11797
+ ] }),
11798
+ tabs.length > 1 && /* @__PURE__ */ jsx(
11799
+ "button",
11800
+ {
11801
+ className: "tab-close",
11802
+ title: "Close tab",
11803
+ onClick: (e) => {
11804
+ e.stopPropagation();
11805
+ onClose(tab.id);
11806
+ },
11807
+ children: "\xD7"
11808
+ }
11809
+ )
11810
+ ]
11811
+ },
11812
+ tab.id
11813
+ ))
11814
+ }
11815
+ ),
11816
+ /* @__PURE__ */ jsx(
11817
+ "button",
11818
+ {
11819
+ className: `tab-scroll-btn tab-scroll-right${canScrollRight ? " visible" : ""}`,
11820
+ onClick: () => scrollTabs(1),
11821
+ tabIndex: -1,
11822
+ title: "Scroll tabs right",
11823
+ children: /* @__PURE__ */ jsx("svg", { viewBox: "0 0 10 14", width: "8", height: "12", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ jsx("polyline", { points: "2,2 8,7 2,12" }) })
11824
+ }
11825
+ ),
9640
11826
  /* @__PURE__ */ jsx("button", { className: "tab-add", onClick: onAdd, title: "New chart tab", children: "+" })
9641
11827
  ] });
9642
11828
  }
@@ -9983,7 +12169,7 @@ function SymbolSearchDialog({ current, onSelect, onClose, symbolResolver }) {
9983
12169
  const [searchHasMore, setSearchHasMore] = useState(false);
9984
12170
  const inputRef = useRef(null);
9985
12171
  const dialogRef = useRef(null);
9986
- const sentinelRef = useRef(null);
12172
+ const observerRef = useRef(null);
9987
12173
  const searchTimer = useRef(null);
9988
12174
  const queryRef = useRef(query);
9989
12175
  const activeTabRef = useRef(activeTab);
@@ -10058,9 +12244,12 @@ function SymbolSearchDialog({ current, onSelect, onClose, symbolResolver }) {
10058
12244
  const loadMoreSearchRef = useRef(loadMoreSearch);
10059
12245
  loadMoreTabRef.current = loadMoreTab;
10060
12246
  loadMoreSearchRef.current = loadMoreSearch;
10061
- useEffect(() => {
10062
- const sentinel = sentinelRef.current;
10063
- if (!sentinel) return;
12247
+ const observeSentinel = useCallback((node) => {
12248
+ if (observerRef.current) {
12249
+ observerRef.current.disconnect();
12250
+ observerRef.current = null;
12251
+ }
12252
+ if (!node) return;
10064
12253
  const observer = new IntersectionObserver((entries) => {
10065
12254
  if (entries[0]?.isIntersecting) {
10066
12255
  if (queryRef.current.trim()) {
@@ -10070,8 +12259,8 @@ function SymbolSearchDialog({ current, onSelect, onClose, symbolResolver }) {
10070
12259
  }
10071
12260
  }
10072
12261
  }, { threshold: 0.1 });
10073
- observer.observe(sentinel);
10074
- return () => observer.disconnect();
12262
+ observer.observe(node);
12263
+ observerRef.current = observer;
10075
12264
  }, []);
10076
12265
  const doSearch = useCallback((q, tab) => {
10077
12266
  if (searchTimer.current) clearTimeout(searchTimer.current);
@@ -10191,12 +12380,12 @@ function SymbolSearchDialog({ current, onSelect, onClose, symbolResolver }) {
10191
12380
  return /* @__PURE__ */ jsxs("div", { className: "sym-list", children: [
10192
12381
  displayList.map(renderRow),
10193
12382
  renderDirectRow(),
10194
- /* @__PURE__ */ jsx("div", { ref: sentinelRef, className: "sym-sentinel" }),
12383
+ /* @__PURE__ */ jsx("div", { ref: observeSentinel, className: "sym-sentinel" }),
10195
12384
  loadingMore && /* @__PURE__ */ jsx("div", { className: "sym-loading-more", children: "Loading more\u2026" })
10196
12385
  ] });
10197
12386
  return /* @__PURE__ */ jsxs("div", { className: "sym-list", children: [
10198
12387
  displayList.map(renderRow),
10199
- /* @__PURE__ */ jsx("div", { ref: sentinelRef, className: "sym-sentinel" }),
12388
+ /* @__PURE__ */ jsx("div", { ref: observeSentinel, className: "sym-sentinel" }),
10200
12389
  loadingMore && /* @__PURE__ */ jsx("div", { className: "sym-loading-more", children: "Loading more\u2026" })
10201
12390
  ] });
10202
12391
  };
@@ -10521,6 +12710,7 @@ function TimeframeDropdown({
10521
12710
  onSelect,
10522
12711
  onToggleFavorite,
10523
12712
  onAddCustom,
12713
+ onRemoveCustom,
10524
12714
  onClose
10525
12715
  }) {
10526
12716
  const [collapsed, setCollapsed] = useState({});
@@ -10628,7 +12818,7 @@ function TimeframeDropdown({
10628
12818
  ]
10629
12819
  }
10630
12820
  ),
10631
- !collapsed["Custom"] && customTimeframes.map((tf) => /* @__PURE__ */ jsx(
12821
+ !collapsed["Custom"] && customTimeframes.map((tf) => /* @__PURE__ */ jsxs(
10632
12822
  "button",
10633
12823
  {
10634
12824
  className: "tf-dropdown-row",
@@ -10636,7 +12826,21 @@ function TimeframeDropdown({
10636
12826
  onSelect(tf);
10637
12827
  onClose();
10638
12828
  },
10639
- children: /* @__PURE__ */ jsx("span", { children: tf })
12829
+ children: [
12830
+ /* @__PURE__ */ jsx("span", { children: tf }),
12831
+ /* @__PURE__ */ jsx(
12832
+ "button",
12833
+ {
12834
+ className: "tf-star-btn",
12835
+ title: "Remove custom interval",
12836
+ onClick: (e) => {
12837
+ e.stopPropagation();
12838
+ onRemoveCustom(tf);
12839
+ },
12840
+ children: /* @__PURE__ */ jsx("span", { style: { fontSize: 12, lineHeight: 1 }, children: "\u2715" })
12841
+ }
12842
+ )
12843
+ ]
10640
12844
  },
10641
12845
  tf
10642
12846
  ))
@@ -10652,6 +12856,7 @@ function TopToolbar({
10652
12856
  onSymbolChange,
10653
12857
  onTimeframeChange,
10654
12858
  onAddCustomTimeframe,
12859
+ onRemoveCustomTimeframe,
10655
12860
  onFavoritesChange,
10656
12861
  onAddIndicator,
10657
12862
  onToggleTheme,
@@ -10702,8 +12907,6 @@ function TopToolbar({
10702
12907
  flexShrink: 0
10703
12908
  },
10704
12909
  children: [
10705
- /* @__PURE__ */ jsx("span", { style: { fontWeight: 700, letterSpacing: "-0.5px", color: "var(--accent)" }, children: "ForgeCharts" }),
10706
- /* @__PURE__ */ jsx("div", { className: "toolbar-sep" }),
10707
12910
  /* @__PURE__ */ jsxs(
10708
12911
  "button",
10709
12912
  {
@@ -10752,15 +12955,14 @@ function TopToolbar({
10752
12955
  },
10753
12956
  tf
10754
12957
  )),
10755
- !favorites.includes(timeframe) && timeframe && /* @__PURE__ */ jsx("button", { className: "active", style: { fontStyle: "italic" }, children: timeframe }),
10756
12958
  /* @__PURE__ */ jsx(
10757
12959
  "button",
10758
12960
  {
10759
- className: tfOpen ? "active" : void 0,
12961
+ className: tfOpen || !favorites.includes(timeframe) ? "active" : void 0,
10760
12962
  onClick: () => setTfOpen((o) => !o),
10761
12963
  title: "More timeframes",
10762
12964
  style: { fontSize: 11, padding: "4px 7px" },
10763
- children: "\u25BE"
12965
+ children: !favorites.includes(timeframe) ? `${timeframe} \u25BE` : "\u25BE"
10764
12966
  }
10765
12967
  ),
10766
12968
  tfOpen && /* @__PURE__ */ jsx(
@@ -10775,6 +12977,7 @@ function TopToolbar({
10775
12977
  onFavoritesChange(next);
10776
12978
  },
10777
12979
  onAddCustom: onAddCustomTimeframe,
12980
+ onRemoveCustom: onRemoveCustomTimeframe,
10778
12981
  onClose: () => setTfOpen(false)
10779
12982
  }
10780
12983
  )
@@ -10904,6 +13107,11 @@ function TopToolbar({
10904
13107
  }
10905
13108
  );
10906
13109
  }
13110
+ var OrderEntryIcon = () => /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 16 16", width: "16", height: "16", fill: "none", stroke: "currentColor", strokeWidth: "1.4", strokeLinecap: "round", strokeLinejoin: "round", children: [
13111
+ /* @__PURE__ */ jsx("rect", { x: "2", y: "2", width: "12", height: "12", rx: "1.5" }),
13112
+ /* @__PURE__ */ jsx("line", { x1: "5", y1: "8", x2: "11", y2: "8" }),
13113
+ /* @__PURE__ */ jsx("line", { x1: "8", y1: "5", x2: "8", y2: "11" })
13114
+ ] });
10907
13115
  var WatchListIcon = () => /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 16 16", width: "16", height: "16", stroke: "currentColor", fill: "none", strokeWidth: "1.5", children: [
10908
13116
  /* @__PURE__ */ jsx("rect", { x: "2", y: "3", width: "12", height: "1.5", rx: "0.5", fill: "currentColor", stroke: "none" }),
10909
13117
  /* @__PURE__ */ jsx("rect", { x: "2", y: "7", width: "9", height: "1.5", rx: "0.5", fill: "currentColor", stroke: "none" }),
@@ -10912,8 +13120,8 @@ var WatchListIcon = () => /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 16 16", wi
10912
13120
  /* @__PURE__ */ jsx("line", { x1: "13", y1: "10.75", x2: "13", y2: "12.75", strokeWidth: "1.2" }),
10913
13121
  /* @__PURE__ */ jsx("line", { x1: "12", y1: "11.75", x2: "14", y2: "11.75", strokeWidth: "1.2" })
10914
13122
  ] });
10915
- function RightToolbar({ watchlistOpen, onToggleWatchlist }) {
10916
- return /* @__PURE__ */ jsx(
13123
+ function RightToolbar({ watchlistOpen, onToggleWatchlist, orderEntryOpen, onToggleOrderEntry, showOrderEntry }) {
13124
+ return /* @__PURE__ */ jsxs(
10917
13125
  "div",
10918
13126
  {
10919
13127
  style: {
@@ -10928,15 +13136,26 @@ function RightToolbar({ watchlistOpen, onToggleWatchlist }) {
10928
13136
  flexShrink: 0,
10929
13137
  userSelect: "none"
10930
13138
  },
10931
- children: /* @__PURE__ */ jsx(
10932
- "button",
10933
- {
10934
- title: "Watch List",
10935
- className: `drawing-tool-btn${watchlistOpen ? " is-active" : ""}`,
10936
- onClick: onToggleWatchlist,
10937
- children: /* @__PURE__ */ jsx(WatchListIcon, {})
10938
- }
10939
- )
13139
+ children: [
13140
+ showOrderEntry !== false && /* @__PURE__ */ jsx(
13141
+ "button",
13142
+ {
13143
+ title: "Order Entry",
13144
+ className: `drawing-tool-btn${orderEntryOpen ? " is-active" : ""}`,
13145
+ onClick: onToggleOrderEntry,
13146
+ children: /* @__PURE__ */ jsx(OrderEntryIcon, {})
13147
+ }
13148
+ ),
13149
+ /* @__PURE__ */ jsx(
13150
+ "button",
13151
+ {
13152
+ title: "Watch List",
13153
+ className: `drawing-tool-btn${watchlistOpen ? " is-active" : ""}`,
13154
+ onClick: onToggleWatchlist,
13155
+ children: /* @__PURE__ */ jsx(WatchListIcon, {})
13156
+ }
13157
+ )
13158
+ ]
10940
13159
  }
10941
13160
  );
10942
13161
  }
@@ -11108,14 +13327,19 @@ function symbolSupportsSession(symbol) {
11108
13327
  function useClockTime(timezone) {
11109
13328
  const [time, setTime] = useState(() => _formatClock(timezone));
11110
13329
  useEffect(() => {
11111
- setTime(_formatClock(timezone));
11112
- const id = setInterval(() => setTime(_formatClock(timezone)), 1e3);
11113
- return () => clearInterval(id);
13330
+ let id;
13331
+ const tick = () => {
13332
+ setTime(_formatClock(timezone));
13333
+ id = setTimeout(tick, 1e3 - Date.now() % 1e3);
13334
+ };
13335
+ id = setTimeout(tick, 1e3 - Date.now() % 1e3);
13336
+ return () => clearTimeout(id);
11114
13337
  }, [timezone]);
11115
13338
  return time;
11116
13339
  }
11117
13340
  function _formatClock(timezone) {
11118
13341
  const tz = timezone === "exchange" ? "UTC" : timezone;
13342
+ const now = new Date(exchangeNow());
11119
13343
  try {
11120
13344
  return new Intl.DateTimeFormat("en-GB", {
11121
13345
  timeZone: tz,
@@ -11123,7 +13347,7 @@ function _formatClock(timezone) {
11123
13347
  minute: "2-digit",
11124
13348
  second: "2-digit",
11125
13349
  hour12: false
11126
- }).format(/* @__PURE__ */ new Date());
13350
+ }).format(now);
11127
13351
  } catch {
11128
13352
  return new Intl.DateTimeFormat("en-GB", {
11129
13353
  timeZone: "UTC",
@@ -11131,7 +13355,7 @@ function _formatClock(timezone) {
11131
13355
  minute: "2-digit",
11132
13356
  second: "2-digit",
11133
13357
  hour12: false
11134
- }).format(/* @__PURE__ */ new Date());
13358
+ }).format(now);
11135
13359
  }
11136
13360
  }
11137
13361
  function BottomToolbar({ symbol, timezone, onTimezoneChange, session, onSessionChange, onToggleScriptDrawer, scriptDrawerOpen }) {
@@ -11256,10 +13480,10 @@ function BottomToolbar({ symbol, timezone, onTimezoneChange, session, onSessionC
11256
13480
  setSessionOpen(false);
11257
13481
  },
11258
13482
  children: [
11259
- session === "regular" && /* @__PURE__ */ jsx("span", { className: "bottom-bar-check", children: "\xE2\u0153\u201C" }),
13483
+ session === "regular" && /* @__PURE__ */ jsx("span", { className: "bottom-bar-check", children: "\u2713" }),
11260
13484
  /* @__PURE__ */ jsxs("span", { className: "bottom-bar-session-label", children: [
11261
13485
  /* @__PURE__ */ jsx("strong", { children: "Regular trading hours" }),
11262
- /* @__PURE__ */ jsx("span", { children: "Exchange session only (RTH) \xE2\u20AC\u201D cleaner signals, higher liquidity" })
13486
+ /* @__PURE__ */ jsx("span", { children: "Exchange session only (RTH) \u2014 cleaner signals, higher liquidity" })
11263
13487
  ] })
11264
13488
  ]
11265
13489
  }
@@ -11273,10 +13497,10 @@ function BottomToolbar({ symbol, timezone, onTimezoneChange, session, onSessionC
11273
13497
  setSessionOpen(false);
11274
13498
  },
11275
13499
  children: [
11276
- session === "extended" && /* @__PURE__ */ jsx("span", { className: "bottom-bar-check", children: "\xE2\u0153\u201C" }),
13500
+ session === "extended" && /* @__PURE__ */ jsx("span", { className: "bottom-bar-check", children: "\u2713" }),
11277
13501
  /* @__PURE__ */ jsxs("span", { className: "bottom-bar-session-label", children: [
11278
13502
  /* @__PURE__ */ jsx("strong", { children: "Extended / electronic hours" }),
11279
- /* @__PURE__ */ jsx("span", { children: "Includes pre-market & after-hours (ETH) \xE2\u20AC\u201D full price range" })
13503
+ /* @__PURE__ */ jsx("span", { children: "Includes pre-market & after-hours (ETH) \u2014 full price range" })
11280
13504
  ] })
11281
13505
  ]
11282
13506
  }
@@ -11303,6 +13527,7 @@ function ChartWorkspace({
11303
13527
  onSymbolChange,
11304
13528
  onTimeframeChange,
11305
13529
  onAddCustomTimeframe,
13530
+ onRemoveCustomTimeframe,
11306
13531
  onFavoriteTfsChange,
11307
13532
  onAddIndicator,
11308
13533
  onToggleTheme,
@@ -11328,6 +13553,9 @@ function ChartWorkspace({
11328
13553
  // RightToolbar
11329
13554
  watchlistOpen,
11330
13555
  onToggleWatchlist,
13556
+ orderEntryOpen,
13557
+ onToggleOrderEntry,
13558
+ showOrderEntry,
11331
13559
  // BottomToolbar
11332
13560
  activeSymbol,
11333
13561
  timezone,
@@ -11340,6 +13568,7 @@ function ChartWorkspace({
11340
13568
  chartSlots,
11341
13569
  isLicensed,
11342
13570
  wsLoaded,
13571
+ leftDrawers,
11343
13572
  drawers
11344
13573
  }) {
11345
13574
  const tabItems = tabs.map((t) => ({
@@ -11396,6 +13625,7 @@ function ChartWorkspace({
11396
13625
  onSymbolChange,
11397
13626
  onTimeframeChange,
11398
13627
  onAddCustomTimeframe,
13628
+ onRemoveCustomTimeframe,
11399
13629
  onFavoritesChange: onFavoriteTfsChange,
11400
13630
  onAddIndicator,
11401
13631
  onToggleTheme,
@@ -11420,8 +13650,8 @@ function ChartWorkspace({
11420
13650
  ...onToggleTradeDrawer !== void 0 ? { onToggleTradeDrawer } : {}
11421
13651
  }
11422
13652
  ),
11423
- /* @__PURE__ */ jsxs("div", { style: { flex: 1, display: "flex", gap: 8, minHeight: 0, overflow: "hidden" }, children: [
11424
- /* @__PURE__ */ jsxs("div", { style: { position: "relative", flex: 1, display: "flex", gap: 8, minWidth: 0, minHeight: 0 }, children: [
13653
+ /* @__PURE__ */ jsxs("div", { style: { flex: 1, display: "flex", gap: 4, minHeight: 0, overflow: "hidden" }, children: [
13654
+ /* @__PURE__ */ jsxs("div", { style: { position: "relative", flex: 1, display: "flex", gap: 4, minWidth: 0, minHeight: 0 }, children: [
11425
13655
  !isLicensed && /* @__PURE__ */ jsxs("div", { style: {
11426
13656
  position: "absolute",
11427
13657
  inset: 0,
@@ -11442,11 +13672,16 @@ function ChartWorkspace({
11442
13672
  /* @__PURE__ */ jsx("div", { style: { color: "rgba(255,255,255,0.65)", fontSize: 13 }, children: "Please activate your license in the admin studio" })
11443
13673
  ] }),
11444
13674
  /* @__PURE__ */ jsx("div", { style: { position: "relative", flex: 1, minWidth: 0, minHeight: 0 }, children: chartSlots }),
13675
+ leftDrawers,
11445
13676
  /* @__PURE__ */ jsx(
11446
13677
  RightToolbar,
11447
13678
  {
11448
13679
  watchlistOpen,
11449
- onToggleWatchlist
13680
+ onToggleWatchlist,
13681
+ orderEntryOpen: orderEntryOpen ?? false,
13682
+ onToggleOrderEntry: onToggleOrderEntry ?? (() => {
13683
+ }),
13684
+ showOrderEntry
11450
13685
  }
11451
13686
  )
11452
13687
  ] }),