@forgecharts/sdk 1.1.32 → 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 (161) 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 +18 -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 +7 -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 +2506 -387
  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 +5103 -988
  119. package/dist/react/internal.js.map +1 -1
  120. package/dist/react/shell/ManagedAppShell.d.ts +20 -0
  121. package/dist/react/shell/ManagedAppShell.d.ts.map +1 -1
  122. package/dist/react/shell/OrderEntryPanel.d.ts +73 -1
  123. package/dist/react/shell/OrderEntryPanel.d.ts.map +1 -1
  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 +8 -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 +5 -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/TopToolbar.d.ts +2 -1
  137. package/dist/react/workspace/toolbars/TopToolbar.d.ts.map +1 -1
  138. package/dist/renderers/CandlestickRenderer.d.ts.map +1 -1
  139. package/dist/services/serverClock.d.ts +61 -0
  140. package/dist/services/serverClock.d.ts.map +1 -0
  141. package/package.json +1 -1
  142. package/dist/pine/PineCompiler.d.ts.map +0 -1
  143. package/dist/pine/diagnostics.d.ts.map +0 -1
  144. package/dist/pine/index.d.ts.map +0 -1
  145. package/dist/pine/pine-ast.d.ts.map +0 -1
  146. package/dist/pine/pine-lexer.d.ts.map +0 -1
  147. package/dist/pine/pine-parser.d.ts +0 -51
  148. package/dist/pine/pine-parser.d.ts.map +0 -1
  149. package/dist/pine/pine-transpiler.d.ts.map +0 -1
  150. package/dist/tscript/TScriptIndicator.d.ts.map +0 -1
  151. package/dist/tscript/ast.d.ts +0 -89
  152. package/dist/tscript/ast.d.ts.map +0 -1
  153. package/dist/tscript/lexer.d.ts.map +0 -1
  154. package/dist/tscript/parser.d.ts.map +0 -1
  155. package/dist/tscript/runtime.d.ts +0 -123
  156. package/dist/tscript/runtime.d.ts.map +0 -1
  157. package/dist/tscript/series.d.ts.map +0 -1
  158. /package/dist/{pine → forgescript/pine}/diagnostics.d.ts +0 -0
  159. /package/dist/{pine → forgescript/pine}/index.d.ts +0 -0
  160. /package/dist/{pine → forgescript/pine}/pine-lexer.d.ts +0 -0
  161. /package/dist/{tscript → forgescript}/series.d.ts +0 -0
package/dist/internal.js CHANGED
@@ -93,9 +93,73 @@ function computeTicks(min, max, targetCount) {
93
93
  return ticks;
94
94
  }
95
95
 
96
+ // ../shared/src/time/timeframeRegistry.ts
97
+ function isCalendarTimeframe(key) {
98
+ return key === "1w" || key === "1M" || key === "3M" || key === "6M" || key === "12M";
99
+ }
100
+
101
+ // ../shared/src/time/timeUtils.ts
102
+ var MONDAY_EPOCH_OFFSET_MS = 3456e5;
103
+ var WEEK_MS = 6048e5;
104
+ function timeframeToMs(tf) {
105
+ if (isCalendarTimeframe(tf)) return null;
106
+ const secs = parseTfSeconds(tf);
107
+ return secs !== null ? secs * 1e3 : null;
108
+ }
109
+ function parseTfSeconds(tf) {
110
+ const m = /^(\d+)(s|m|h|d|w|M)$/.exec(tf);
111
+ if (!m) return null;
112
+ const n = parseInt(m[1], 10);
113
+ const unit = m[2];
114
+ const UNIT_SECONDS = {
115
+ s: 1,
116
+ m: 60,
117
+ h: 3600,
118
+ d: 86400,
119
+ w: 604800,
120
+ M: 2592e3
121
+ };
122
+ const unitSec = UNIT_SECONDS[unit];
123
+ return unitSec !== void 0 ? n * unitSec : null;
124
+ }
125
+ function getBucketStart(timeMs, timeframe) {
126
+ if (timeframe === "1w") return getWeekBucketStart(timeMs);
127
+ if (timeframe === "1M") return getMonthBucketStart(timeMs);
128
+ if (timeframe === "3M") return getQuarterBucketStart(timeMs);
129
+ if (timeframe === "6M") return getHalfYearBucketStart(timeMs);
130
+ if (timeframe === "12M") return getYearBucketStart(timeMs);
131
+ const secs = parseTfSeconds(timeframe);
132
+ if (secs && secs > 0) return Math.floor(timeMs / (secs * 1e3)) * (secs * 1e3);
133
+ return Math.floor(timeMs / 6e4) * 6e4;
134
+ }
135
+ function getWeekBucketStart(timeMs) {
136
+ return Math.floor((timeMs - MONDAY_EPOCH_OFFSET_MS) / WEEK_MS) * WEEK_MS + MONDAY_EPOCH_OFFSET_MS;
137
+ }
138
+ function getMonthBucketStart(timeMs) {
139
+ const d = new Date(timeMs);
140
+ return Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), 1);
141
+ }
142
+ function getQuarterBucketStart(timeMs) {
143
+ const d = new Date(timeMs);
144
+ const quarterMonth = Math.floor(d.getUTCMonth() / 3) * 3;
145
+ return Date.UTC(d.getUTCFullYear(), quarterMonth, 1);
146
+ }
147
+ function getHalfYearBucketStart(timeMs) {
148
+ const d = new Date(timeMs);
149
+ const halfMonth = d.getUTCMonth() < 6 ? 0 : 6;
150
+ return Date.UTC(d.getUTCFullYear(), halfMonth, 1);
151
+ }
152
+ function getYearBucketStart(timeMs) {
153
+ return Date.UTC(new Date(timeMs).getUTCFullYear(), 0, 1);
154
+ }
155
+
96
156
  // ../utils/src/time.ts
157
+ function parseTimeframeSeconds(tf) {
158
+ return parseTfSeconds(tf);
159
+ }
97
160
  function formatTimestampLabel(ts, stepSeconds, tz) {
98
- const timeZone = !tz || tz === "exchange" ? "UTC" : tz;
161
+ const userTZ = !tz || tz === "exchange" ? "UTC" : tz;
162
+ const timeZone = stepSeconds >= 86400 ? "UTC" : userTZ;
99
163
  const d = new Date(ts * 1e3);
100
164
  if (stepSeconds < 3600) {
101
165
  return new Intl.DateTimeFormat("en-US", {
@@ -126,9 +190,19 @@ function formatTimestampLabel(ts, stepSeconds, tz) {
126
190
  );
127
191
  const yearStr = new Intl.DateTimeFormat("en-US", { year: "numeric", timeZone }).format(d);
128
192
  const monthStr = new Intl.DateTimeFormat("en-US", { month: "short", timeZone }).format(d);
129
- if (stepSeconds >= 25 * 86400) {
193
+ if (stepSeconds >= 182 * 86400) {
130
194
  return month === 1 ? yearStr : monthStr;
131
195
  }
196
+ if (stepSeconds <= 3 * 86400) {
197
+ const dayNum2 = parseInt(
198
+ new Intl.DateTimeFormat("en-US", { day: "numeric", timeZone }).format(d),
199
+ 10
200
+ );
201
+ if (dayNum2 === 1) {
202
+ return month === 1 ? yearStr : monthStr;
203
+ }
204
+ return String(dayNum2);
205
+ }
132
206
  const dayNum = parseInt(
133
207
  new Intl.DateTimeFormat("en-US", { day: "numeric", timeZone }).format(d),
134
208
  10
@@ -266,6 +340,8 @@ var TimeScale = class {
266
340
  return 70;
267
341
  }
268
342
  _niceTimeStep(rawSeconds) {
343
+ const minStep = parseTimeframeSeconds(this._activeTimeframe) ?? 1;
344
+ const effectiveRaw = Math.max(rawSeconds, minStep);
269
345
  const nice = [
270
346
  1,
271
347
  5,
@@ -283,6 +359,10 @@ var TimeScale = class {
283
359
  43200,
284
360
  86400,
285
361
  // 1 day
362
+ 2 * 86400,
363
+ // 2 days
364
+ 3 * 86400,
365
+ // 3 days
286
366
  7 * 86400,
287
367
  // 1 week
288
368
  14 * 86400,
@@ -297,9 +377,9 @@ var TimeScale = class {
297
377
  // ~1 year
298
378
  ];
299
379
  for (const s of nice) {
300
- if (s >= rawSeconds) return s;
380
+ if (s >= effectiveRaw) return s;
301
381
  }
302
- return 365 * 86400 * Math.ceil(rawSeconds / (365 * 86400));
382
+ return 365 * 86400 * Math.ceil(effectiveRaw / (365 * 86400));
303
383
  }
304
384
  };
305
385
 
@@ -734,17 +814,18 @@ var Crosshair = class {
734
814
  }
735
815
  /** Formats a Unix timestamp (seconds) as a human-readable date/time string. */
736
816
  _formatTime(time) {
737
- const timeZone = !this.timezone || this.timezone === "exchange" ? "UTC" : this.timezone;
817
+ const userTZ = !this.timezone || this.timezone === "exchange" ? "UTC" : this.timezone;
738
818
  const d = new Date(time * 1e3);
739
- const weekday = d.toLocaleDateString("en-US", { weekday: "short", timeZone });
740
- const day = d.toLocaleDateString("en-US", { day: "2-digit", timeZone });
741
- const month = d.toLocaleDateString("en-US", { month: "short", timeZone });
742
- const year = d.toLocaleDateString("en-US", { year: "2-digit", timeZone });
743
- const datePart = `${weekday} ${day} ${month} '${year}`;
744
819
  const dailyIntervals = /* @__PURE__ */ new Set(["1d", "2d", "3d", "1w", "2w", "1M", "3M", "6M", "12M"]);
820
+ const dateZone = dailyIntervals.has(this.interval) ? "UTC" : userTZ;
821
+ const weekday = d.toLocaleDateString("en-US", { weekday: "short", timeZone: dateZone });
822
+ const day = d.toLocaleDateString("en-US", { day: "2-digit", timeZone: dateZone });
823
+ const month = d.toLocaleDateString("en-US", { month: "short", timeZone: dateZone });
824
+ const year = d.toLocaleDateString("en-US", { year: "2-digit", timeZone: dateZone });
825
+ const datePart = `${weekday} ${day} ${month} '${year}`;
745
826
  if (dailyIntervals.has(this.interval)) return datePart;
746
- const hh = d.toLocaleString("en-US", { hour: "2-digit", hour12: false, timeZone });
747
- const mm = d.toLocaleString("en-US", { minute: "2-digit", timeZone }).padStart(2, "0");
827
+ const hh = d.toLocaleString("en-US", { hour: "2-digit", hour12: false, timeZone: userTZ });
828
+ const mm = d.toLocaleString("en-US", { minute: "2-digit", timeZone: userTZ }).padStart(2, "0");
748
829
  return `${datePart} ${hh}:${mm}`;
749
830
  }
750
831
  };
@@ -783,12 +864,8 @@ var CandlestickRenderer = class {
783
864
  const pixelsPerSecond = visibleSpan > 0 ? plotW / visibleSpan : 1;
784
865
  const barPitch = candleInterval > 0 ? candleInterval * pixelsPerSecond : plotW / Math.max(1, plotData.length);
785
866
  const minBarWidth = 1;
786
- const maxBarWidth = 20;
787
867
  const gap = Math.max(1, Math.round(barPitch * 0.15));
788
- const barWidth = Math.min(
789
- maxBarWidth,
790
- Math.max(minBarWidth, Math.floor(barPitch) - gap)
791
- );
868
+ const barWidth = Math.max(minBarWidth, Math.floor(barPitch) - gap);
792
869
  const halfBar = Math.max(0.5, barWidth / 2);
793
870
  for (const bar of plotData) {
794
871
  const x = mapRange(
@@ -1041,6 +1118,8 @@ var InteractionManager = class {
1041
1118
  // ── Hit-test data feeds ───────────────────────────────────────────────────
1042
1119
  _drawings = [];
1043
1120
  _bars = [];
1121
+ /** Overlay indicator polylines for hit-testing. Each entry is an indicator id + its data points. */
1122
+ _indicatorLines = [];
1044
1123
  // ── Mouse state ───────────────────────────────────────────────────────────
1045
1124
  _isDragging = false;
1046
1125
  _dragStartX = 0;
@@ -1057,9 +1136,11 @@ var InteractionManager = class {
1057
1136
  _drawingMode = false;
1058
1137
  /** True when the native cursor should be hidden (crosshair / dot / demonstration). */
1059
1138
  _hideCursor = false;
1060
- /** Sets cursor style, respecting _hideCursor — 'none' always wins when hiding. */
1139
+ /** Sets cursor style, respecting _hideCursor — axis resize cursors always show
1140
+ * even in crosshair/dot/demonstration mode so the user knows they can drag the axes. */
1061
1141
  _setCursor(style) {
1062
- this._el.style.cursor = this._hideCursor ? "none" : style;
1142
+ const isAxisCursor = style === "ns-resize" || style === "ew-resize";
1143
+ this._el.style.cursor = this._hideCursor && !isAxisCursor ? "none" : style;
1063
1144
  }
1064
1145
  /** Set when a drawing is committed mid-mousedown; prevents the paired mouseup from auto-selecting it. */
1065
1146
  _suppressNextClick = false;
@@ -1100,6 +1181,13 @@ var InteractionManager = class {
1100
1181
  setBars(bars) {
1101
1182
  this._bars = bars;
1102
1183
  }
1184
+ /**
1185
+ * Provide overlay indicator polylines for proximity hit-testing.
1186
+ * Each line is an indicator id + array of { time, value } points.
1187
+ */
1188
+ setIndicatorLines(lines) {
1189
+ this._indicatorLines = lines;
1190
+ }
1103
1191
  // ─── Selection ────────────────────────────────────────────────────────────
1104
1192
  /** Id of the currently selected drawing, or `null`. */
1105
1193
  get selectedDrawingId() {
@@ -1256,10 +1344,16 @@ var InteractionManager = class {
1256
1344
  if (!this._isDragging) {
1257
1345
  if (x >= this._transform.plotWidth && y < this._transform.plotHeight) {
1258
1346
  this._setCursor("ns-resize");
1347
+ } else if (y >= this._transform.plotHeight) {
1348
+ this._setCursor("ew-resize");
1259
1349
  } else if (hit.kind === "drawingHandle") {
1260
1350
  this._setCursor("grab");
1261
1351
  } else if (hit.kind === "drawing") {
1262
1352
  this._setCursor("grab");
1353
+ } else if (hit.kind === "indicator") {
1354
+ this._setCursor("pointer");
1355
+ } else if (hit.kind === "bar") {
1356
+ this._setCursor("pointer");
1263
1357
  } else {
1264
1358
  this._setCursor("default");
1265
1359
  }
@@ -1397,6 +1491,9 @@ var InteractionManager = class {
1397
1491
  if (hit.kind === "drawing") {
1398
1492
  this._selectedDrawingId = hit.id;
1399
1493
  this._handlers.onSelect?.(x, y, hit);
1494
+ } else if (hit.kind === "indicator") {
1495
+ this._selectedDrawingId = null;
1496
+ this._handlers.onSelect?.(x, y, hit);
1400
1497
  } else if (hit.kind === "bar") {
1401
1498
  this._selectedDrawingId = null;
1402
1499
  this._handlers.onSelect?.(x, y, hit);
@@ -1427,6 +1524,10 @@ var InteractionManager = class {
1427
1524
  if (drawingHit !== null) {
1428
1525
  return { kind: "drawing", id: drawingHit.id, type: drawingHit.type };
1429
1526
  }
1527
+ const indicatorId = this._hitTestIndicators(x, y);
1528
+ if (indicatorId !== null) {
1529
+ return { kind: "indicator", indicatorId };
1530
+ }
1430
1531
  const barHit = this._hitTestBar(x);
1431
1532
  if (barHit !== null) {
1432
1533
  return { kind: "bar", index: barHit.index, bar: barHit.bar };
@@ -1443,6 +1544,34 @@ var InteractionManager = class {
1443
1544
  }
1444
1545
  return -1;
1445
1546
  }
1547
+ _hitTestIndicators(x, y) {
1548
+ const t = this._transform;
1549
+ const { from, to } = t.timeRange;
1550
+ let bestId = null;
1551
+ let bestDist = DRAWING_HIT_PX + 1;
1552
+ for (const line of this._indicatorLines) {
1553
+ const pts = line.points;
1554
+ for (let i = 0; i < pts.length - 1; i++) {
1555
+ const a = pts[i];
1556
+ const b = pts[i + 1];
1557
+ if (a.time < from && b.time < from) continue;
1558
+ if (a.time > to && b.time > to) continue;
1559
+ const dist = this._distPointToSegment(
1560
+ x,
1561
+ y,
1562
+ t.timeToX(a.time),
1563
+ t.priceToY(a.value),
1564
+ t.timeToX(b.time),
1565
+ t.priceToY(b.value)
1566
+ );
1567
+ if (dist < bestDist) {
1568
+ bestDist = dist;
1569
+ bestId = line.id;
1570
+ }
1571
+ }
1572
+ }
1573
+ return bestId;
1574
+ }
1446
1575
  _hitTestDrawings(x, y) {
1447
1576
  let bestId = null;
1448
1577
  let bestType = null;
@@ -1679,26 +1808,14 @@ function rayExit(plotW, plotH, px, py, dx, dy) {
1679
1808
  else if (dy < 0) tMin = Math.min(tMin, -py / dy);
1680
1809
  return tMin === Infinity ? { x: px, y: py } : { x: px + dx * tMin, y: py + dy * tMin };
1681
1810
  }
1682
- var TF_SECS = {
1683
- "1s": 1,
1684
- "5s": 5,
1685
- "10s": 10,
1686
- "30s": 30,
1687
- "1m": 60,
1688
- "3m": 180,
1689
- "5m": 300,
1690
- "15m": 900,
1691
- "30m": 1800,
1692
- "1h": 3600,
1693
- "2h": 7200,
1694
- "4h": 14400,
1695
- "6h": 21600,
1696
- "12h": 43200,
1697
- "1d": 86400,
1698
- "3d": 259200,
1699
- "1w": 604800,
1700
- "1M": 2592e3
1701
- };
1811
+ var TF_SECS = new Proxy({}, {
1812
+ get(_t, tf) {
1813
+ return parseTfSeconds(tf) ?? 3600;
1814
+ },
1815
+ has(_t, _tf) {
1816
+ return true;
1817
+ }
1818
+ });
1702
1819
  function formatDuration(seconds) {
1703
1820
  if (seconds < 60) return `${seconds}s`;
1704
1821
  if (seconds < 3600) {
@@ -2536,29 +2653,68 @@ var LIGHT_COLORS = {
2536
2653
  crosshair: "#000000"
2537
2654
  };
2538
2655
 
2656
+ // src/services/serverClock.ts
2657
+ var PROBES = 3;
2658
+ var RESYNC_INTERVAL = 30 * 60 * 1e3;
2659
+ var _clientToServerOffset = 0;
2660
+ var _providerOffset = 0;
2661
+ var _endpoint = "/api/time";
2662
+ var _syncTimer = null;
2663
+ var _initialised = false;
2664
+ function exchangeNow() {
2665
+ return Date.now() + _clientToServerOffset + _providerOffset;
2666
+ }
2667
+ function serverNow() {
2668
+ return exchangeNow();
2669
+ }
2670
+ function serverClockOffset() {
2671
+ return _clientToServerOffset + _providerOffset;
2672
+ }
2673
+ async function initServerClock(endpoint = "/api/time") {
2674
+ _endpoint = endpoint;
2675
+ if (!_initialised) {
2676
+ _initialised = true;
2677
+ if (_syncTimer) clearInterval(_syncTimer);
2678
+ _syncTimer = setInterval(() => void _sync(), RESYNC_INTERVAL);
2679
+ }
2680
+ await _sync();
2681
+ }
2682
+ async function _probe() {
2683
+ const t0 = Date.now();
2684
+ try {
2685
+ const resp = await fetch(_endpoint, { cache: "no-store" });
2686
+ const t2 = Date.now();
2687
+ if (!resp.ok) return null;
2688
+ const body = await resp.json();
2689
+ if (typeof body.serverTime !== "number") return null;
2690
+ const rtt = t2 - t0;
2691
+ const offset = body.serverTime - (t0 + rtt / 2);
2692
+ const providerOffset = typeof body.providerOffset === "number" ? body.providerOffset : 0;
2693
+ return { offset, rtt, providerOffset };
2694
+ } catch {
2695
+ return null;
2696
+ }
2697
+ }
2698
+ async function _sync() {
2699
+ const results = [];
2700
+ for (let i = 0; i < PROBES; i++) {
2701
+ const r = await _probe();
2702
+ if (r) results.push(r);
2703
+ }
2704
+ if (results.length === 0) {
2705
+ return;
2706
+ }
2707
+ results.sort((a, b) => a.rtt - b.rtt);
2708
+ _clientToServerOffset = results[0].offset;
2709
+ _providerOffset = results[0].providerOffset;
2710
+ console.log(
2711
+ `[serverClock] clientToServer=${_clientToServerOffset > 0 ? "+" : ""}${Math.round(_clientToServerOffset)}ms providerOffset=${_providerOffset > 0 ? "+" : ""}${Math.round(_providerOffset)}ms rtt=${results[0].rtt}ms (${results.length}/${PROBES} probes ok)`
2712
+ );
2713
+ }
2714
+
2539
2715
  // src/core/Chart.ts
2540
- var _TF_SECONDS = {
2541
- "1s": 1,
2542
- "5s": 5,
2543
- "10s": 10,
2544
- "30s": 30,
2545
- "1m": 60,
2546
- "3m": 180,
2547
- "5m": 300,
2548
- "15m": 900,
2549
- "30m": 1800,
2550
- "1h": 3600,
2551
- "2h": 7200,
2552
- "4h": 14400,
2553
- "6h": 21600,
2554
- "12h": 43200,
2555
- "1d": 86400,
2556
- "3d": 259200,
2557
- "1w": 604800,
2558
- "1M": 2592e3
2559
- };
2560
2716
  function _tfToSeconds(tf) {
2561
- return _TF_SECONDS[tf] ?? 3600;
2717
+ return parseTfSeconds(tf) ?? 3600;
2562
2718
  }
2563
2719
  function _formatLastPrice(price) {
2564
2720
  const decimals = price < 1 ? 6 : price < 1e3 ? 4 : 2;
@@ -2571,6 +2727,20 @@ function _formatCountdown(secs) {
2571
2727
  if (h > 0) return `${h}:${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`;
2572
2728
  return `${m}:${String(s).padStart(2, "0")}`;
2573
2729
  }
2730
+ function _orderRoleLabel(role) {
2731
+ switch (role) {
2732
+ case "stop_loss":
2733
+ return "SL";
2734
+ case "take_profit":
2735
+ return "TP";
2736
+ case "limit":
2737
+ return "LMT";
2738
+ case "stop":
2739
+ return "STP";
2740
+ default:
2741
+ return "";
2742
+ }
2743
+ }
2574
2744
  var Chart = class {
2575
2745
  _container;
2576
2746
  _options;
@@ -2579,6 +2749,9 @@ var Chart = class {
2579
2749
  _gridLayer;
2580
2750
  _seriesLayer;
2581
2751
  _overlayLayer;
2752
+ /** Dedicated layer for the crosshair — rendered synchronously on every
2753
+ * mousemove so there is zero rAF-frame lag between the pointer and the cross. */
2754
+ _crosshairLayer;
2582
2755
  _timeScale;
2583
2756
  _priceScale;
2584
2757
  _transform;
@@ -2605,6 +2778,9 @@ var Chart = class {
2605
2778
  _drawingCursorPt = null;
2606
2779
  /** When false the crosshair is suppressed (e.g. Arrow/cursor mode). */
2607
2780
  _crosshairEnabled = true;
2781
+ // ── Trading overlay ───────────────────────────────────────────────────────────
2782
+ /** Current set of order lines supplied by TChart for canvas rendering. */
2783
+ _tradingOrders = [];
2608
2784
  /** Called whenever a drawing is committed (created or updated). */
2609
2785
  onDrawingCommitted;
2610
2786
  /** Called whenever a drawing is deleted. */
@@ -2628,6 +2804,7 @@ var Chart = class {
2628
2804
  this._gridLayer = new CanvasLayer(container, { width, height, dpr, zIndex: 1 });
2629
2805
  this._seriesLayer = new CanvasLayer(container, { width, height, dpr, zIndex: 2 });
2630
2806
  this._overlayLayer = new CanvasLayer(container, { width, height, dpr, zIndex: 3 });
2807
+ this._crosshairLayer = new CanvasLayer(container, { width, height, dpr, zIndex: 4 });
2631
2808
  this._timeScale = new TimeScale(this._gridLayer, this._colors, this._options.timeScale);
2632
2809
  this._priceScale = new PriceScale(this._gridLayer, this._colors, this._options.priceScale);
2633
2810
  this._transform = new CoordTransform(
@@ -2659,19 +2836,20 @@ var Chart = class {
2659
2836
  },
2660
2837
  onCrosshairMove: (x, y) => {
2661
2838
  if (!this._crosshairEnabled) return;
2662
- const { x: snappedX, time: snappedTime } = this._snapXToBar(x);
2839
+ const { x: snappedX, time: snappedTime } = this.snapXToBar(x);
2663
2840
  this._crosshair.update(snappedX, y, snappedTime);
2664
- this._dirty = true;
2841
+ this._renderCrosshairNow();
2665
2842
  },
2666
2843
  onCrosshairHide: () => {
2667
2844
  this._crosshair.hide();
2668
- this._dirty = true;
2845
+ this._renderCrosshairNow();
2669
2846
  },
2670
2847
  onKeyAction: (action) => {
2671
2848
  this._handleKeyAction(action);
2672
2849
  },
2673
2850
  onFitContent: () => {
2674
- this.scrollToEnd();
2851
+ this.fitDefaultView();
2852
+ this._dirty = true;
2675
2853
  },
2676
2854
  onPriceScaleZoom: (factor, originY) => {
2677
2855
  this._priceScaleManual = true;
@@ -2810,9 +2988,11 @@ var Chart = class {
2810
2988
  this._gridLayer.resize(width, height, dpr);
2811
2989
  this._seriesLayer.resize(width, height, dpr);
2812
2990
  this._overlayLayer.resize(width, height, dpr);
2991
+ this._crosshairLayer.resize(width, height, dpr);
2813
2992
  this._transform.update(width, height);
2814
2993
  this._dirty = true;
2815
2994
  this._render();
2995
+ this._renderCrosshairNow();
2816
2996
  }
2817
2997
  destroy() {
2818
2998
  if (this._animationFrame !== null) cancelAnimationFrame(this._animationFrame);
@@ -2821,6 +3001,7 @@ var Chart = class {
2821
3001
  this._gridLayer.destroy();
2822
3002
  this._seriesLayer.destroy();
2823
3003
  this._overlayLayer.destroy();
3004
+ this._crosshairLayer.destroy();
2824
3005
  this._crosshair.destroy();
2825
3006
  this._plugins.forEach((p) => p.onDetach());
2826
3007
  this._series = [];
@@ -2847,6 +3028,18 @@ var Chart = class {
2847
3028
  this._renderOverlay();
2848
3029
  this._plugins.forEach((p) => p.onRender());
2849
3030
  }
3031
+ /**
3032
+ * Clears and redraws only the crosshair canvas layer.
3033
+ * Called synchronously inside the mousemove handler — bypassing the
3034
+ * dirty-flag / rAF queue — so the crosshair has zero frame lag relative
3035
+ * to the actual pointer position.
3036
+ */
3037
+ _renderCrosshairNow() {
3038
+ const ctx = this._crosshairLayer.context;
3039
+ const { width, height } = this._crosshairLayer;
3040
+ ctx.clearRect(0, 0, width, height);
3041
+ this._crosshair.render(ctx, width, height);
3042
+ }
2850
3043
  _renderGrid() {
2851
3044
  const ctx = this._gridLayer.context;
2852
3045
  const { width, height } = this._gridLayer;
@@ -2880,16 +3073,17 @@ var Chart = class {
2880
3073
  const ctx = this._overlayLayer.context;
2881
3074
  const { width, height } = this._overlayLayer;
2882
3075
  ctx.clearRect(0, 0, width, height);
3076
+ this._renderTradingOrders(ctx);
2883
3077
  const selectedId = this._interaction.selectedDrawingId;
2884
3078
  const candles = this._series[0]?.data() ?? [];
2885
3079
  this._drawingMgr.render(ctx, this._transform, selectedId, this._interval, candles);
2886
3080
  this._interaction.setDrawings(this._drawingMgr.all());
3081
+ this._interaction.setBars(candles);
2887
3082
  if (this._drawingTool !== null && this._drawingCursorPt !== null) {
2888
3083
  const draft = this._buildDraftPreview();
2889
3084
  if (draft) this._drawingMgr.renderPreview(ctx, this._transform, draft, this._interval, candles);
2890
3085
  }
2891
3086
  this._renderLastPriceLine(ctx);
2892
- this._crosshair.render(ctx, width, height);
2893
3087
  }
2894
3088
  // ─── Private helpers ─────────────────────────────────────────────────────────
2895
3089
  /**
@@ -2897,7 +3091,8 @@ var Chart = class {
2897
3091
  * If no bars are loaded, or the nearest bar is more than SNAP_PX pixels away,
2898
3092
  * returns the original x so the crosshair always renders at the cursor position.
2899
3093
  */
2900
- _snapXToBar(x) {
3094
+ /** Snap a pixel x-coordinate to the nearest bar center. Public so it can be used by PointerOverlay. */
3095
+ snapXToBar(x) {
2901
3096
  const SNAP_PX = 20;
2902
3097
  let times = null;
2903
3098
  for (const s of this._series) {
@@ -2957,7 +3152,8 @@ var Chart = class {
2957
3152
  this._transform.zoomTime(zf, cx);
2958
3153
  break;
2959
3154
  case "scrollToEnd":
2960
- this._scrollToLatestBar();
3155
+ this.fitDefaultView();
3156
+ this._dirty = true;
2961
3157
  break;
2962
3158
  case "scrollToStart":
2963
3159
  break;
@@ -3020,12 +3216,11 @@ var Chart = class {
3020
3216
  ctx.stroke();
3021
3217
  ctx.setLineDash([]);
3022
3218
  const tfSecs = _tfToSeconds(this._interval);
3023
- const now = Math.floor(Date.now() / 1e3);
3024
- const remaining = lastBar.time + tfSecs - now;
3025
- const showCd = remaining > 0 && remaining <= tfSecs;
3219
+ const now = Math.floor(exchangeNow() / 1e3);
3220
+ const remaining = tfSecs - now % tfSecs;
3026
3221
  const priceLabel = _formatLastPrice(price);
3027
3222
  const lineH = 15;
3028
- const boxH = showCd ? lineH * 2 + 1 : lineH;
3223
+ const boxH = lineH * 2 + 1 ;
3029
3224
  const boxY = y - lineH / 2;
3030
3225
  ctx.fillStyle = lineColor;
3031
3226
  ctx.fillRect(plotWidth, boxY, psWidth, boxH);
@@ -3034,13 +3229,57 @@ var Chart = class {
3034
3229
  ctx.textBaseline = "middle";
3035
3230
  ctx.font = "bold 11px system-ui, sans-serif";
3036
3231
  ctx.fillText(priceLabel, plotWidth + 5, boxY + lineH / 2);
3037
- if (showCd) {
3232
+ {
3038
3233
  ctx.font = "10px system-ui, sans-serif";
3039
3234
  ctx.fillStyle = "rgba(255,255,255,0.85)";
3040
3235
  ctx.fillText(_formatCountdown(remaining), plotWidth + 5, boxY + lineH + 1 + lineH / 2);
3041
3236
  }
3042
3237
  ctx.restore();
3043
3238
  }
3239
+ /** Provides the overlay renderer with the latest order list from TChart. */
3240
+ setTradingOrders(orders) {
3241
+ this._tradingOrders = orders;
3242
+ }
3243
+ /** Renders active order lines above the series but below chart drawings. */
3244
+ _renderTradingOrders(ctx) {
3245
+ if (this._tradingOrders.length === 0) return;
3246
+ const plotWidth = this._transform.plotWidth;
3247
+ const plotHeight = this._transform.plotHeight;
3248
+ const psWidth = this._transform.priceScaleWidth;
3249
+ ctx.save();
3250
+ ctx.font = "bold 10px system-ui, sans-serif";
3251
+ ctx.textBaseline = "middle";
3252
+ ctx.textAlign = "left";
3253
+ for (const order of this._tradingOrders) {
3254
+ if (order.status === "cancelled" || order.status === "rejected" || order.status === "expired" || order.status === "filled") continue;
3255
+ const y = this._transform.priceToY(order.price);
3256
+ if (y < 0 || y > plotHeight) continue;
3257
+ const isBuy = order.side === "buy";
3258
+ const isPending = order.status === "pending";
3259
+ const color = isBuy ? "#26a641" : "#f85149";
3260
+ ctx.beginPath();
3261
+ ctx.strokeStyle = color;
3262
+ ctx.lineWidth = 1;
3263
+ ctx.globalAlpha = isPending ? 0.6 : 1;
3264
+ ctx.setLineDash(isPending ? [4, 4] : []);
3265
+ ctx.moveTo(0, y);
3266
+ ctx.lineTo(plotWidth, y);
3267
+ ctx.stroke();
3268
+ ctx.setLineDash([]);
3269
+ ctx.globalAlpha = 1;
3270
+ const roleLabel = _orderRoleLabel(order.role);
3271
+ const label = roleLabel ? `${roleLabel} ${order.qty}` : `${order.side.toUpperCase()} ${order.qty}`;
3272
+ const lineH = 15;
3273
+ const boxY = y - lineH / 2;
3274
+ ctx.globalAlpha = isPending ? 0.6 : 1;
3275
+ ctx.fillStyle = color;
3276
+ ctx.fillRect(plotWidth, boxY, psWidth, lineH);
3277
+ ctx.globalAlpha = 1;
3278
+ ctx.fillStyle = "#ffffff";
3279
+ ctx.fillText(label, plotWidth + 4, y);
3280
+ }
3281
+ ctx.restore();
3282
+ }
3044
3283
  /** Pans the viewport so the latest bar sits near the right edge. */
3045
3284
  _scrollToLatestBar() {
3046
3285
  let latestTime = -Infinity;
@@ -3071,6 +3310,7 @@ var Chart = class {
3071
3310
  this._priceScale.setVisibleRange(v.priceRange);
3072
3311
  this._priceScaleManual = true;
3073
3312
  this._dirty = true;
3313
+ this.onViewportChanged?.();
3074
3314
  }
3075
3315
  /**
3076
3316
  * Clears the manual-price-scale flag so the price axis returns to auto-fit mode.
@@ -3102,6 +3342,7 @@ var Chart = class {
3102
3342
  });
3103
3343
  this._priceScaleManual = false;
3104
3344
  this._dirty = true;
3345
+ this.onViewportChanged?.();
3105
3346
  }
3106
3347
  // ─── Drawing tool API ──────────────────────────────────────────────────────
3107
3348
  /** Returns the DrawingManager owned by this chart. */
@@ -3155,6 +3396,10 @@ var Chart = class {
3155
3396
  setNativeCursorHidden(hidden) {
3156
3397
  this._interaction.setHideCursor(hidden);
3157
3398
  }
3399
+ /** Feed overlay indicator polylines for hit-testing. */
3400
+ setIndicatorLines(lines) {
3401
+ this._interaction.setIndicatorLines(lines);
3402
+ }
3158
3403
  /** Deletes the currently selected drawing (if any). */
3159
3404
  deleteSelectedDrawing() {
3160
3405
  const id = this._interaction.selectedDrawingId;
@@ -3520,58 +3765,6 @@ var PaneManager = class {
3520
3765
  }
3521
3766
  };
3522
3767
 
3523
- // ../shared/src/time/timeframeRegistry.ts
3524
- var TIMEFRAMES = {
3525
- // ── Sub-hour ──────────────────────────────────────────────────────────────
3526
- "1m": { key: "1m", kind: "fixed", ms: 6e4 },
3527
- "2m": { key: "2m", kind: "fixed", ms: 12e4 },
3528
- "3m": { key: "3m", kind: "fixed", ms: 18e4 },
3529
- "4m": { key: "4m", kind: "fixed", ms: 24e4 },
3530
- "5m": { key: "5m", kind: "fixed", ms: 3e5 },
3531
- "10m": { key: "10m", kind: "fixed", ms: 6e5 },
3532
- "15m": { key: "15m", kind: "fixed", ms: 9e5 },
3533
- "20m": { key: "20m", kind: "fixed", ms: 12e5 },
3534
- "30m": { key: "30m", kind: "fixed", ms: 18e5 },
3535
- "45m": { key: "45m", kind: "fixed", ms: 27e5 },
3536
- // ── Hourly ────────────────────────────────────────────────────────────────
3537
- "1h": { key: "1h", kind: "fixed", ms: 36e5 },
3538
- "2h": { key: "2h", kind: "fixed", ms: 72e5 },
3539
- "4h": { key: "4h", kind: "fixed", ms: 144e5 },
3540
- "6h": { key: "6h", kind: "fixed", ms: 216e5 },
3541
- "12h": { key: "12h", kind: "fixed", ms: 432e5 },
3542
- // ── Daily ─────────────────────────────────────────────────────────────────
3543
- "1d": { key: "1d", kind: "fixed", ms: 864e5 },
3544
- "3d": { key: "3d", kind: "fixed", ms: 2592e5 },
3545
- // ── Calendar — MUST NOT have a fixed ms value ──────────────────────────
3546
- "1w": { key: "1w", kind: "calendar_week" },
3547
- "1M": { key: "1M", kind: "calendar_month" }
3548
- };
3549
-
3550
- // ../shared/src/time/timeUtils.ts
3551
- var MONDAY_EPOCH_OFFSET_MS = 3456e5;
3552
- var WEEK_MS = 6048e5;
3553
- function timeframeToMs(tf) {
3554
- const def = TIMEFRAMES[tf];
3555
- if (!def || def.kind !== "fixed") return null;
3556
- return def.ms;
3557
- }
3558
- function getBucketStart(timeMs, timeframe) {
3559
- const def = TIMEFRAMES[timeframe];
3560
- if (!def) {
3561
- return Math.floor(timeMs / 6e4) * 6e4;
3562
- }
3563
- if (def.kind === "calendar_week") return getWeekBucketStart(timeMs);
3564
- if (def.kind === "calendar_month") return getMonthBucketStart(timeMs);
3565
- return Math.floor(timeMs / def.ms) * def.ms;
3566
- }
3567
- function getWeekBucketStart(timeMs) {
3568
- return Math.floor((timeMs - MONDAY_EPOCH_OFFSET_MS) / WEEK_MS) * WEEK_MS + MONDAY_EPOCH_OFFSET_MS;
3569
- }
3570
- function getMonthBucketStart(timeMs) {
3571
- const d = new Date(timeMs);
3572
- return Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), 1);
3573
- }
3574
-
3575
3768
  // src/engine/timeframeUtils.ts
3576
3769
  function timeframeToMs2(timeframe) {
3577
3770
  return timeframeToMs(timeframe) ?? 6e4;
@@ -3925,51 +4118,12 @@ var CandleEngine = class {
3925
4118
  function toOHLCV(bar) {
3926
4119
  return { time: bar.time, open: bar.open, high: bar.high, low: bar.low, close: bar.close, volume: bar.volume };
3927
4120
  }
3928
- var HISTORY_WINDOWS = {
3929
- // Sub-hour: small display window; DB fills 12 months in background for scrollback.
3930
- "1m": 3 * 24 * 3600,
3931
- // 3 days → ~4 320 bars
3932
- "3m": 7 * 24 * 3600,
3933
- // 7 days → ~3 360 bars
3934
- "5m": 10 * 24 * 3600,
3935
- // 10 days → ~2 880 bars
3936
- "15m": 30 * 24 * 3600,
3937
- // 30 days → ~2 880 bars
3938
- "30m": 60 * 24 * 3600,
3939
- // 60 days → ~2 880 bars
3940
- "1h": 90 * 24 * 3600,
3941
- // 90 days → ~2 160 bars
3942
- // 4h and above: 12 months — bar counts are small enough to load in full.
3943
- "2h": 365 * 86400,
3944
- // 12 months → ~4 380 bars
3945
- "4h": 365 * 86400,
3946
- // 12 months → ~2 190 bars
3947
- "6h": 365 * 86400,
3948
- // 12 months → ~1 460 bars
3949
- "12h": 365 * 86400,
3950
- // 12 months → ~730 bars
3951
- "1d": 365 * 86400,
3952
- // 12 months → ~365 bars
3953
- "3d": 365 * 86400,
3954
- // 12 months → ~122 bars
3955
- "1w": 365 * 86400,
3956
- // 12 months → ~52 bars
3957
- "1M": 365 * 86400
3958
- // 12 months → ~12 bars
3959
- };
3960
- var DEFAULT_HISTORY_WINDOW = 90 * 24 * 3600;
3961
- var PAGE_SIZES = {
3962
- "1m": 500 * 60,
3963
- "5m": 500 * 300,
3964
- "15m": 500 * 900,
3965
- "30m": 500 * 1800,
3966
- "1h": 500 * 3600,
3967
- "4h": 500 * 4 * 3600,
3968
- "1d": 500 * 86400,
3969
- "1w": 500 * 7 * 86400,
3970
- "1M": 500 * 30 * 86400
3971
- };
3972
- var DEFAULT_PAGE_SIZE = 500 * 3600;
4121
+ function _historyWindow(tf) {
4122
+ return 500 * (parseTfSeconds(tf) ?? 3600);
4123
+ }
4124
+ function _pageSize(tf) {
4125
+ return 500 * (parseTfSeconds(tf) ?? 3600);
4126
+ }
3973
4127
  var _uidCounter = 0;
3974
4128
  var DatafeedConnector = class {
3975
4129
  _datafeed;
@@ -3999,12 +4153,13 @@ var DatafeedConnector = class {
3999
4153
  */
4000
4154
  connect(symbol, timeframe) {
4001
4155
  if (this._destroyed) return;
4156
+ if (!symbol) return;
4002
4157
  this._cancelActive();
4003
4158
  const uid = `forgecharts-${symbol}-${timeframe}-${++_uidCounter}`;
4004
4159
  this._activeUID = uid;
4005
4160
  this._cb.onLoading(symbol, timeframe);
4006
4161
  const to = Math.floor(Date.now() / 1e3);
4007
- const from = to - (HISTORY_WINDOWS[timeframe] ?? DEFAULT_HISTORY_WINDOW);
4162
+ const from = to - _historyWindow(timeframe);
4008
4163
  const engine = new CandleEngine({
4009
4164
  // Every live tick mutates the engine's bar array and the chart series.
4010
4165
  onBarUpdated: (bar) => {
@@ -4091,7 +4246,7 @@ var DatafeedConnector = class {
4091
4246
  if (!oldest) return;
4092
4247
  const uid = this._activeUID;
4093
4248
  const to = Math.floor(oldest.timeMs / 1e3);
4094
- const page = PAGE_SIZES[timeframe] ?? DEFAULT_PAGE_SIZE;
4249
+ const page = _pageSize(timeframe);
4095
4250
  const from = to - page;
4096
4251
  this._isLoadingMore = true;
4097
4252
  this._datafeed.getHistoricalBars(symbol, timeframe, from, to - 1).then(({ bars }) => {
@@ -4336,6 +4491,9 @@ var LicenseManager = class _LicenseManager {
4336
4491
  }
4337
4492
  };
4338
4493
 
4494
+ // src/licensing/licenseTypes.ts
4495
+ var TRADING_TIERS = /* @__PURE__ */ new Set(["trading", "dom"]);
4496
+
4339
4497
  // src/licensing/ChartRuntimeResolver.ts
4340
4498
  var ChartRuntimeResolver = class _ChartRuntimeResolver {
4341
4499
  static instance = null;
@@ -4387,33 +4545,45 @@ var ChartRuntimeResolver = class _ChartRuntimeResolver {
4387
4545
  /**
4388
4546
  * Built-in order entry UI hooks.
4389
4547
  * Requires managed mode AND the `orderEntry` feature flag (or no flag restriction).
4548
+ * When a tier is present on the license, the tier must be 'trading' or 'dom'.
4390
4549
  */
4391
4550
  canUseOrderEntry() {
4392
4551
  if (this.isUnmanagedMode()) return false;
4552
+ const tier = this.lm.getLicense()?.tier;
4553
+ if (tier && !TRADING_TIERS.has(tier)) return false;
4393
4554
  return this.#featureOrDefault("orderEntry", true);
4394
4555
  }
4395
4556
  /**
4396
4557
  * Managed trading service hooks (broker execution, position management).
4397
4558
  * Requires managed mode AND the `managedTrading` feature flag (or no flag restriction).
4559
+ * When a tier is present on the license, the tier must be 'trading' or 'dom'.
4398
4560
  */
4399
4561
  canUseManagedTrading() {
4400
4562
  if (this.isUnmanagedMode()) return false;
4563
+ const tier = this.lm.getLicense()?.tier;
4564
+ if (tier && !TRADING_TIERS.has(tier)) return false;
4401
4565
  return this.#featureOrDefault("managedTrading", true);
4402
4566
  }
4403
4567
  /**
4404
4568
  * Drag-to-price order placement.
4405
4569
  * Requires managed mode AND the `draggableOrders` feature flag (or no flag restriction).
4570
+ * When a tier is present on the license, the tier must be 'trading' or 'dom'.
4406
4571
  */
4407
4572
  canUseDraggableOrders() {
4408
4573
  if (this.isUnmanagedMode()) return false;
4574
+ const tier = this.lm.getLicense()?.tier;
4575
+ if (tier && !TRADING_TIERS.has(tier)) return false;
4409
4576
  return this.#featureOrDefault("draggableOrders", true);
4410
4577
  }
4411
4578
  /**
4412
4579
  * Bracket / OCO order support.
4413
4580
  * Requires managed mode AND the `bracketOrders` feature flag (or no flag restriction).
4581
+ * When a tier is present on the license, the tier must be 'trading' or 'dom'.
4414
4582
  */
4415
4583
  canUseBracketOrders() {
4416
4584
  if (this.isUnmanagedMode()) return false;
4585
+ const tier = this.lm.getLicense()?.tier;
4586
+ if (tier && !TRADING_TIERS.has(tier)) return false;
4417
4587
  return this.#featureOrDefault("bracketOrders", true);
4418
4588
  }
4419
4589
  // ── UI render capability checks ──────────────────────────────────────────────
@@ -4904,26 +5074,6 @@ var ManagedTradingController = class {
4904
5074
  };
4905
5075
 
4906
5076
  // src/api/TChart.ts
4907
- var _TF_SECS = {
4908
- "1s": 1,
4909
- "5s": 5,
4910
- "10s": 10,
4911
- "30s": 30,
4912
- "1m": 60,
4913
- "3m": 180,
4914
- "5m": 300,
4915
- "15m": 900,
4916
- "30m": 1800,
4917
- "1h": 3600,
4918
- "2h": 7200,
4919
- "4h": 14400,
4920
- "6h": 21600,
4921
- "12h": 43200,
4922
- "1d": 86400,
4923
- "3d": 259200,
4924
- "1w": 604800,
4925
- "1M": 2592e3
4926
- };
4927
5077
  var TChart = class {
4928
5078
  _core;
4929
5079
  _bus;
@@ -5001,9 +5151,14 @@ var TChart = class {
5001
5151
  };
5002
5152
  this._series = this._core.addSeries({ type: "candlestick" });
5003
5153
  this._core.setInterval(this._interval);
5154
+ this._core.setTradingOrders(this._overlayStore.getOrders());
5155
+ this._overlayStore.onChange(() => {
5156
+ this._core.setTradingOrders(this._overlayStore.getOrders());
5157
+ this._core.markDirty();
5158
+ });
5004
5159
  if (config.datafeed !== void 0) {
5005
5160
  this._connector = this._buildConnector(config.datafeed);
5006
- this._connector.connect(this._symbol, this._interval);
5161
+ if (this._symbol) this._connector.connect(this._symbol, this._interval);
5007
5162
  }
5008
5163
  this._core.onViewportChanged = () => {
5009
5164
  const connector = this._connector;
@@ -5011,9 +5166,9 @@ var TChart = class {
5011
5166
  const bars = this._series.data();
5012
5167
  if (bars.length === 0) return;
5013
5168
  const oldestBarTime = bars[0].time;
5014
- const { from } = this._core.getViewport().timeRange;
5015
- const tfSec = _TF_SECS[this._interval] ?? 3600;
5016
- if (from < oldestBarTime + tfSec * 30) {
5169
+ const { from, to } = this._core.getViewport().timeRange;
5170
+ const visibleSpan = to - from;
5171
+ if (from <= oldestBarTime + visibleSpan * 0.75) {
5017
5172
  connector.loadMoreHistory(this._symbol, this._interval);
5018
5173
  }
5019
5174
  };
@@ -5095,7 +5250,7 @@ var TChart = class {
5095
5250
  if (symbol === this._symbol) return;
5096
5251
  this._symbol = symbol;
5097
5252
  this._bus.emit("symbolChanged", { symbol });
5098
- this._connector?.reconnect(symbol, this._interval);
5253
+ if (symbol) this._connector?.reconnect(symbol, this._interval);
5099
5254
  }
5100
5255
  /**
5101
5256
  * Changes the active interval.
@@ -5270,6 +5425,16 @@ var TChart = class {
5270
5425
  this._assertAlive();
5271
5426
  this._core.setNativeCursorHidden(hidden);
5272
5427
  }
5428
+ /** Feed overlay indicator polylines for hit-testing. */
5429
+ setIndicatorLines(lines) {
5430
+ this._assertAlive();
5431
+ this._core.setIndicatorLines(lines);
5432
+ }
5433
+ /** Snap a pixel x-coordinate to the nearest bar center (for pointer overlay snapping). */
5434
+ snapXToBar(x) {
5435
+ this._assertAlive();
5436
+ return this._core.snapXToBar(x);
5437
+ }
5273
5438
  /** Deletes the currently selected drawing (if any). */
5274
5439
  deleteSelectedDrawing() {
5275
5440
  this._assertAlive();
@@ -5905,7 +6070,7 @@ function computeWMAFromSeries(input, period) {
5905
6070
  return result;
5906
6071
  }
5907
6072
 
5908
- // src/tscript/series.ts
6073
+ // src/forgescript/series.ts
5909
6074
  var Series2 = class {
5910
6075
  _buf;
5911
6076
  _cap;
@@ -5971,6 +6136,51 @@ var Lexer = class {
5971
6136
  this._src = src;
5972
6137
  }
5973
6138
  tokenize() {
6139
+ const raw = this._rawTokenize();
6140
+ return this._injectIndents(raw);
6141
+ }
6142
+ // ─── INDENT/DEDENT injection ──────────────────────────────────────────────
6143
+ /** After each NEWLINE, inject INDENT/DEDENT tokens based on the indentation
6144
+ * of the following non-blank line. Enables indented if/else blocks. */
6145
+ _injectIndents(raw) {
6146
+ const out = [];
6147
+ const indentStack = [0];
6148
+ let i = 0;
6149
+ while (i < raw.length) {
6150
+ const tok = raw[i];
6151
+ if (tok.kind !== "NEWLINE") {
6152
+ out.push(tok);
6153
+ i++;
6154
+ continue;
6155
+ }
6156
+ out.push(tok);
6157
+ i++;
6158
+ while (i < raw.length && raw[i].kind === "NEWLINE") {
6159
+ out.push(raw[i]);
6160
+ i++;
6161
+ }
6162
+ if (i >= raw.length || raw[i].kind === "EOF") break;
6163
+ const nextTok = raw[i];
6164
+ const spaces = nextTok.col - 1;
6165
+ const currentIndent = indentStack[indentStack.length - 1];
6166
+ if (spaces > currentIndent) {
6167
+ indentStack.push(spaces);
6168
+ out.push({ kind: "INDENT", value: "", line: nextTok.line, col: 1 });
6169
+ } else {
6170
+ while (spaces < indentStack[indentStack.length - 1]) {
6171
+ indentStack.pop();
6172
+ out.push({ kind: "DEDENT", value: "", line: nextTok.line, col: 1 });
6173
+ }
6174
+ }
6175
+ }
6176
+ while (indentStack.length > 1) {
6177
+ indentStack.pop();
6178
+ out.push({ kind: "DEDENT", value: "", line: 0, col: 0 });
6179
+ }
6180
+ out.push({ kind: "EOF", value: "", line: this._line, col: this._col });
6181
+ return out;
6182
+ }
6183
+ _rawTokenize() {
5974
6184
  const tokens = [];
5975
6185
  while (this._pos < this._src.length) {
5976
6186
  this._skipWhitespaceAndComments();
@@ -5995,7 +6205,23 @@ var Lexer = class {
5995
6205
  tokens.push(this._readIdent());
5996
6206
  continue;
5997
6207
  }
6208
+ if (ch === "#") {
6209
+ tokens.push(this._readColor());
6210
+ continue;
6211
+ }
5998
6212
  const two = this._src.slice(this._pos, this._pos + 2);
6213
+ if (two === ":=") {
6214
+ tokens.push(this._make("COLONEQ", two));
6215
+ this._pos += 2;
6216
+ this._col += 2;
6217
+ continue;
6218
+ }
6219
+ if (two === "=>") {
6220
+ tokens.push(this._make("ARROW", two));
6221
+ this._pos += 2;
6222
+ this._col += 2;
6223
+ continue;
6224
+ }
5999
6225
  if (two === "<=") {
6000
6226
  tokens.push(this._make("LTE", two));
6001
6227
  this._pos += 2;
@@ -6030,6 +6256,7 @@ var Lexer = class {
6030
6256
  ">": "GT",
6031
6257
  "?": "QMARK",
6032
6258
  ":": "COLON",
6259
+ ".": "DOT",
6033
6260
  "(": "LPAREN",
6034
6261
  ")": "RPAREN",
6035
6262
  "[": "LBRACKET",
@@ -6043,7 +6270,7 @@ var Lexer = class {
6043
6270
  this._col++;
6044
6271
  continue;
6045
6272
  }
6046
- throw new SyntaxError(`[TScript] Unexpected character '${ch}' at ${this._line}:${this._col}`);
6273
+ throw new SyntaxError(`[ForgeScript] Unexpected character '${ch}' at ${this._line}:${this._col}`);
6047
6274
  }
6048
6275
  tokens.push(this._make("EOF", ""));
6049
6276
  return tokens;
@@ -6126,14 +6353,31 @@ var Lexer = class {
6126
6353
  }
6127
6354
  return { kind: "IDENT", value: name, line: this._line, col: startCol };
6128
6355
  }
6356
+ _readColor() {
6357
+ const startCol = this._col;
6358
+ this._pos++;
6359
+ this._col++;
6360
+ let hex = "#";
6361
+ while (this._pos < this._src.length && this._isHex(this._src[this._pos])) {
6362
+ hex += this._src[this._pos];
6363
+ this._pos++;
6364
+ this._col++;
6365
+ }
6366
+ if (hex.length !== 4 && hex.length !== 7 && hex.length !== 9) {
6367
+ throw new SyntaxError(`[ForgeScript] Invalid color literal '${hex}' at ${this._line}:${startCol}`);
6368
+ }
6369
+ return { kind: "COLOR", value: hex, line: this._line, col: startCol };
6370
+ }
6371
+ _isHex(ch) {
6372
+ return ch >= "0" && ch <= "9" || ch >= "a" && ch <= "f" || ch >= "A" && ch <= "F";
6373
+ }
6129
6374
  };
6130
6375
 
6131
- // src/tscript/parser.ts
6376
+ // src/forgescript/parser.ts
6132
6377
  var Parser = class {
6133
6378
  _tokens;
6134
6379
  _pos = 0;
6135
6380
  constructor(src) {
6136
- this._tokens = new Lexer(src).tokenize().filter((t) => t.kind !== "NEWLINE" || this._isStatementBoundary(t));
6137
6381
  this._tokens = this._collapseNewlines(new Lexer(src).tokenize());
6138
6382
  }
6139
6383
  parse() {
@@ -6147,21 +6391,83 @@ var Parser = class {
6147
6391
  }
6148
6392
  // ─── Statements ─────────────────────────────────────────────────────────────
6149
6393
  _stmt() {
6394
+ if (this._checkIdent("if")) {
6395
+ return this._ifStmt();
6396
+ }
6397
+ if (this._checkIdent("while")) {
6398
+ return this._whileStmt();
6399
+ }
6400
+ if (this._checkIdent("for")) {
6401
+ return this._forStmt();
6402
+ }
6403
+ if (this._checkIdent("var") || this._checkIdent("varip")) {
6404
+ return this._varDeclStmt();
6405
+ }
6150
6406
  if (this._checkIdent("indicator")) {
6151
6407
  return this._indicatorDecl();
6152
6408
  }
6409
+ if (this._check("IDENT") && this._peekKind(1) === "LPAREN" && this._isFnDecl()) {
6410
+ return this._fnDeclStmt();
6411
+ }
6153
6412
  if (this._check("IDENT") && this._peekKind(1) === "EQ") {
6154
6413
  return this._assign();
6155
6414
  }
6415
+ if (this._check("IDENT") && this._peekKind(1) === "COLONEQ") {
6416
+ return this._reassign();
6417
+ }
6156
6418
  return this._exprStmt();
6157
6419
  }
6158
6420
  _indicatorDecl() {
6159
6421
  this._consumeIdent("indicator");
6160
6422
  this._consume("LPAREN", "Expected '(' after 'indicator'");
6161
6423
  const title = this._consume("STRING", "Expected string title in indicator()").value;
6162
- this._consume("RPAREN", "Expected ')' after indicator title");
6424
+ const namedArgs = /* @__PURE__ */ new Map();
6425
+ while (this._match("COMMA")) {
6426
+ if (this._check("IDENT") && this._peekKind(1) === "EQ") {
6427
+ const key = this._consume("IDENT", "Expected named arg").value;
6428
+ this._consume("EQ", "Expected '='");
6429
+ namedArgs.set(key, this._expr());
6430
+ } else {
6431
+ this._expr();
6432
+ }
6433
+ }
6434
+ this._consume("RPAREN", "Expected ')' after indicator args");
6435
+ this._consumeNewlineOrEOF();
6436
+ return { kind: "IndicatorDecl", title, namedArgs };
6437
+ }
6438
+ _varDeclStmt() {
6439
+ const modifier = this._consume("IDENT", "Expected 'var' or 'varip'").value;
6440
+ if (this._check("IDENT") && this._peekKind(1) === "IDENT") {
6441
+ this._advance();
6442
+ }
6443
+ const name = this._consume("IDENT", "Expected variable name after var/varip").value;
6444
+ this._consume("EQ", "Expected '=' in var declaration");
6445
+ const value = this._expr();
6446
+ this._consumeNewlineOrEOF();
6447
+ return { kind: "VarDeclStmt", modifier, name, value };
6448
+ }
6449
+ _reassign() {
6450
+ const name = this._consume("IDENT", "Expected identifier").value;
6451
+ this._consume("COLONEQ", "Expected ':='");
6452
+ const value = this._expr();
6453
+ this._consumeNewlineOrEOF();
6454
+ return { kind: "ReassignStmt", name, value };
6455
+ }
6456
+ _forStmt() {
6457
+ this._consumeIdent("for");
6458
+ const name = this._consume("IDENT", "Expected loop variable after 'for'").value;
6459
+ this._consume("EQ", "Expected '=' after loop variable");
6460
+ const start = this._expr();
6461
+ this._consumeIdent("to");
6462
+ const end = this._expr();
6463
+ let step = null;
6464
+ if (this._checkIdent("by")) {
6465
+ this._advance();
6466
+ step = this._expr();
6467
+ }
6163
6468
  this._consumeNewlineOrEOF();
6164
- return { kind: "IndicatorDecl", title };
6469
+ const body = this._indentedBlock();
6470
+ return { kind: "ForStmt", name, start, end, step, body };
6165
6471
  }
6166
6472
  _assign() {
6167
6473
  const name = this._consume("IDENT", "Expected identifier").value;
@@ -6175,33 +6481,102 @@ var Parser = class {
6175
6481
  this._consumeNewlineOrEOF();
6176
6482
  return { kind: "ExprStmt", expr };
6177
6483
  }
6178
- // ─── Expressions ────────────────────────────────────────────────────────────
6179
- _expr() {
6180
- return this._ternary();
6181
- }
6182
- _ternary() {
6183
- let left = this._or();
6184
- if (this._match("QMARK")) {
6185
- const consequent = this._expr();
6186
- this._consume("COLON", "Expected ':' in ternary expression");
6187
- const alternate = this._expr();
6188
- const node = { kind: "TernaryExpr", condition: left, consequent, alternate };
6189
- left = node;
6190
- }
6191
- return left;
6192
- }
6193
- _or() {
6194
- let left = this._and();
6195
- while (this._checkIdent("or")) {
6484
+ _ifStmt() {
6485
+ this._consumeIdent("if");
6486
+ const condition = this._expr();
6487
+ this._consumeNewlineOrEOF();
6488
+ const then = this._indentedBlock();
6489
+ let else_ = [];
6490
+ if (this._checkIdent("else")) {
6196
6491
  this._advance();
6197
- const right = this._and();
6198
- const node = { kind: "LogicalExpr", op: "or", left, right };
6199
- left = node;
6492
+ this._consumeNewlineOrEOF();
6493
+ else_ = this._indentedBlock();
6200
6494
  }
6201
- return left;
6495
+ return { kind: "IfStmt", condition, then, else_ };
6202
6496
  }
6203
- _and() {
6204
- let left = this._not();
6497
+ _whileStmt() {
6498
+ this._consumeIdent("while");
6499
+ const condition = this._expr();
6500
+ this._consumeNewlineOrEOF();
6501
+ const body = this._indentedBlock();
6502
+ return { kind: "WhileStmt", condition, body };
6503
+ }
6504
+ /** Lookahead: is this IDENT '(' ... ')' '=>'? Scans without consuming. */
6505
+ _isFnDecl() {
6506
+ let depth = 0;
6507
+ let i = this._pos + 1;
6508
+ while (i < this._tokens.length) {
6509
+ const k = this._tokens[i].kind;
6510
+ if (k === "LPAREN") depth++;
6511
+ else if (k === "RPAREN") {
6512
+ depth--;
6513
+ if (depth === 0) break;
6514
+ } else if (k === "EOF" || k === "NEWLINE") return false;
6515
+ i++;
6516
+ }
6517
+ return i + 1 < this._tokens.length && this._tokens[i + 1].kind === "ARROW";
6518
+ }
6519
+ /** Parse: name(p1, p2, ...) => expr OR name(p1, p2, ...) =>\n block */
6520
+ _fnDeclStmt() {
6521
+ const name = this._consume("IDENT", "Expected function name").value;
6522
+ this._consume("LPAREN", "Expected '(' after function name");
6523
+ const params = [];
6524
+ if (!this._check("RPAREN")) {
6525
+ params.push(this._consume("IDENT", "Expected parameter name").value);
6526
+ while (this._match("COMMA")) {
6527
+ params.push(this._consume("IDENT", "Expected parameter name").value);
6528
+ }
6529
+ }
6530
+ this._consume("RPAREN", "Expected ')' after parameters");
6531
+ this._consume("ARROW", "Expected '=>'");
6532
+ if (this._check("NEWLINE") || this._check("EOF")) {
6533
+ this._consumeNewlineOrEOF();
6534
+ const body = this._indentedBlock();
6535
+ return { kind: "FnDeclStmt", name, params, body };
6536
+ }
6537
+ const expr = this._expr();
6538
+ this._consumeNewlineOrEOF();
6539
+ return { kind: "FnDeclStmt", name, params, body: [{ kind: "ExprStmt", expr }] };
6540
+ }
6541
+ _indentedBlock() {
6542
+ const stmts = [];
6543
+ if (!this._check("INDENT")) return stmts;
6544
+ this._advance();
6545
+ this._skipNewlines();
6546
+ while (!this._check("DEDENT") && !this._check("EOF")) {
6547
+ stmts.push(this._stmt());
6548
+ this._skipNewlines();
6549
+ }
6550
+ if (this._check("DEDENT")) this._advance();
6551
+ return stmts;
6552
+ }
6553
+ // ─── Expressions ────────────────────────────────────────────────────────────
6554
+ _expr() {
6555
+ return this._ternary();
6556
+ }
6557
+ _ternary() {
6558
+ let left = this._or();
6559
+ if (this._match("QMARK")) {
6560
+ const consequent = this._expr();
6561
+ this._consume("COLON", "Expected ':' in ternary expression");
6562
+ const alternate = this._expr();
6563
+ const node = { kind: "TernaryExpr", condition: left, consequent, alternate };
6564
+ left = node;
6565
+ }
6566
+ return left;
6567
+ }
6568
+ _or() {
6569
+ let left = this._and();
6570
+ while (this._checkIdent("or")) {
6571
+ this._advance();
6572
+ const right = this._and();
6573
+ const node = { kind: "LogicalExpr", op: "or", left, right };
6574
+ left = node;
6575
+ }
6576
+ return left;
6577
+ }
6578
+ _and() {
6579
+ let left = this._not();
6205
6580
  while (this._checkIdent("and")) {
6206
6581
  this._advance();
6207
6582
  const right = this._not();
@@ -6309,11 +6684,19 @@ var Parser = class {
6309
6684
  const node = { kind: "BoolLit", value: tok.value === "true" };
6310
6685
  return node;
6311
6686
  }
6687
+ if (tok.kind === "COLOR") {
6688
+ this._advance();
6689
+ const node = { kind: "ColorLit", value: tok.value };
6690
+ return node;
6691
+ }
6312
6692
  if (tok.kind === "IDENT" && tok.value === "na") {
6313
6693
  this._advance();
6314
6694
  const node = { kind: "NumberLit", value: NaN };
6315
6695
  return node;
6316
6696
  }
6697
+ if (tok.kind === "IDENT" && this._peekKind(1) === "DOT") {
6698
+ return this._nsCallOrMember();
6699
+ }
6317
6700
  if (tok.kind === "IDENT" && this._peekKind(1) === "LPAREN") {
6318
6701
  return this._callExpr();
6319
6702
  }
@@ -6336,21 +6719,57 @@ var Parser = class {
6336
6719
  return inner;
6337
6720
  }
6338
6721
  throw new SyntaxError(
6339
- `[TScript] Unexpected token '${tok.value}' (${tok.kind}) at ${tok.line}:${tok.col}`
6722
+ `[ForgeScript] Unexpected token '${tok.value}' (${tok.kind}) at ${tok.line}:${tok.col}`
6340
6723
  );
6341
6724
  }
6725
+ /** Parse IDENT.IDENT or IDENT.IDENT(args) */
6726
+ _nsCallOrMember() {
6727
+ const ns = this._consume("IDENT", "Expected namespace").value;
6728
+ this._consume("DOT", "Expected '.'");
6729
+ const member = this._consume("IDENT", "Expected member name").value;
6730
+ if (this._check("LPAREN")) {
6731
+ this._advance();
6732
+ const { posArgs, namedArgs } = this._argList();
6733
+ this._consume("RPAREN", "Expected ')'");
6734
+ const node2 = { kind: "NsCallExpr", namespace: ns, fn: member, args: posArgs, namedArgs };
6735
+ return node2;
6736
+ }
6737
+ let node = { kind: "MemberExpr", object: ns, prop: member };
6738
+ while (this._check("LBRACKET")) {
6739
+ this._advance();
6740
+ const index = this._expr();
6741
+ this._consume("RBRACKET", "Expected ']'");
6742
+ const indexNode = { kind: "IndexExpr", series: node, index };
6743
+ node = indexNode;
6744
+ }
6745
+ return node;
6746
+ }
6342
6747
  _callExpr() {
6343
6748
  const callee = this._consume("IDENT", "Expected function name").value;
6344
6749
  this._consume("LPAREN", "Expected '('");
6345
- const args = [];
6346
- if (!this._check("RPAREN")) {
6347
- args.push(this._expr());
6348
- while (this._match("COMMA")) {
6349
- args.push(this._expr());
6750
+ const { posArgs, namedArgs } = this._argList();
6751
+ this._consume("RPAREN", "Expected ')'");
6752
+ return { kind: "CallExpr", callee, args: posArgs, namedArgs };
6753
+ }
6754
+ /** Parse a mixed positional + named argument list. */
6755
+ _argList() {
6756
+ const posArgs = [];
6757
+ const namedArgs = /* @__PURE__ */ new Map();
6758
+ if (this._check("RPAREN")) return { posArgs, namedArgs };
6759
+ const parseArg = () => {
6760
+ if (this._check("IDENT") && this._peekKind(1) === "EQ") {
6761
+ const key = this._consume("IDENT", "Expected named arg").value;
6762
+ this._consume("EQ", "Expected '='");
6763
+ namedArgs.set(key, this._expr());
6764
+ } else {
6765
+ posArgs.push(this._expr());
6350
6766
  }
6767
+ };
6768
+ parseArg();
6769
+ while (this._match("COMMA")) {
6770
+ parseArg();
6351
6771
  }
6352
- this._consume("RPAREN", "Expected ')'");
6353
- return { kind: "CallExpr", callee, args };
6772
+ return { posArgs, namedArgs };
6354
6773
  }
6355
6774
  // ─── Utility helpers ────────────────────────────────────────────────────────
6356
6775
  _binop(op, left, right) {
@@ -6382,12 +6801,12 @@ var Parser = class {
6382
6801
  _consume(kind, msg) {
6383
6802
  if (this._check(kind)) return this._advance();
6384
6803
  const t = this._current();
6385
- throw new SyntaxError(`[TScript] ${msg} \u2014 got '${t.value}' at ${t.line}:${t.col}`);
6804
+ throw new SyntaxError(`[ForgeScript] ${msg} \u2014 got '${t.value}' at ${t.line}:${t.col}`);
6386
6805
  }
6387
6806
  _consumeIdent(name) {
6388
6807
  if (this._checkIdent(name)) return this._advance();
6389
6808
  const t = this._current();
6390
- throw new SyntaxError(`[TScript] Expected '${name}' \u2014 got '${t.value}' at ${t.line}:${t.col}`);
6809
+ throw new SyntaxError(`[ForgeScript] Expected '${name}' \u2014 got '${t.value}' at ${t.line}:${t.col}`);
6391
6810
  }
6392
6811
  _consumeNewlineOrEOF() {
6393
6812
  if (this._check("NEWLINE") || this._check("EOF")) {
@@ -6417,12 +6836,52 @@ var Parser = class {
6417
6836
  }
6418
6837
  };
6419
6838
 
6420
- // src/tscript/runtime.ts
6839
+ // src/forgescript/runtime.ts
6840
+ var TArray = class _TArray {
6841
+ items;
6842
+ constructor(items = []) {
6843
+ this.items = items;
6844
+ }
6845
+ size() {
6846
+ return this.items.length;
6847
+ }
6848
+ get(i) {
6849
+ return i >= 0 && i < this.items.length ? this.items[i] : NaN;
6850
+ }
6851
+ set(i, v) {
6852
+ if (i >= 0 && i < this.items.length) this.items[i] = v;
6853
+ }
6854
+ push(v) {
6855
+ this.items.push(v);
6856
+ }
6857
+ pop() {
6858
+ return this.items.length > 0 ? this.items.pop() : NaN;
6859
+ }
6860
+ remove(i) {
6861
+ if (i < 0 || i >= this.items.length) return NaN;
6862
+ return this.items.splice(i, 1)[0];
6863
+ }
6864
+ clear() {
6865
+ this.items.length = 0;
6866
+ }
6867
+ includes(v) {
6868
+ return this.items.includes(v);
6869
+ }
6870
+ indexOf(v) {
6871
+ return this.items.indexOf(v);
6872
+ }
6873
+ slice(start, end) {
6874
+ return new _TArray(this.items.slice(start, end));
6875
+ }
6876
+ join(sep) {
6877
+ return this.items.map(String).join(sep);
6878
+ }
6879
+ };
6421
6880
  function _num(v, ctx) {
6422
6881
  if (v instanceof Series2) return v.value;
6423
6882
  if (typeof v === "number") return v;
6424
6883
  if (typeof v === "boolean") return v ? 1 : 0;
6425
- throw new TypeError(`[TScript] ${ctx}: expected number, got ${typeof v}`);
6884
+ throw new TypeError(`[ForgeScript] ${ctx}: expected number, got ${typeof v}`);
6426
6885
  }
6427
6886
  function _bool(v) {
6428
6887
  if (v instanceof Series2) return !isNaN(v.value) && v.value !== 0;
@@ -6430,11 +6889,31 @@ function _bool(v) {
6430
6889
  if (typeof v === "boolean") return v;
6431
6890
  return Boolean(v);
6432
6891
  }
6433
- var TScriptRuntime = class {
6892
+ var ForgeScriptRuntime = class _ForgeScriptRuntime {
6434
6893
  _program;
6435
6894
  _scope = /* @__PURE__ */ new Map();
6436
6895
  _plotSeries = [];
6437
6896
  // one entry per plot() call order
6897
+ _plotMetas = [];
6898
+ _hlines = [];
6899
+ _fills = [];
6900
+ _bgcolors = [];
6901
+ _barcolors = [];
6902
+ _shapes = [];
6903
+ _tables = [];
6904
+ _functions = /* @__PURE__ */ new Map();
6905
+ /** Tracks which vars have been initialized (for var/varip init-once semantics). */
6906
+ _varInitialized = /* @__PURE__ */ new Set();
6907
+ /** varip variables update intra-bar; we track their names to handle them differently. */
6908
+ _varipNames = /* @__PURE__ */ new Set();
6909
+ /** Current bar index for use in output functions. */
6910
+ _barIndex = 0;
6911
+ /** Total number of bars in the current run (for barstate). */
6912
+ _totalBars = 0;
6913
+ /** Current bar for output timestamp. */
6914
+ _currentBar = null;
6915
+ /** Whether this indicator is an overlay. */
6916
+ _overlay = false;
6438
6917
  // Public metadata set by indicator() declaration
6439
6918
  title = "Script";
6440
6919
  constructor(src) {
@@ -6442,6 +6921,8 @@ var TScriptRuntime = class {
6442
6921
  for (const stmt of this._program.stmts) {
6443
6922
  if (stmt.kind === "IndicatorDecl") {
6444
6923
  this.title = stmt.title;
6924
+ const ov = stmt.namedArgs.get("overlay");
6925
+ if (ov && ov.kind === "BoolLit") this._overlay = ov.value;
6445
6926
  break;
6446
6927
  }
6447
6928
  }
@@ -6456,6 +6937,8 @@ var TScriptRuntime = class {
6456
6937
  * @returns the current values of all `plot()` series after this bar.
6457
6938
  */
6458
6939
  execBar(bar, barIndex) {
6940
+ this._currentBar = bar;
6941
+ this._barIndex = barIndex;
6459
6942
  this._pushBuiltin("open", bar.open);
6460
6943
  this._pushBuiltin("high", bar.high);
6461
6944
  this._pushBuiltin("low", bar.low);
@@ -6478,11 +6961,16 @@ var TScriptRuntime = class {
6478
6961
  */
6479
6962
  run(bars) {
6480
6963
  this.reset();
6964
+ this._totalBars = bars.length;
6481
6965
  for (let i = 0; i < bars.length; i++) {
6482
6966
  this.execBar(bars[i], i);
6483
6967
  }
6484
6968
  return this.getPlots(bars);
6485
6969
  }
6970
+ /** Set total bar count for barstate properties (call before execBar loop). */
6971
+ setTotalBars(n) {
6972
+ this._totalBars = n;
6973
+ }
6486
6974
  /**
6487
6975
  * Snapshot the current plot series as IndicatorPoint arrays.
6488
6976
  * Must be called after `run()` or after the last `execBar()`.
@@ -6498,6 +6986,21 @@ var TScriptRuntime = class {
6498
6986
  })).filter((p) => !isNaN(p.value));
6499
6987
  });
6500
6988
  }
6989
+ /** Return the full script result including all output types. */
6990
+ getResult(bars) {
6991
+ return {
6992
+ title: this.title,
6993
+ plots: this.getPlots(bars),
6994
+ plotMetas: this._plotMetas,
6995
+ hlines: this._hlines,
6996
+ fills: this._fills,
6997
+ bgcolors: this._bgcolors,
6998
+ barcolors: this._barcolors,
6999
+ shapes: this._shapes,
7000
+ tables: this._tables,
7001
+ overlay: this._overlay
7002
+ };
7003
+ }
6501
7004
  /** Reset all persistent series state (call on symbol/timeframe change). */
6502
7005
  reset() {
6503
7006
  for (const v of this._scope.values()) {
@@ -6505,6 +7008,17 @@ var TScriptRuntime = class {
6505
7008
  }
6506
7009
  for (const s of this._plotSeries) s.reset();
6507
7010
  this._plotIdx = 0;
7011
+ this._varInitialized.clear();
7012
+ this._internalState.clear();
7013
+ this._hlines.length = 0;
7014
+ this._fills.length = 0;
7015
+ this._bgcolors.length = 0;
7016
+ this._barcolors.length = 0;
7017
+ this._shapes.length = 0;
7018
+ this._tables.length = 0;
7019
+ this._plotMetas.length = 0;
7020
+ this._functions.clear();
7021
+ this._tableCounter = 0;
6508
7022
  }
6509
7023
  // ─── Private: statement execution ───────────────────────────────────────────
6510
7024
  _execStmt(stmt) {
@@ -6512,17 +7026,102 @@ var TScriptRuntime = class {
6512
7026
  case "IndicatorDecl":
6513
7027
  break;
6514
7028
  // handled at construction
7029
+ case "VarDeclStmt":
7030
+ this._execVarDecl(stmt);
7031
+ break;
6515
7032
  case "AssignStmt":
6516
7033
  this._execAssign(stmt);
6517
7034
  break;
7035
+ case "ReassignStmt":
7036
+ this._execReassign(stmt);
7037
+ break;
6518
7038
  case "ExprStmt":
6519
7039
  this._evalExpr(stmt.expr);
6520
7040
  break;
7041
+ case "IfStmt":
7042
+ this._execIf(stmt);
7043
+ break;
7044
+ case "WhileStmt":
7045
+ this._execWhile(stmt);
7046
+ break;
7047
+ case "ForStmt":
7048
+ this._execFor(stmt);
7049
+ break;
7050
+ case "FnDeclStmt":
7051
+ this._functions.set(stmt.name, stmt);
7052
+ break;
7053
+ }
7054
+ }
7055
+ _execIf(stmt) {
7056
+ const branch = _bool(this._evalExpr(stmt.condition)) ? stmt.then : stmt.else_;
7057
+ for (const s of branch) this._execStmt(s);
7058
+ }
7059
+ _execWhile(stmt) {
7060
+ const MAX_ITER = 1e4;
7061
+ let count = 0;
7062
+ while (_bool(this._evalExpr(stmt.condition))) {
7063
+ if (++count > MAX_ITER) {
7064
+ throw new RangeError("[ForgeScript] while loop exceeded 10,000 iterations \u2014 possible infinite loop");
7065
+ }
7066
+ for (const s of stmt.body) this._execStmt(s);
7067
+ }
7068
+ }
7069
+ _execFor(stmt) {
7070
+ const startVal = _num(this._evalExpr(stmt.start), "for start");
7071
+ const endVal = _num(this._evalExpr(stmt.end), "for end");
7072
+ const stepVal = stmt.step ? _num(this._evalExpr(stmt.step), "for step") : 1;
7073
+ if (stepVal === 0) throw new RangeError("[ForgeScript] for loop step cannot be 0");
7074
+ const MAX_ITER = 1e5;
7075
+ let count = 0;
7076
+ for (let i = startVal; stepVal > 0 ? i <= endVal : i >= endVal; i += stepVal) {
7077
+ if (++count > MAX_ITER) {
7078
+ throw new RangeError("[ForgeScript] for loop exceeded 100,000 iterations");
7079
+ }
7080
+ this._scope.set(stmt.name, i);
7081
+ for (const s of stmt.body) this._execStmt(s);
7082
+ }
7083
+ }
7084
+ /** var / varip — only initialize on the first bar (bar_index 0). */
7085
+ _execVarDecl(stmt) {
7086
+ if (stmt.modifier === "varip") this._varipNames.add(stmt.name);
7087
+ if (!this._varInitialized.has(stmt.name)) {
7088
+ this._varInitialized.add(stmt.name);
7089
+ const rhs = this._evalExpr(stmt.value);
7090
+ if (typeof rhs === "number" || rhs instanceof Series2) {
7091
+ const series = new Series2(1e3);
7092
+ series.push(rhs instanceof Series2 ? rhs.value : rhs);
7093
+ this._scope.set(stmt.name, series);
7094
+ } else {
7095
+ this._scope.set(stmt.name, rhs);
7096
+ }
7097
+ } else {
7098
+ const existing = this._scope.get(stmt.name);
7099
+ if (existing instanceof Series2) {
7100
+ existing.push(existing.value);
7101
+ }
7102
+ }
7103
+ }
7104
+ /** := reassignment — variable must already exist. */
7105
+ _execReassign(stmt) {
7106
+ const rhs = this._evalExpr(stmt.value);
7107
+ const existing = this._scope.get(stmt.name);
7108
+ if (existing === void 0) {
7109
+ throw new ReferenceError(`[ForgeScript] Cannot reassign undefined variable '${stmt.name}'`);
7110
+ }
7111
+ if (existing instanceof Series2) {
7112
+ const numVal = _num(rhs, `reassignment to '${stmt.name}'`);
7113
+ existing.push(numVal);
7114
+ } else {
7115
+ this._scope.set(stmt.name, rhs);
6521
7116
  }
6522
7117
  }
6523
7118
  _execAssign(stmt) {
6524
7119
  const rhs = this._evalExpr(stmt.value);
6525
7120
  const existing = this._scope.get(stmt.name);
7121
+ if (rhs instanceof TArray || typeof rhs === "string" && rhs.startsWith("__table_")) {
7122
+ this._scope.set(stmt.name, rhs);
7123
+ return;
7124
+ }
6526
7125
  if (existing instanceof Series2) {
6527
7126
  existing.push(_num(rhs, `assignment to '${stmt.name}'`));
6528
7127
  } else {
@@ -6545,12 +7144,18 @@ var TScriptRuntime = class {
6545
7144
  return expr.value;
6546
7145
  case "BoolLit":
6547
7146
  return expr.value;
7147
+ case "ColorLit":
7148
+ return expr.value;
6548
7149
  case "Identifier":
6549
7150
  return this._evalIdent(expr);
6550
7151
  case "IndexExpr":
6551
7152
  return this._evalIndex(expr);
6552
7153
  case "CallExpr":
6553
7154
  return this._evalCall(expr);
7155
+ case "NsCallExpr":
7156
+ return this._evalNsCall(expr);
7157
+ case "MemberExpr":
7158
+ return this._evalMember(expr);
6554
7159
  case "BinaryExpr":
6555
7160
  return this._evalBinary(expr);
6556
7161
  case "UnaryExpr":
@@ -6564,7 +7169,7 @@ var TScriptRuntime = class {
6564
7169
  _evalIdent(expr) {
6565
7170
  const v = this._scope.get(expr.name);
6566
7171
  if (v === void 0) {
6567
- throw new ReferenceError(`[TScript] Undefined variable '${expr.name}'`);
7172
+ throw new ReferenceError(`[ForgeScript] Undefined variable '${expr.name}'`);
6568
7173
  }
6569
7174
  return v;
6570
7175
  }
@@ -6572,13 +7177,20 @@ var TScriptRuntime = class {
6572
7177
  const series = this._evalExpr(expr.series);
6573
7178
  const idx = _num(this._evalExpr(expr.index), "series index");
6574
7179
  if (!(series instanceof Series2)) {
6575
- throw new TypeError("[TScript] Index operator [] can only be applied to a series");
7180
+ throw new TypeError("[ForgeScript] Index operator [] can only be applied to a series");
6576
7181
  }
6577
7182
  return series.get(Math.round(idx));
6578
7183
  }
6579
7184
  _evalBinary(expr) {
6580
- const l = _num(this._evalExpr(expr.left), `left of '${expr.op}'`);
6581
- const r = _num(this._evalExpr(expr.right), `right of '${expr.op}'`);
7185
+ const rawL = this._evalExpr(expr.left);
7186
+ const rawR = this._evalExpr(expr.right);
7187
+ if (expr.op === "+" && (typeof rawL === "string" || typeof rawR === "string")) {
7188
+ const ls = rawL instanceof Series2 ? String(rawL.value) : String(rawL);
7189
+ const rs = rawR instanceof Series2 ? String(rawR.value) : String(rawR);
7190
+ return ls + rs;
7191
+ }
7192
+ const l = _num(rawL, `left of '${expr.op}'`);
7193
+ const r = _num(rawR, `right of '${expr.op}'`);
6582
7194
  switch (expr.op) {
6583
7195
  case "+":
6584
7196
  return l + r;
@@ -6616,6 +7228,349 @@ var TScriptRuntime = class {
6616
7228
  if (expr.op === "and") return l ? _bool(this._evalExpr(expr.right)) : false;
6617
7229
  return l ? true : _bool(this._evalExpr(expr.right));
6618
7230
  }
7231
+ // ─── Namespace call dispatch ───────────────────────────────────────────────
7232
+ _evalNsCall(expr) {
7233
+ const { namespace, fn, args } = expr;
7234
+ switch (namespace) {
7235
+ case "ta":
7236
+ return this._evalTaCall(fn, args);
7237
+ case "math":
7238
+ return this._evalMathCall(fn, args);
7239
+ case "color":
7240
+ return this._evalColorCall(fn, args);
7241
+ case "str":
7242
+ return this._evalStrCall(fn, args);
7243
+ case "array":
7244
+ return this._evalArrayCall(fn, args);
7245
+ case "table":
7246
+ return this._evalTableCall(fn, args, expr.namedArgs);
7247
+ case "input":
7248
+ return this._evalInputNsCall(fn, args);
7249
+ default:
7250
+ throw new ReferenceError(`[ForgeScript] Unknown namespace '${namespace}'`);
7251
+ }
7252
+ }
7253
+ _evalTaCall(method, args) {
7254
+ switch (method) {
7255
+ case "sma":
7256
+ return this._ta_sma(args);
7257
+ case "ema":
7258
+ return this._ta_ema(args);
7259
+ case "wma":
7260
+ return this._ta_wma(args);
7261
+ case "rma":
7262
+ return this._ta_rma(args);
7263
+ case "stdev":
7264
+ return this._ta_stdev(args);
7265
+ case "highest":
7266
+ return this._ta_rolling(args, Math.max);
7267
+ case "lowest":
7268
+ return this._ta_rolling(args, Math.min);
7269
+ case "change":
7270
+ return this._ta_change(args);
7271
+ case "mom":
7272
+ return this._ta_change(args);
7273
+ case "atr":
7274
+ return this._ta_atr(args);
7275
+ case "rsi":
7276
+ return this._ta_rsi(args);
7277
+ case "crossover":
7278
+ return this._ta_crossover(args);
7279
+ case "crossunder":
7280
+ return this._ta_crossunder(args);
7281
+ case "cross":
7282
+ return this._ta_cross(args);
7283
+ case "barssince":
7284
+ return this._ta_barssince(args);
7285
+ case "macd":
7286
+ return this._ta_macd(args);
7287
+ case "bb":
7288
+ return this._ta_bb(args);
7289
+ case "stoch":
7290
+ return this._ta_stoch(args);
7291
+ case "obv":
7292
+ return this._ta_obv(args);
7293
+ case "correlation":
7294
+ return this._ta_correlation(args);
7295
+ case "dev":
7296
+ return this._ta_dev(args);
7297
+ case "variance":
7298
+ return this._ta_variance(args);
7299
+ case "cum":
7300
+ return this._ta_cum(args);
7301
+ case "sum":
7302
+ return this._ta_sum(args);
7303
+ case "valuewhen":
7304
+ return this._ta_valuewhen(args);
7305
+ case "pivothigh":
7306
+ return this._ta_pivothigh(args);
7307
+ case "pivotlow":
7308
+ return this._ta_pivotlow(args);
7309
+ case "tr":
7310
+ return this._ta_tr(args);
7311
+ case "swma":
7312
+ return this._ta_swma(args);
7313
+ case "vwma":
7314
+ return this._ta_vwma(args);
7315
+ case "rising":
7316
+ return this._ta_rising(args);
7317
+ case "falling":
7318
+ return this._ta_falling(args);
7319
+ default:
7320
+ throw new ReferenceError(`[ForgeScript] Unknown ta function 'ta.${method}'`);
7321
+ }
7322
+ }
7323
+ _evalMathCall(method, args) {
7324
+ switch (method) {
7325
+ case "abs":
7326
+ return Math.abs(_num(this._evalExpr(args[0]), "math.abs()"));
7327
+ case "round":
7328
+ return Math.round(_num(this._evalExpr(args[0]), "math.round()"));
7329
+ case "floor":
7330
+ return Math.floor(_num(this._evalExpr(args[0]), "math.floor()"));
7331
+ case "ceil":
7332
+ return Math.ceil(_num(this._evalExpr(args[0]), "math.ceil()"));
7333
+ case "sqrt":
7334
+ return Math.sqrt(_num(this._evalExpr(args[0]), "math.sqrt()"));
7335
+ case "log":
7336
+ return Math.log(_num(this._evalExpr(args[0]), "math.log()"));
7337
+ case "log10":
7338
+ return Math.log10(_num(this._evalExpr(args[0]), "math.log10()"));
7339
+ case "sign":
7340
+ return Math.sign(_num(this._evalExpr(args[0]), "math.sign()"));
7341
+ case "exp":
7342
+ return Math.exp(_num(this._evalExpr(args[0]), "math.exp()"));
7343
+ case "pow": {
7344
+ const base = _num(this._evalExpr(args[0]), "math.pow() base");
7345
+ const exp = _num(this._evalExpr(args[1]), "math.pow() exponent");
7346
+ return Math.pow(base, exp);
7347
+ }
7348
+ case "max":
7349
+ return Math.max(...args.map((a) => _num(this._evalExpr(a), "math.max()")));
7350
+ case "min":
7351
+ return Math.min(...args.map((a) => _num(this._evalExpr(a), "math.min()")));
7352
+ case "avg": {
7353
+ const vals = args.map((a) => _num(this._evalExpr(a), "math.avg()"));
7354
+ return vals.reduce((s, v) => s + v, 0) / vals.length;
7355
+ }
7356
+ case "sum": {
7357
+ return args.map((a) => _num(this._evalExpr(a), "math.sum()")).reduce((s, v) => s + v, 0);
7358
+ }
7359
+ case "todegrees":
7360
+ return _num(this._evalExpr(args[0]), "math.todegrees()") * (180 / Math.PI);
7361
+ case "toradians":
7362
+ return _num(this._evalExpr(args[0]), "math.toradians()") * (Math.PI / 180);
7363
+ case "sin":
7364
+ return Math.sin(_num(this._evalExpr(args[0]), "math.sin()"));
7365
+ case "cos":
7366
+ return Math.cos(_num(this._evalExpr(args[0]), "math.cos()"));
7367
+ case "tan":
7368
+ return Math.tan(_num(this._evalExpr(args[0]), "math.tan()"));
7369
+ case "asin":
7370
+ return Math.asin(_num(this._evalExpr(args[0]), "math.asin()"));
7371
+ case "acos":
7372
+ return Math.acos(_num(this._evalExpr(args[0]), "math.acos()"));
7373
+ case "atan":
7374
+ return Math.atan(_num(this._evalExpr(args[0]), "math.atan()"));
7375
+ case "random":
7376
+ return Math.random();
7377
+ default:
7378
+ throw new ReferenceError(`[ForgeScript] Unknown math function 'math.${method}'`);
7379
+ }
7380
+ }
7381
+ _evalColorCall(method, args) {
7382
+ if (method === "new") {
7383
+ const base = String(this._evalExpr(args[0]));
7384
+ const transp = _num(this._evalExpr(args[1]), "color.new() transparency");
7385
+ const alpha = Math.round(255 * (1 - transp / 100));
7386
+ const hex = base.startsWith("#") ? base.slice(0, 7) : base;
7387
+ return hex + alpha.toString(16).padStart(2, "0");
7388
+ }
7389
+ if (method === "rgb") {
7390
+ const r = Math.round(_num(this._evalExpr(args[0]), "color.rgb() r"));
7391
+ const g = Math.round(_num(this._evalExpr(args[1]), "color.rgb() g"));
7392
+ const b = Math.round(_num(this._evalExpr(args[2]), "color.rgb() b"));
7393
+ const t = args.length > 3 ? _num(this._evalExpr(args[3]), "color.rgb() transp") : 0;
7394
+ const a = Math.round(255 * (1 - t / 100));
7395
+ 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")}`;
7396
+ }
7397
+ throw new ReferenceError(`[ForgeScript] Unknown color function 'color.${method}'`);
7398
+ }
7399
+ _evalStrCall(method, args) {
7400
+ switch (method) {
7401
+ case "tostring":
7402
+ return String(this._evalExpr(args[0]));
7403
+ case "tonumber": {
7404
+ const v = Number(this._evalExpr(args[0]));
7405
+ return isNaN(v) ? NaN : v;
7406
+ }
7407
+ case "format": {
7408
+ let fmt = String(this._evalExpr(args[0]));
7409
+ for (let i = 1; i < args.length; i++) {
7410
+ fmt = fmt.replace(`{${i - 1}}`, String(this._evalExpr(args[i])));
7411
+ }
7412
+ return fmt;
7413
+ }
7414
+ case "length":
7415
+ return String(this._evalExpr(args[0])).length;
7416
+ case "trim":
7417
+ return String(this._evalExpr(args[0])).trim();
7418
+ case "contains": {
7419
+ const s = String(this._evalExpr(args[0]));
7420
+ const sub = String(this._evalExpr(args[1]));
7421
+ return s.includes(sub);
7422
+ }
7423
+ case "substring": {
7424
+ const s = String(this._evalExpr(args[0]));
7425
+ const start = Math.trunc(Number(this._evalExpr(args[1])));
7426
+ const end = args.length > 2 ? Math.trunc(Number(this._evalExpr(args[2]))) : void 0;
7427
+ return s.substring(start, end);
7428
+ }
7429
+ case "replace_all": {
7430
+ const s = String(this._evalExpr(args[0]));
7431
+ const target = String(this._evalExpr(args[1]));
7432
+ const replacement = String(this._evalExpr(args[2]));
7433
+ return s.split(target).join(replacement);
7434
+ }
7435
+ case "upper":
7436
+ return String(this._evalExpr(args[0])).toUpperCase();
7437
+ case "lower":
7438
+ return String(this._evalExpr(args[0])).toLowerCase();
7439
+ case "split": {
7440
+ const s = String(this._evalExpr(args[0]));
7441
+ const sep = args.length > 1 ? String(this._evalExpr(args[1])) : ",";
7442
+ return new TArray(s.split(sep));
7443
+ }
7444
+ default:
7445
+ throw new ReferenceError(`[ForgeScript] Unknown str function 'str.${method}'`);
7446
+ }
7447
+ }
7448
+ // ─── Member access dispatch ──────────────────────────────────────────────────
7449
+ static _COLOR_MAP = {
7450
+ red: "#FF0000",
7451
+ green: "#00FF00",
7452
+ blue: "#0000FF",
7453
+ white: "#FFFFFF",
7454
+ black: "#000000",
7455
+ yellow: "#FFFF00",
7456
+ orange: "#FF9800",
7457
+ purple: "#9C27B0",
7458
+ aqua: "#00BCD4",
7459
+ lime: "#8BC34A",
7460
+ teal: "#009688",
7461
+ fuchsia: "#E040FB",
7462
+ silver: "#9E9E9E",
7463
+ gray: "#787B86",
7464
+ olive: "#808000",
7465
+ maroon: "#880E4F",
7466
+ navy: "#311B92"
7467
+ };
7468
+ _evalMember(expr) {
7469
+ const { object, prop } = expr;
7470
+ switch (object) {
7471
+ case "color":
7472
+ return _ForgeScriptRuntime._COLOR_MAP[prop] ?? NaN;
7473
+ case "syminfo": {
7474
+ switch (prop) {
7475
+ case "tickerid":
7476
+ return this._scope.get("__syminfo_tickerid") ?? "";
7477
+ case "ticker":
7478
+ return this._scope.get("__syminfo_ticker") ?? "";
7479
+ case "mintick":
7480
+ return this._scope.get("__syminfo_mintick") ?? 0.01;
7481
+ case "pointvalue":
7482
+ return this._scope.get("__syminfo_pointvalue") ?? 1;
7483
+ case "currency":
7484
+ return "USD";
7485
+ case "type":
7486
+ return "stock";
7487
+ default:
7488
+ return NaN;
7489
+ }
7490
+ }
7491
+ case "timeframe": {
7492
+ switch (prop) {
7493
+ case "period":
7494
+ return this._scope.get("__timeframe_period") ?? "1";
7495
+ case "multiplier":
7496
+ return this._scope.get("__timeframe_multiplier") ?? 1;
7497
+ case "isintraday":
7498
+ return true;
7499
+ case "isdaily":
7500
+ return false;
7501
+ case "isweekly":
7502
+ return false;
7503
+ case "ismonthly":
7504
+ return false;
7505
+ default:
7506
+ return NaN;
7507
+ }
7508
+ }
7509
+ // Pine v6 enum-like namespaces — return property name as string
7510
+ case "shape":
7511
+ return prop;
7512
+ // shape.circle → "circle"
7513
+ case "location":
7514
+ return prop;
7515
+ // location.abovebar → "abovebar"
7516
+ case "plot":
7517
+ return prop.replace(/^style_/, "");
7518
+ // plot.style_line → "line"
7519
+ case "math": {
7520
+ switch (prop) {
7521
+ case "pi":
7522
+ return Math.PI;
7523
+ case "e":
7524
+ return Math.E;
7525
+ default:
7526
+ return NaN;
7527
+ }
7528
+ }
7529
+ case "barstate": {
7530
+ switch (prop) {
7531
+ case "islast":
7532
+ return this._totalBars > 0 && this._barIndex === this._totalBars - 1;
7533
+ case "isfirst":
7534
+ return this._barIndex === 0;
7535
+ case "isconfirmed":
7536
+ return true;
7537
+ // historical bars are always confirmed
7538
+ case "isnew":
7539
+ return true;
7540
+ // each bar is "new" in per-bar execution
7541
+ case "isrealtime":
7542
+ return false;
7543
+ // ForgeScript runs on historical data
7544
+ case "ishistory":
7545
+ return true;
7546
+ default:
7547
+ return false;
7548
+ }
7549
+ }
7550
+ case "position":
7551
+ return prop;
7552
+ // position.top_right → "top_right"
7553
+ case "size":
7554
+ return prop;
7555
+ // size.large → "large"
7556
+ case "text": {
7557
+ return prop.replace(/^align_/, "");
7558
+ }
7559
+ case "bar_index":
7560
+ return this._barIndex;
7561
+ case "close":
7562
+ case "open":
7563
+ case "high":
7564
+ case "low":
7565
+ case "volume":
7566
+ case "time": {
7567
+ const v = this._scope.get(object);
7568
+ return v === void 0 ? NaN : v;
7569
+ }
7570
+ default:
7571
+ throw new ReferenceError(`[ForgeScript] Unknown member access '${object}.${prop}'`);
7572
+ }
7573
+ }
6619
7574
  // ─── Built-in function dispatch ──────────────────────────────────────────────
6620
7575
  _plotIdx = 0;
6621
7576
  _evalCall(expr) {
@@ -6623,16 +7578,32 @@ var TScriptRuntime = class {
6623
7578
  const args = expr.args;
6624
7579
  switch (fn) {
6625
7580
  // ── Utility ────────────────────────────────────────────────────────────
6626
- case "input": {
7581
+ case "input":
7582
+ case "input.text_area":
7583
+ case "input.string": {
6627
7584
  if (args.length === 0) return 0;
6628
7585
  const def = this._evalExpr(args[0]);
6629
- return typeof def === "string" ? parseFloat(def) || 0 : _num(def, "input()");
7586
+ if (fn === "input.text_area" || fn === "input.string") return typeof def === "string" ? def : String(def);
7587
+ if (typeof def === "string") {
7588
+ const n = parseFloat(def);
7589
+ return isNaN(n) ? def : n;
7590
+ }
7591
+ return _num(def, "input()");
6630
7592
  }
6631
7593
  case "plot": {
6632
7594
  const value = _num(this._evalExpr(args[0]), "plot()");
6633
7595
  const idx = this._plotIdx++;
6634
7596
  if (idx >= this._plotSeries.length) {
6635
7597
  this._plotSeries.push(new Series2(1e3));
7598
+ const na = expr.namedArgs;
7599
+ const title = na?.get("title") ? String(this._evalExpr(na.get("title"))) : `Plot ${idx}`;
7600
+ const color = na?.get("color") ? String(this._evalExpr(na.get("color"))) : void 0;
7601
+ const linewidth = na?.get("linewidth") ? _num(this._evalExpr(na.get("linewidth")), "plot linewidth") : void 0;
7602
+ const style = na?.get("style") ? String(this._evalExpr(na.get("style"))) : "line";
7603
+ const meta = { title, style };
7604
+ if (color !== void 0) meta.color = color;
7605
+ if (linewidth !== void 0) meta.linewidth = linewidth;
7606
+ this._plotMetas.push(meta);
6636
7607
  }
6637
7608
  this._plotSeries[idx].push(value);
6638
7609
  return value;
@@ -6693,14 +7664,74 @@ var TScriptRuntime = class {
6693
7664
  return this._ta_change(args);
6694
7665
  case "atr":
6695
7666
  return this._ta_atr(args);
7667
+ case "rsi":
7668
+ return this._ta_rsi(args);
6696
7669
  case "crossover":
6697
7670
  return this._ta_crossover(args);
6698
7671
  case "crossunder":
6699
7672
  return this._ta_crossunder(args);
7673
+ case "cross":
7674
+ return this._ta_cross(args);
6700
7675
  case "barssince":
6701
7676
  return this._ta_barssince(args);
6702
- default:
6703
- throw new ReferenceError(`[TScript] Unknown function '${fn}'`);
7677
+ // New TA functions (flat-call form)
7678
+ case "macd":
7679
+ return this._ta_macd(args);
7680
+ case "bb":
7681
+ return this._ta_bb(args);
7682
+ case "stoch":
7683
+ return this._ta_stoch(args);
7684
+ case "obv":
7685
+ return this._ta_obv(args);
7686
+ case "correlation":
7687
+ return this._ta_correlation(args);
7688
+ case "dev":
7689
+ return this._ta_dev(args);
7690
+ case "variance":
7691
+ return this._ta_variance(args);
7692
+ case "cum":
7693
+ return this._ta_cum(args);
7694
+ case "sum":
7695
+ return this._ta_sum(args);
7696
+ case "valuewhen":
7697
+ return this._ta_valuewhen(args);
7698
+ case "pivothigh":
7699
+ return this._ta_pivothigh(args);
7700
+ case "pivotlow":
7701
+ return this._ta_pivotlow(args);
7702
+ case "tr":
7703
+ return this._ta_tr(args);
7704
+ case "swma":
7705
+ return this._ta_swma(args);
7706
+ case "vwma":
7707
+ return this._ta_vwma(args);
7708
+ case "rising":
7709
+ return this._ta_rising(args);
7710
+ case "falling":
7711
+ return this._ta_falling(args);
7712
+ case "exp":
7713
+ return Math.exp(_num(this._evalExpr(args[0]), "exp()"));
7714
+ // ── Output functions ──────────────────────────────────────────────────
7715
+ case "plotshape":
7716
+ return this._callPlotshape(expr);
7717
+ case "plotchar":
7718
+ return this._callPlotshape(expr);
7719
+ // same output type
7720
+ case "plotarrow":
7721
+ return this._callPlotarrow(expr);
7722
+ case "hline":
7723
+ return this._callHline(expr);
7724
+ case "fill":
7725
+ return this._callFill(expr);
7726
+ case "bgcolor":
7727
+ return this._callBgcolor(expr);
7728
+ case "barcolor":
7729
+ return this._callBarcolor(expr);
7730
+ default: {
7731
+ const fnDecl = this._functions.get(fn);
7732
+ if (fnDecl) return this._callUserFunction(fnDecl, args);
7733
+ throw new ReferenceError(`[ForgeScript] Unknown function '${fn}'`);
7734
+ }
6704
7735
  }
6705
7736
  }
6706
7737
  // ─── TA helpers ──────────────────────────────────────────────────────────────
@@ -6713,11 +7744,42 @@ var TScriptRuntime = class {
6713
7744
  s.push(v);
6714
7745
  return s;
6715
7746
  }
6716
- throw new TypeError(`[TScript] ${ctx}: expected a series`);
7747
+ throw new TypeError(`[ForgeScript] ${ctx}: expected a series`);
6717
7748
  }
6718
7749
  _argInt(arg, ctx) {
6719
7750
  return Math.round(_num(this._evalExpr(arg), ctx));
6720
7751
  }
7752
+ _ta_rsi(args) {
7753
+ const src = this._argSeries(args[0], "rsi(src)");
7754
+ const period = this._argInt(args[1], "rsi(length)");
7755
+ if (src.length <= period) return NaN;
7756
+ const gainKey = `__rsi_gain_${args[0].kind}_${period}`;
7757
+ const lossKey = `__rsi_loss_${args[0].kind}_${period}`;
7758
+ let prevGain = this._getInternalNum(gainKey);
7759
+ let prevLoss = this._getInternalNum(lossKey);
7760
+ const change = src.get(0) - src.get(1);
7761
+ const gain = change > 0 ? change : 0;
7762
+ const loss = change < 0 ? -change : 0;
7763
+ let avgGain;
7764
+ let avgLoss;
7765
+ if (isNaN(prevGain)) {
7766
+ let gSum = 0, lSum = 0;
7767
+ for (let i = 0; i < period; i++) {
7768
+ const d = src.get(i) - src.get(i + 1);
7769
+ if (d > 0) gSum += d;
7770
+ else lSum -= d;
7771
+ }
7772
+ avgGain = gSum / period;
7773
+ avgLoss = lSum / period;
7774
+ } else {
7775
+ avgGain = (prevGain * (period - 1) + gain) / period;
7776
+ avgLoss = (prevLoss * (period - 1) + loss) / period;
7777
+ }
7778
+ this._setInternal(gainKey, avgGain);
7779
+ this._setInternal(lossKey, avgLoss);
7780
+ if (avgLoss === 0) return 100;
7781
+ return 100 - 100 / (1 + avgGain / avgLoss);
7782
+ }
6721
7783
  _ta_sma(args) {
6722
7784
  const src = this._argSeries(args[0], "sma(src)");
6723
7785
  const period = this._argInt(args[1], "sma(length)");
@@ -6822,53 +7884,616 @@ var TScriptRuntime = class {
6822
7884
  }
6823
7885
  return NaN;
6824
7886
  }
6825
- // ─── Internal state helpers ──────────────────────────────────────────────────
6826
- /** Persistent scalar state for stateful TA functions (EMA, RMA, ATR). */
6827
- _internalState = /* @__PURE__ */ new Map();
6828
- _getInternalNum(key) {
6829
- return this._internalState.get(key) ?? NaN;
7887
+ // ─── Additional TA functions ─────────────────────────────────────────────────
7888
+ /** ta.cross(a, b) true when a crosses b in either direction */
7889
+ _ta_cross(args) {
7890
+ const a = this._argSeries(args[0], "cross(a)");
7891
+ const b = this._argSeries(args[1], "cross(b)");
7892
+ 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);
7893
+ }
7894
+ /** ta.macd(src, fast, slow, signal) — returns [macd, signal, hist] via internal series */
7895
+ _ta_macd(args) {
7896
+ const src = this._argSeries(args[0], "macd(src)");
7897
+ const fast = this._argInt(args[1], "macd(fastlen)");
7898
+ const slow = this._argInt(args[2], "macd(slowlen)");
7899
+ const signal = this._argInt(args[3], "macd(signallen)");
7900
+ const fastKey = `__macd_fast_${args[0].kind}_${fast}`;
7901
+ const slowKey = `__macd_slow_${args[0].kind}_${slow}`;
7902
+ const sigKey = `__macd_sig_${args[0].kind}_${fast}_${slow}_${signal}`;
7903
+ const cur = src.get(0);
7904
+ const kFast = 2 / (fast + 1);
7905
+ const kSlow = 2 / (slow + 1);
7906
+ const kSig = 2 / (signal + 1);
7907
+ let prevFast = this._getInternalNum(fastKey);
7908
+ let prevSlow = this._getInternalNum(slowKey);
7909
+ let prevSig = this._getInternalNum(sigKey);
7910
+ const fastEMA = isNaN(prevFast) ? src.length >= fast ? this._smaOfSeries(src, fast) : NaN : cur * kFast + prevFast * (1 - kFast);
7911
+ const slowEMA = isNaN(prevSlow) ? src.length >= slow ? this._smaOfSeries(src, slow) : NaN : cur * kSlow + prevSlow * (1 - kSlow);
7912
+ this._setInternal(fastKey, fastEMA);
7913
+ this._setInternal(slowKey, slowEMA);
7914
+ const macdLine = fastEMA - slowEMA;
7915
+ const sigLine = isNaN(prevSig) ? macdLine : macdLine * kSig + prevSig * (1 - kSig);
7916
+ this._setInternal(sigKey, sigLine);
7917
+ const hist = macdLine - sigLine;
7918
+ this._pushBuiltin("__macd_line", macdLine);
7919
+ this._pushBuiltin("__macd_signal", sigLine);
7920
+ this._pushBuiltin("__macd_hist", hist);
7921
+ return macdLine;
7922
+ }
7923
+ /** ta.bb(src, length, mult) — returns middle band; stores upper/lower in named series */
7924
+ _ta_bb(args) {
7925
+ const src = this._argSeries(args[0], "bb(src)");
7926
+ const period = this._argInt(args[1], "bb(length)");
7927
+ const mult = _num(this._evalExpr(args[2]), "bb(mult)");
7928
+ if (src.length < period) return NaN;
7929
+ const middle = this._smaOfSeries(src, period);
7930
+ let variance = 0;
7931
+ for (let i = 0; i < period; i++) {
7932
+ const d = src.get(i) - middle;
7933
+ variance += d * d;
7934
+ }
7935
+ const stdevVal = Math.sqrt(variance / period);
7936
+ const upper = middle + mult * stdevVal;
7937
+ const lower = middle - mult * stdevVal;
7938
+ this._pushBuiltin("__bb_upper", upper);
7939
+ this._pushBuiltin("__bb_middle", middle);
7940
+ this._pushBuiltin("__bb_lower", lower);
7941
+ return middle;
7942
+ }
7943
+ /** ta.stoch(source, high, low, length) — %K */
7944
+ _ta_stoch(args) {
7945
+ const src = this._argSeries(args[0], "stoch(src)");
7946
+ const hi = this._argSeries(args[1], "stoch(high)");
7947
+ const lo = this._argSeries(args[2], "stoch(low)");
7948
+ const period = this._argInt(args[3], "stoch(length)");
7949
+ if (hi.length < period || lo.length < period) return NaN;
7950
+ let hh = -Infinity, ll = Infinity;
7951
+ for (let i = 0; i < period; i++) {
7952
+ hh = Math.max(hh, hi.get(i));
7953
+ ll = Math.min(ll, lo.get(i));
7954
+ }
7955
+ const denom = hh - ll;
7956
+ return denom === 0 ? 50 : 100 * (src.get(0) - ll) / denom;
6830
7957
  }
6831
- _setInternal(key, value) {
6832
- this._internalState.set(key, value);
7958
+ /** ta.obv — On Balance Volume */
7959
+ _ta_obv(_args) {
7960
+ const close = this._scope.get("close");
7961
+ const volume = this._scope.get("volume");
7962
+ if (!(close instanceof Series2) || !(volume instanceof Series2)) return NaN;
7963
+ if (close.length < 2) return volume.get(0);
7964
+ const stateKey = "__obv";
7965
+ const prev = this._getInternalNum(stateKey);
7966
+ const prevOBV = isNaN(prev) ? 0 : prev;
7967
+ const diff = close.get(0) - close.get(1);
7968
+ const result = diff > 0 ? prevOBV + volume.get(0) : diff < 0 ? prevOBV - volume.get(0) : prevOBV;
7969
+ this._setInternal(stateKey, result);
7970
+ return result;
6833
7971
  }
6834
- /** Push a value into a named built-in Series, creating it if needed. */
6835
- _pushBuiltin(name, value) {
6836
- let series = this._scope.get(name);
6837
- if (!(series instanceof Series2)) {
6838
- series = new Series2(1e3);
6839
- this._scope.set(name, series);
6840
- }
6841
- series.push(value);
7972
+ /** ta.correlation(src1, src2, length) Pearson r */
7973
+ _ta_correlation(args) {
7974
+ const a = this._argSeries(args[0], "correlation(src1)");
7975
+ const b = this._argSeries(args[1], "correlation(src2)");
7976
+ const period = this._argInt(args[2], "correlation(length)");
7977
+ if (a.length < period || b.length < period) return NaN;
7978
+ let sumA = 0, sumB = 0, sumAB = 0, sumA2 = 0, sumB2 = 0;
7979
+ for (let i = 0; i < period; i++) {
7980
+ const av = a.get(i), bv = b.get(i);
7981
+ sumA += av;
7982
+ sumB += bv;
7983
+ sumAB += av * bv;
7984
+ sumA2 += av * av;
7985
+ sumB2 += bv * bv;
7986
+ }
7987
+ const n = period;
7988
+ const denom = Math.sqrt((n * sumA2 - sumA * sumA) * (n * sumB2 - sumB * sumB));
7989
+ return denom === 0 ? 0 : (n * sumAB - sumA * sumB) / denom;
7990
+ }
7991
+ /** ta.dev(src, length) — mean absolute deviation */
7992
+ _ta_dev(args) {
7993
+ const src = this._argSeries(args[0], "dev(src)");
7994
+ const period = this._argInt(args[1], "dev(length)");
7995
+ if (src.length < period) return NaN;
7996
+ const mean = this._smaOfSeries(src, period);
7997
+ let sum = 0;
7998
+ for (let i = 0; i < period; i++) sum += Math.abs(src.get(i) - mean);
7999
+ return sum / period;
6842
8000
  }
6843
- /** Compute a simple rolling average directly from a Series (no state). */
6844
- _smaOfSeries(src, period) {
8001
+ /** ta.variance(src, length) */
8002
+ _ta_variance(args) {
8003
+ const src = this._argSeries(args[0], "variance(src)");
8004
+ const period = this._argInt(args[1], "variance(length)");
8005
+ if (src.length < period) return NaN;
8006
+ const mean = this._smaOfSeries(src, period);
6845
8007
  let sum = 0;
6846
- for (let i = 0; i < period; i++) sum += src.get(i);
8008
+ for (let i = 0; i < period; i++) {
8009
+ const d = src.get(i) - mean;
8010
+ sum += d * d;
8011
+ }
6847
8012
  return sum / period;
6848
8013
  }
6849
- };
6850
-
6851
- // src/tscript/TScriptIndicator.ts
6852
- var TScriptIndicator = class {
6853
- _runtime;
6854
- /** Title from the `indicator("...")` declaration, or `'Script'` if absent. */
6855
- title;
6856
- constructor(src) {
6857
- this._runtime = new TScriptRuntime(src);
6858
- this.title = this._runtime.title;
8014
+ /** ta.cum(src) — cumulative sum */
8015
+ _ta_cum(args) {
8016
+ const v = _num(this._evalExpr(args[0]), "cum(src)");
8017
+ const key = "__cum";
8018
+ const prev = this._getInternalNum(key);
8019
+ const result = (isNaN(prev) ? 0 : prev) + (isNaN(v) ? 0 : v);
8020
+ this._setInternal(key, result);
8021
+ return result;
6859
8022
  }
6860
- /**
6861
- * Execute the script over the full bar history.
6862
- *
6863
- * Each call resets internal state and replays every bar from scratch,
6864
- * ensuring the output is deterministic regardless of prior calls.
6865
- *
8023
+ /** ta.sum(src, length) — rolling sum */
8024
+ _ta_sum(args) {
8025
+ const src = this._argSeries(args[0], "sum(src)");
8026
+ const period = this._argInt(args[1], "sum(length)");
8027
+ if (src.length < period) return NaN;
8028
+ let sum = 0;
8029
+ for (let i = 0; i < period; i++) sum += src.get(i);
8030
+ return sum;
8031
+ }
8032
+ /** ta.valuewhen(condition, source, occurrence) */
8033
+ _ta_valuewhen(args) {
8034
+ const cond = this._argSeries(args[0], "valuewhen(cond)");
8035
+ const src = this._argSeries(args[1], "valuewhen(src)");
8036
+ const occ = args.length > 2 ? this._argInt(args[2], "valuewhen(occurrence)") : 0;
8037
+ let count = 0;
8038
+ for (let i = 0; i < cond.length; i++) {
8039
+ if (_bool(cond.get(i))) {
8040
+ if (count === occ) return src.get(i);
8041
+ count++;
8042
+ }
8043
+ }
8044
+ return NaN;
8045
+ }
8046
+ /** ta.pivothigh(src, leftbars, rightbars) */
8047
+ _ta_pivothigh(args) {
8048
+ const src = this._argSeries(args[0], "pivothigh(src)");
8049
+ const left = this._argInt(args[1], "pivothigh(leftbars)");
8050
+ const right = this._argInt(args[2], "pivothigh(rightbars)");
8051
+ if (src.length < left + right + 1) return NaN;
8052
+ const pivotIdx = right;
8053
+ const pivotVal = src.get(pivotIdx);
8054
+ for (let i = 1; i <= left; i++) {
8055
+ if (src.get(pivotIdx + i) >= pivotVal) return NaN;
8056
+ }
8057
+ for (let i = 1; i <= right; i++) {
8058
+ if (src.get(pivotIdx - i) >= pivotVal) return NaN;
8059
+ }
8060
+ return pivotVal;
8061
+ }
8062
+ /** ta.pivotlow(src, leftbars, rightbars) */
8063
+ _ta_pivotlow(args) {
8064
+ const src = this._argSeries(args[0], "pivotlow(src)");
8065
+ const left = this._argInt(args[1], "pivotlow(leftbars)");
8066
+ const right = this._argInt(args[2], "pivotlow(rightbars)");
8067
+ if (src.length < left + right + 1) return NaN;
8068
+ const pivotIdx = right;
8069
+ const pivotVal = src.get(pivotIdx);
8070
+ for (let i = 1; i <= left; i++) {
8071
+ if (src.get(pivotIdx + i) <= pivotVal) return NaN;
8072
+ }
8073
+ for (let i = 1; i <= right; i++) {
8074
+ if (src.get(pivotIdx - i) <= pivotVal) return NaN;
8075
+ }
8076
+ return pivotVal;
8077
+ }
8078
+ /** ta.tr(handleNa?) — True Range */
8079
+ _ta_tr(_args) {
8080
+ const high = this._scope.get("high");
8081
+ const low = this._scope.get("low");
8082
+ const close = this._scope.get("close");
8083
+ if (!(high instanceof Series2) || !(low instanceof Series2) || !(close instanceof Series2)) return NaN;
8084
+ if (close.length < 2) return high.get(0) - low.get(0);
8085
+ const h = high.get(0);
8086
+ const l = low.get(0);
8087
+ const pc = close.get(1);
8088
+ return Math.max(h - l, Math.abs(h - pc), Math.abs(l - pc));
8089
+ }
8090
+ /** ta.swma(src) — Symmetrically Weighted MA with fixed period 4 */
8091
+ _ta_swma(args) {
8092
+ const src = this._argSeries(args[0], "swma(src)");
8093
+ if (src.length < 4) return NaN;
8094
+ return (src.get(3) * 1 + src.get(2) * 3 + src.get(1) * 3 + src.get(0) * 1) / 8;
8095
+ }
8096
+ /** ta.vwma(src, length) — Volume Weighted MA */
8097
+ _ta_vwma(args) {
8098
+ const src = this._argSeries(args[0], "vwma(src)");
8099
+ const period = this._argInt(args[1], "vwma(length)");
8100
+ const volume = this._scope.get("volume");
8101
+ if (!(volume instanceof Series2)) return this._smaOfSeries(src, period);
8102
+ if (src.length < period || volume.length < period) return NaN;
8103
+ let sumSV = 0, sumV = 0;
8104
+ for (let i = 0; i < period; i++) {
8105
+ const v = volume.get(i);
8106
+ sumSV += src.get(i) * v;
8107
+ sumV += v;
8108
+ }
8109
+ return sumV === 0 ? NaN : sumSV / sumV;
8110
+ }
8111
+ /** ta.rising(src, length) */
8112
+ _ta_rising(args) {
8113
+ const src = this._argSeries(args[0], "rising(src)");
8114
+ const period = this._argInt(args[1], "rising(length)");
8115
+ if (src.length <= period) return false;
8116
+ for (let i = 0; i < period; i++) {
8117
+ if (src.get(i) <= src.get(i + 1)) return false;
8118
+ }
8119
+ return true;
8120
+ }
8121
+ /** ta.falling(src, length) */
8122
+ _ta_falling(args) {
8123
+ const src = this._argSeries(args[0], "falling(src)");
8124
+ const period = this._argInt(args[1], "falling(length)");
8125
+ if (src.length <= period) return false;
8126
+ for (let i = 0; i < period; i++) {
8127
+ if (src.get(i) >= src.get(i + 1)) return false;
8128
+ }
8129
+ return true;
8130
+ }
8131
+ // ─── Output function implementations ─────────────────────────────────────────
8132
+ _callPlotshape(expr) {
8133
+ const args = expr.args;
8134
+ const na = expr.namedArgs;
8135
+ const cond = args.length > 0 ? _bool(this._evalExpr(args[0])) : true;
8136
+ if (!cond || !this._currentBar) return NaN;
8137
+ const style = na?.get("style") ? String(this._evalExpr(na.get("style"))) : "xcross";
8138
+ const location = na?.get("location") ? String(this._evalExpr(na.get("location"))) : "abovebar";
8139
+ const color = na?.get("color") ? String(this._evalExpr(na.get("color"))) : "#FF0000";
8140
+ const text = na?.get("text") ? String(this._evalExpr(na.get("text"))) : void 0;
8141
+ const title = na?.get("title") ? String(this._evalExpr(na.get("title"))) : void 0;
8142
+ const shape = { time: this._currentBar.time, style, location, color };
8143
+ if (text !== void 0) shape.text = text;
8144
+ if (title !== void 0) shape.title = title;
8145
+ this._shapes.push(shape);
8146
+ return NaN;
8147
+ }
8148
+ _callPlotarrow(expr) {
8149
+ const args = expr.args;
8150
+ const na = expr.namedArgs;
8151
+ const value = _num(this._evalExpr(args[0]), "plotarrow()");
8152
+ if (isNaN(value) || !this._currentBar) return NaN;
8153
+ const colorUp = na?.get("colorup") ? String(this._evalExpr(na.get("colorup"))) : "#00FF00";
8154
+ const colorDown = na?.get("colordown") ? String(this._evalExpr(na.get("colordown"))) : "#FF0000";
8155
+ this._shapes.push({
8156
+ time: this._currentBar.time,
8157
+ style: value > 0 ? "arrowup" : "arrowdown",
8158
+ location: value > 0 ? "abovebar" : "belowbar",
8159
+ color: value > 0 ? colorUp : colorDown
8160
+ });
8161
+ return value;
8162
+ }
8163
+ _callHline(expr) {
8164
+ const args = expr.args;
8165
+ const na = expr.namedArgs;
8166
+ const price = _num(this._evalExpr(args[0]), "hline()");
8167
+ const title = na?.get("title") ? String(this._evalExpr(na.get("title"))) : void 0;
8168
+ const color = na?.get("color") ? String(this._evalExpr(na.get("color"))) : "#787B86";
8169
+ const lineStyle = na?.get("linestyle") ? String(this._evalExpr(na.get("linestyle"))) : "dashed";
8170
+ const lineWidth = na?.get("linewidth") ? _num(this._evalExpr(na.get("linewidth")), "hline linewidth") : 1;
8171
+ const id = `hline_${price}`;
8172
+ if (!this._hlines.some((h) => h.price === price)) {
8173
+ const hline = { id, price, color, lineStyle, lineWidth };
8174
+ if (title !== void 0) hline.title = title;
8175
+ this._hlines.push(hline);
8176
+ }
8177
+ return price;
8178
+ }
8179
+ _callFill(expr) {
8180
+ const args = expr.args;
8181
+ const na = expr.namedArgs;
8182
+ const id1 = String(this._evalExpr(args[0]));
8183
+ const id2 = String(this._evalExpr(args[1]));
8184
+ const color = na?.get("color") ? String(this._evalExpr(na.get("color"))) : args.length > 2 ? String(this._evalExpr(args[2])) : "#00000020";
8185
+ const title = na?.get("title") ? String(this._evalExpr(na.get("title"))) : void 0;
8186
+ const fill = { id1, id2, color };
8187
+ if (title !== void 0) fill.title = title;
8188
+ this._fills.push(fill);
8189
+ return NaN;
8190
+ }
8191
+ _callBgcolor(expr) {
8192
+ const args = expr.args;
8193
+ const color = String(this._evalExpr(args[0]));
8194
+ if (!this._currentBar) return NaN;
8195
+ this._bgcolors.push({ time: this._currentBar.time, color });
8196
+ return NaN;
8197
+ }
8198
+ _callBarcolor(expr) {
8199
+ const args = expr.args;
8200
+ const color = String(this._evalExpr(args[0]));
8201
+ if (!this._currentBar) return NaN;
8202
+ this._barcolors.push({ time: this._currentBar.time, color });
8203
+ return NaN;
8204
+ }
8205
+ // ─── Array namespace ──────────────────────────────────────────────────────
8206
+ _evalArrayCall(method, args) {
8207
+ switch (method) {
8208
+ case "new_float":
8209
+ case "new_int":
8210
+ case "new_bool":
8211
+ case "new_string":
8212
+ case "new": {
8213
+ const size = args.length > 0 ? Math.trunc(_num(this._evalExpr(args[0]), "array.new() size")) : 0;
8214
+ const fill = args.length > 1 ? this._evalExpr(args[1]) : NaN;
8215
+ return new TArray(Array(size).fill(fill));
8216
+ }
8217
+ case "from": {
8218
+ return new TArray(args.map((a) => this._evalExpr(a)));
8219
+ }
8220
+ case "size": {
8221
+ const arr = this._evalExpr(args[0]);
8222
+ if (!(arr instanceof TArray)) throw new TypeError("[ForgeScript] array.size() expects an array");
8223
+ return arr.size();
8224
+ }
8225
+ case "get": {
8226
+ const arr = this._evalExpr(args[0]);
8227
+ if (!(arr instanceof TArray)) throw new TypeError("[ForgeScript] array.get() expects an array");
8228
+ return arr.get(Math.trunc(_num(this._evalExpr(args[1]), "array.get() index")));
8229
+ }
8230
+ case "set": {
8231
+ const arr = this._evalExpr(args[0]);
8232
+ if (!(arr instanceof TArray)) throw new TypeError("[ForgeScript] array.set() expects an array");
8233
+ arr.set(
8234
+ Math.trunc(_num(this._evalExpr(args[1]), "array.set() index")),
8235
+ this._evalExpr(args[2])
8236
+ );
8237
+ return NaN;
8238
+ }
8239
+ case "push": {
8240
+ const arr = this._evalExpr(args[0]);
8241
+ if (!(arr instanceof TArray)) throw new TypeError("[ForgeScript] array.push() expects an array");
8242
+ arr.push(this._evalExpr(args[1]));
8243
+ return NaN;
8244
+ }
8245
+ case "pop": {
8246
+ const arr = this._evalExpr(args[0]);
8247
+ if (!(arr instanceof TArray)) throw new TypeError("[ForgeScript] array.pop() expects an array");
8248
+ return arr.pop();
8249
+ }
8250
+ case "remove": {
8251
+ const arr = this._evalExpr(args[0]);
8252
+ if (!(arr instanceof TArray)) throw new TypeError("[ForgeScript] array.remove() expects an array");
8253
+ return arr.remove(Math.trunc(_num(this._evalExpr(args[1]), "array.remove() index")));
8254
+ }
8255
+ case "clear": {
8256
+ const arr = this._evalExpr(args[0]);
8257
+ if (!(arr instanceof TArray)) throw new TypeError("[ForgeScript] array.clear() expects an array");
8258
+ arr.clear();
8259
+ return NaN;
8260
+ }
8261
+ case "includes": {
8262
+ const arr = this._evalExpr(args[0]);
8263
+ if (!(arr instanceof TArray)) throw new TypeError("[ForgeScript] array.includes() expects an array");
8264
+ return arr.includes(this._evalExpr(args[1]));
8265
+ }
8266
+ case "indexof": {
8267
+ const arr = this._evalExpr(args[0]);
8268
+ if (!(arr instanceof TArray)) throw new TypeError("[ForgeScript] array.indexof() expects an array");
8269
+ return arr.indexOf(this._evalExpr(args[1]));
8270
+ }
8271
+ case "slice": {
8272
+ const arr = this._evalExpr(args[0]);
8273
+ if (!(arr instanceof TArray)) throw new TypeError("[ForgeScript] array.slice() expects an array");
8274
+ const start = Math.trunc(_num(this._evalExpr(args[1]), "array.slice() start"));
8275
+ const end = args.length > 2 ? Math.trunc(_num(this._evalExpr(args[2]), "array.slice() end")) : void 0;
8276
+ return arr.slice(start, end);
8277
+ }
8278
+ case "join": {
8279
+ const arr = this._evalExpr(args[0]);
8280
+ if (!(arr instanceof TArray)) throw new TypeError("[ForgeScript] array.join() expects an array");
8281
+ const sep = args.length > 1 ? String(this._evalExpr(args[1])) : ",";
8282
+ return arr.join(sep);
8283
+ }
8284
+ case "sort": {
8285
+ const arr = this._evalExpr(args[0]);
8286
+ if (!(arr instanceof TArray)) throw new TypeError("[ForgeScript] array.sort() expects an array");
8287
+ arr.items.sort((a, b) => _num(a, "sort") - _num(b, "sort"));
8288
+ return NaN;
8289
+ }
8290
+ case "reverse": {
8291
+ const arr = this._evalExpr(args[0]);
8292
+ if (!(arr instanceof TArray)) throw new TypeError("[ForgeScript] array.reverse() expects an array");
8293
+ arr.items.reverse();
8294
+ return NaN;
8295
+ }
8296
+ case "avg": {
8297
+ const arr = this._evalExpr(args[0]);
8298
+ if (!(arr instanceof TArray)) throw new TypeError("[ForgeScript] array.avg() expects an array");
8299
+ if (arr.size() === 0) return NaN;
8300
+ let sum = 0;
8301
+ for (const v of arr.items) sum += _num(v, "array.avg()");
8302
+ return sum / arr.size();
8303
+ }
8304
+ case "sum": {
8305
+ const arr = this._evalExpr(args[0]);
8306
+ if (!(arr instanceof TArray)) throw new TypeError("[ForgeScript] array.sum() expects an array");
8307
+ let sum = 0;
8308
+ for (const v of arr.items) sum += _num(v, "array.sum()");
8309
+ return sum;
8310
+ }
8311
+ case "min": {
8312
+ const arr = this._evalExpr(args[0]);
8313
+ if (!(arr instanceof TArray)) throw new TypeError("[ForgeScript] array.min() expects an array");
8314
+ if (arr.size() === 0) return NaN;
8315
+ let m = Infinity;
8316
+ for (const v of arr.items) m = Math.min(m, _num(v, "array.min()"));
8317
+ return m;
8318
+ }
8319
+ case "max": {
8320
+ const arr = this._evalExpr(args[0]);
8321
+ if (!(arr instanceof TArray)) throw new TypeError("[ForgeScript] array.max() expects an array");
8322
+ if (arr.size() === 0) return NaN;
8323
+ let m = -Infinity;
8324
+ for (const v of arr.items) m = Math.max(m, _num(v, "array.max()"));
8325
+ return m;
8326
+ }
8327
+ default:
8328
+ throw new ReferenceError(`[ForgeScript] Unknown array function 'array.${method}'`);
8329
+ }
8330
+ }
8331
+ // ─── Input namespace (input.text_area, input.string) ─────────────────────
8332
+ _evalInputNsCall(fn, args) {
8333
+ if (args.length === 0) return fn === "text_area" || fn === "string" ? "" : 0;
8334
+ const def = this._evalExpr(args[0]);
8335
+ if (fn === "text_area" || fn === "string") return typeof def === "string" ? def : String(def);
8336
+ if (typeof def === "boolean") return def;
8337
+ if (typeof def === "string") {
8338
+ const n = parseFloat(def);
8339
+ return isNaN(n) ? def : n;
8340
+ }
8341
+ return _num(def, `input.${fn}()`);
8342
+ }
8343
+ // ─── Table namespace ────────────────────────────────────────────────────────
8344
+ _tableCounter = 0;
8345
+ _evalTableCall(method, args, namedArgs) {
8346
+ switch (method) {
8347
+ case "new": {
8348
+ const position = args.length > 0 ? String(this._evalExpr(args[0])) : "top_right";
8349
+ const cols = args.length > 1 ? Math.trunc(_num(this._evalExpr(args[1]), "table.new() columns")) : 1;
8350
+ const rows = args.length > 2 ? Math.trunc(_num(this._evalExpr(args[2]), "table.new() rows")) : 1;
8351
+ const bgColor = args.length > 3 ? String(this._evalExpr(args[3])) : namedArgs?.get("bgcolor") ? String(this._evalExpr(namedArgs.get("bgcolor"))) : "transparent";
8352
+ const borderColor = args.length > 4 ? String(this._evalExpr(args[4])) : namedArgs?.get("border_color") ? String(this._evalExpr(namedArgs.get("border_color"))) : "transparent";
8353
+ const borderWidth = namedArgs?.get("border_width") ? _num(this._evalExpr(namedArgs.get("border_width")), "table.new() border_width") : 0;
8354
+ const frameColor = namedArgs?.get("frame_color") ? String(this._evalExpr(namedArgs.get("frame_color"))) : args.length > 5 ? String(this._evalExpr(args[5])) : "transparent";
8355
+ 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;
8356
+ const id = `__table_${this._tableCounter++}`;
8357
+ this._tables.push({ id, position, rows, cols, cells: [], bgColor, borderColor, borderWidth, frameColor, frameWidth });
8358
+ return id;
8359
+ }
8360
+ case "cell": {
8361
+ const tableId = String(this._evalExpr(args[0]));
8362
+ const col = Math.trunc(_num(this._evalExpr(args[1]), "table.cell() column"));
8363
+ const row = Math.trunc(_num(this._evalExpr(args[2]), "table.cell() row"));
8364
+ const text = args.length > 3 ? String(this._evalExpr(args[3])) : namedArgs?.get("text") ? String(this._evalExpr(namedArgs.get("text"))) : "";
8365
+ const textColor = namedArgs?.get("text_color") ? String(this._evalExpr(namedArgs.get("text_color"))) : "#FFFFFF";
8366
+ const bgColor = namedArgs?.get("bgcolor") ? String(this._evalExpr(namedArgs.get("bgcolor"))) : "transparent";
8367
+ const textSize = namedArgs?.get("text_size") ? String(this._evalExpr(namedArgs.get("text_size"))) : "normal";
8368
+ const textHAlign = namedArgs?.get("text_halign") ? String(this._evalExpr(namedArgs.get("text_halign"))) : "center";
8369
+ const textVAlign = namedArgs?.get("text_valign") ? String(this._evalExpr(namedArgs.get("text_valign"))) : "center";
8370
+ const table = this._tables.find((t) => t.id === tableId);
8371
+ if (table) {
8372
+ const existing = table.cells.findIndex((c) => c.row === row && c.col === col);
8373
+ const cell = { row, col, text, textColor, bgColor, textSize, textHAlign, textVAlign };
8374
+ if (existing >= 0) table.cells[existing] = cell;
8375
+ else table.cells.push(cell);
8376
+ }
8377
+ return NaN;
8378
+ }
8379
+ case "clear": {
8380
+ const tableId = String(this._evalExpr(args[0]));
8381
+ const col = Math.trunc(_num(this._evalExpr(args[1]), "table.clear() column"));
8382
+ const row = Math.trunc(_num(this._evalExpr(args[2]), "table.clear() row"));
8383
+ const table = this._tables.find((t) => t.id === tableId);
8384
+ if (table) {
8385
+ const idx = table.cells.findIndex((c) => c.row === row && c.col === col);
8386
+ if (idx >= 0) table.cells.splice(idx, 1);
8387
+ }
8388
+ return NaN;
8389
+ }
8390
+ case "delete": {
8391
+ const tableId = String(this._evalExpr(args[0]));
8392
+ const idx = this._tables.findIndex((t) => t.id === tableId);
8393
+ if (idx >= 0) this._tables.splice(idx, 1);
8394
+ return NaN;
8395
+ }
8396
+ default:
8397
+ throw new ReferenceError(`[ForgeScript] Unknown table function 'table.${method}'`);
8398
+ }
8399
+ }
8400
+ // ─── User-defined function calls ────────────────────────────────────────────
8401
+ _callUserFunction(fnDecl, callArgs) {
8402
+ const savedParams = fnDecl.params.map((p) => this._scope.get(p));
8403
+ for (let i = 0; i < fnDecl.params.length; i++) {
8404
+ const val = i < callArgs.length ? this._evalExpr(callArgs[i]) : NaN;
8405
+ this._scope.set(fnDecl.params[i], val);
8406
+ }
8407
+ let result = NaN;
8408
+ for (const stmt of fnDecl.body) {
8409
+ if (stmt.kind === "ExprStmt") {
8410
+ result = this._evalExpr(stmt.expr);
8411
+ } else {
8412
+ this._execStmt(stmt);
8413
+ }
8414
+ }
8415
+ for (let i = 0; i < fnDecl.params.length; i++) {
8416
+ const saved = savedParams[i];
8417
+ if (saved === void 0) this._scope.delete(fnDecl.params[i]);
8418
+ else this._scope.set(fnDecl.params[i], saved);
8419
+ }
8420
+ return result;
8421
+ }
8422
+ // ─── Internal state helpers ──────────────────────────────────────────────────
8423
+ /** Persistent scalar state for stateful TA functions (EMA, RMA, ATR). */
8424
+ _internalState = /* @__PURE__ */ new Map();
8425
+ _getInternalNum(key) {
8426
+ return this._internalState.get(key) ?? NaN;
8427
+ }
8428
+ _setInternal(key, value) {
8429
+ this._internalState.set(key, value);
8430
+ }
8431
+ /** Push a value into a named built-in Series, creating it if needed. */
8432
+ _pushBuiltin(name, value) {
8433
+ let series = this._scope.get(name);
8434
+ if (!(series instanceof Series2)) {
8435
+ series = new Series2(1e3);
8436
+ this._scope.set(name, series);
8437
+ }
8438
+ series.push(value);
8439
+ }
8440
+ /** Compute a simple rolling average directly from a Series (no state). */
8441
+ _smaOfSeries(src, period) {
8442
+ let sum = 0;
8443
+ for (let i = 0; i < period; i++) sum += src.get(i);
8444
+ return sum / period;
8445
+ }
8446
+ };
8447
+
8448
+ // src/forgescript/ForgeScriptIndicator.ts
8449
+ var ForgeScriptIndicator = class _ForgeScriptIndicator {
8450
+ _runtime;
8451
+ /** Title from the `indicator("...")` declaration, or `'Script'` if absent. */
8452
+ title;
8453
+ constructor(src) {
8454
+ this._runtime = new ForgeScriptRuntime(_ForgeScriptIndicator._dedent(src));
8455
+ this.title = this._runtime.title;
8456
+ }
8457
+ /** Strip common leading whitespace from template-literal source. */
8458
+ static _dedent(s) {
8459
+ const lines = s.split("\n");
8460
+ while (lines.length && !lines[0].trim()) lines.shift();
8461
+ while (lines.length && !lines[lines.length - 1].trim()) lines.pop();
8462
+ const min = lines.filter((l) => l.trim().length > 0).reduce((m, l) => Math.min(m, l.search(/\S/)), Infinity);
8463
+ if (!isFinite(min) || min === 0) return lines.join("\n");
8464
+ return lines.map((l) => l.slice(min)).join("\n");
8465
+ }
8466
+ /**
8467
+ * Execute the script over the full bar history.
8468
+ *
8469
+ * Each call resets internal state and replays every bar from scratch,
8470
+ * ensuring the output is deterministic regardless of prior calls.
8471
+ *
6866
8472
  * @returns array of plot series — `plots[0]` corresponds to the first
6867
8473
  * `plot()` call in source, `plots[1]` to the second, etc.
6868
8474
  */
6869
8475
  run(bars) {
6870
8476
  return this._runtime.run(bars);
6871
8477
  }
8478
+ /**
8479
+ * Return the full ScriptResult from the most recent `run()` or `runFull()`.
8480
+ * Must be called after `run()` — otherwise returns empty data.
8481
+ */
8482
+ getLastResult(bars) {
8483
+ return this._runtime.getResult(bars);
8484
+ }
8485
+ /**
8486
+ * Execute the script and return the full result including shapes, hlines,
8487
+ * fills, bgcolors, barcolors, and plot metadata.
8488
+ */
8489
+ runFull(bars) {
8490
+ this._runtime.reset();
8491
+ this._runtime.setTotalBars(bars.length);
8492
+ for (let i = 0; i < bars.length; i++) {
8493
+ this._runtime.execBar(bars[i], i);
8494
+ }
8495
+ return this._runtime.getResult(bars);
8496
+ }
6872
8497
  /**
6873
8498
  * Reset all persistent series state (ring buffers) without re-parsing.
6874
8499
  * Call when the chart symbol or timeframe changes.
@@ -7087,10 +8712,11 @@ var IndicatorDAG = class {
7087
8712
  const src = node.config.script;
7088
8713
  if (!src) return { kind: "series", points: [] };
7089
8714
  if (!node.scriptRuntime) {
7090
- node.scriptRuntime = new TScriptIndicator(src);
8715
+ node.scriptRuntime = new ForgeScriptIndicator(src);
7091
8716
  }
7092
8717
  const plots = node.scriptRuntime.run(bars);
7093
- return { kind: "series", points: plots[0] ?? [] };
8718
+ const scriptResult = node.scriptRuntime.getLastResult(bars);
8719
+ return { kind: "series", points: plots[0] ?? [], scriptResult };
7094
8720
  }
7095
8721
  default:
7096
8722
  return { kind: "series", points: [...input] };
@@ -7138,7 +8764,7 @@ var IndicatorDAG = class {
7138
8764
  }
7139
8765
  };
7140
8766
 
7141
- // src/pine/pine-lexer.ts
8767
+ // src/forgescript/pine/pine-lexer.ts
7142
8768
  var PineLexer = class {
7143
8769
  _src;
7144
8770
  _pos = 0;
@@ -7364,7 +8990,7 @@ var PineLexer = class {
7364
8990
  }
7365
8991
  };
7366
8992
 
7367
- // src/pine/pine-parser.ts
8993
+ // src/forgescript/pine/pine-parser.ts
7368
8994
  var PineParser = class {
7369
8995
  _tokens;
7370
8996
  _pos = 0;
@@ -7394,6 +9020,39 @@ var PineParser = class {
7394
9020
  if (t.kind === "IDENT" && t.value === "if") {
7395
9021
  return this._ifStmt();
7396
9022
  }
9023
+ if (t.kind === "IDENT" && t.value === "switch") {
9024
+ return this._switchStmt();
9025
+ }
9026
+ if (t.kind === "IDENT" && t.value === "while") {
9027
+ return this._whileStmt();
9028
+ }
9029
+ if (t.kind === "IDENT" && t.value === "for") {
9030
+ return this._forInStmt();
9031
+ }
9032
+ if (t.kind === "IDENT" && t.value === "type") {
9033
+ return this._typeDefStmt();
9034
+ }
9035
+ if (t.kind === "IDENT" && t.value === "import") {
9036
+ return this._importStmt();
9037
+ }
9038
+ if (t.kind === "IDENT" && t.value === "export") {
9039
+ this._skipToNL();
9040
+ this._skipBlock();
9041
+ return null;
9042
+ }
9043
+ if (t.kind === "IDENT" && t.value === "method") {
9044
+ this._skipToNL();
9045
+ this._skipBlock();
9046
+ return null;
9047
+ }
9048
+ if (t.kind === "IDENT" && t.value === "enum") {
9049
+ this._skipToNL();
9050
+ this._skipBlock();
9051
+ return null;
9052
+ }
9053
+ if (t.kind === "IDENT" && this._peekKind(1) === "LPAREN" && this._looksLikeFuncDef()) {
9054
+ return this._funcDefStmt();
9055
+ }
7397
9056
  if (t.kind === "IDENT") {
7398
9057
  const p1 = this._peekKind(1);
7399
9058
  const p2 = this._peekKind(2);
@@ -7417,11 +9076,16 @@ var PineParser = class {
7417
9076
  let typeHint;
7418
9077
  if (this._check("IDENT") && this._peekKind(1) === "ASSIGN") {
7419
9078
  const maybeType = this._cur().value;
7420
- if (["float", "int", "bool", "string", "color", "series", "simple", "const"].includes(maybeType)) {
9079
+ if (["float", "int", "bool", "string", "color", "series", "simple", "const", "label", "line", "box", "table"].includes(maybeType)) {
7421
9080
  typeHint = maybeType;
7422
9081
  this._advance();
7423
9082
  }
7424
9083
  }
9084
+ if (typeHint !== void 0 && this._check("LBRACKET")) {
9085
+ this._advance();
9086
+ if (this._check("RBRACKET")) this._advance();
9087
+ typeHint += "[]";
9088
+ }
7425
9089
  const name = this._consume("IDENT", "Expected variable name after 'var'").value;
7426
9090
  this._consume("ASSIGN", "Expected '=' in var declaration");
7427
9091
  const value = this._expr();
@@ -7482,6 +9146,192 @@ var PineParser = class {
7482
9146
  if (this._check("DEDENT")) this._advance();
7483
9147
  return stmts;
7484
9148
  }
9149
+ // ─── Pine v6 statements ──────────────────────────────────────────────────────
9150
+ /**
9151
+ * switch expr
9152
+ * val1 => body ← single-line
9153
+ * val2 => ← multi-line
9154
+ * stmt1
9155
+ * => defaultBody ← default / else (no condition)
9156
+ *
9157
+ * OR boolean-style (no subject):
9158
+ * switch
9159
+ * cond1 => body
9160
+ * cond2 => body
9161
+ * => defaultBody
9162
+ */
9163
+ _switchStmt() {
9164
+ const tok = this._advance();
9165
+ const loc = { line: tok.line, col: tok.col };
9166
+ let subject = null;
9167
+ if (!this._check("NEWLINE") && !this._check("EOF")) {
9168
+ subject = this._expr();
9169
+ }
9170
+ this._consumeNLorEOF();
9171
+ const cases = [];
9172
+ if (!this._check("INDENT")) {
9173
+ return { kind: "PineSwitch", subject, cases, loc };
9174
+ }
9175
+ this._advance();
9176
+ this._skipNL();
9177
+ while (!this._check("DEDENT") && !this._check("EOF")) {
9178
+ if (this._check("ARROW")) {
9179
+ this._advance();
9180
+ const body2 = this._singleLineOrBlock();
9181
+ cases.push({ condition: null, body: body2 });
9182
+ break;
9183
+ }
9184
+ const condition = this._expr();
9185
+ this._consume("ARROW", "Expected '=>' in switch case");
9186
+ const body = this._singleLineOrBlock();
9187
+ cases.push({ condition, body });
9188
+ this._skipNL();
9189
+ }
9190
+ if (this._check("DEDENT")) this._advance();
9191
+ return { kind: "PineSwitch", subject, cases, loc };
9192
+ }
9193
+ /** Parse single-line body (after =>) OR consume NL and parse an indented block. */
9194
+ _singleLineOrBlock() {
9195
+ if (!this._check("NEWLINE") && !this._check("EOF")) {
9196
+ const loc = this._locOfCur();
9197
+ const expr = this._expr();
9198
+ this._consumeNLorEOF();
9199
+ return [{ kind: "PineExprStmt", expr, loc }];
9200
+ }
9201
+ this._consumeNLorEOF();
9202
+ return this._indentedBlock();
9203
+ }
9204
+ _whileStmt() {
9205
+ const tok = this._advance();
9206
+ const loc = { line: tok.line, col: tok.col };
9207
+ const condition = this._expr();
9208
+ this._consumeNLorEOF();
9209
+ const body = this._indentedBlock();
9210
+ return { kind: "PineWhile", condition, body, loc };
9211
+ }
9212
+ _forInStmt() {
9213
+ const tok = this._advance();
9214
+ const loc = { line: tok.line, col: tok.col };
9215
+ const varName = this._consume("IDENT", "Expected variable name in 'for' loop").value;
9216
+ if (this._check("ASSIGN") || this._check("REASSIGN")) {
9217
+ this._skipToNL();
9218
+ this._skipBlock();
9219
+ const placeholder = { kind: "PineNumberLit", value: 0, loc };
9220
+ return { kind: "PineForIn", varName, iterable: placeholder, body: [], loc };
9221
+ }
9222
+ if (this._check("IDENT") && this._cur().value === "in") {
9223
+ this._advance();
9224
+ } else {
9225
+ this._skipToNL();
9226
+ this._skipBlock();
9227
+ const placeholder = { kind: "PineNumberLit", value: 0, loc };
9228
+ return { kind: "PineForIn", varName, iterable: placeholder, body: [], loc };
9229
+ }
9230
+ const iterable = this._expr();
9231
+ this._consumeNLorEOF();
9232
+ const body = this._indentedBlock();
9233
+ return { kind: "PineForIn", varName, iterable, body, loc };
9234
+ }
9235
+ /**
9236
+ * type MyType
9237
+ * float field1 = 0
9238
+ * int field2 = 0
9239
+ *
9240
+ * We parse the name and skip the indented field block.
9241
+ */
9242
+ _typeDefStmt() {
9243
+ const tok = this._advance();
9244
+ const loc = { line: tok.line, col: tok.col };
9245
+ const name = this._check("IDENT") ? this._advance().value : "__unknown__";
9246
+ this._consumeNLorEOF();
9247
+ this._skipBlock();
9248
+ return { kind: "PineTypeDef", name, loc };
9249
+ }
9250
+ /**
9251
+ * import username/libraryname/version as alias
9252
+ */
9253
+ _importStmt() {
9254
+ const tok = this._advance();
9255
+ const loc = { line: tok.line, col: tok.col };
9256
+ let path = "";
9257
+ while (!this._check("NEWLINE") && !this._check("EOF") && !(this._check("IDENT") && this._cur().value === "as")) {
9258
+ path += this._advance().value;
9259
+ }
9260
+ let alias = "";
9261
+ if (this._check("IDENT") && this._cur().value === "as") {
9262
+ this._advance();
9263
+ if (this._check("IDENT")) alias = this._advance().value;
9264
+ }
9265
+ this._consumeNLorEOF();
9266
+ return { kind: "PineImport", path: path.trim(), alias, loc };
9267
+ }
9268
+ // ─── Block-skip helpers ──────────────────────────────────────────────────────
9269
+ /**
9270
+ * Returns true if the current position looks like a user-defined function
9271
+ * declaration: `IDENT '(' ... ')' '=>'`.
9272
+ * Scans forward to find the matching RPAREN, then checks for ARROW.
9273
+ */
9274
+ _looksLikeFuncDef() {
9275
+ let i = this._pos + 1;
9276
+ if (this._tokens[i]?.kind !== "LPAREN") return false;
9277
+ i++;
9278
+ let depth = 1;
9279
+ while (i < this._tokens.length && depth > 0) {
9280
+ const k = this._tokens[i].kind;
9281
+ if (k === "LPAREN") depth++;
9282
+ else if (k === "RPAREN") depth--;
9283
+ else if (k === "EOF" || k === "NEWLINE") return false;
9284
+ i++;
9285
+ }
9286
+ return this._tokens[i]?.kind === "ARROW";
9287
+ }
9288
+ /**
9289
+ * Skip a user-defined function declaration and its body.
9290
+ *
9291
+ * funcname(param, ...) =>
9292
+ * body
9293
+ *
9294
+ * OR single-line:
9295
+ * funcname(params) => expr
9296
+ *
9297
+ * Emits no output — user-defined functions are unsupported in ForgeScript.
9298
+ */
9299
+ _funcDefStmt() {
9300
+ this._advance();
9301
+ this._advance();
9302
+ let depth = 1;
9303
+ while (depth > 0 && !this._check("EOF")) {
9304
+ if (this._check("LPAREN")) depth++;
9305
+ else if (this._check("RPAREN")) depth--;
9306
+ this._advance();
9307
+ }
9308
+ if (this._check("ARROW")) this._advance();
9309
+ if (!this._check("NEWLINE") && !this._check("EOF")) {
9310
+ this._skipToNL();
9311
+ } else {
9312
+ this._consumeNLorEOF();
9313
+ this._skipBlock();
9314
+ }
9315
+ return null;
9316
+ }
9317
+ /** Skip to end of current line (consumes any tokens before NEWLINE/EOF). */
9318
+ _skipToNL() {
9319
+ while (!this._check("NEWLINE") && !this._check("EOF")) {
9320
+ this._advance();
9321
+ }
9322
+ this._consumeNLorEOF();
9323
+ }
9324
+ /** Skip a full INDENT…DEDENT block, handling nesting. Does nothing if no INDENT. */
9325
+ _skipBlock() {
9326
+ if (!this._check("INDENT")) return;
9327
+ this._advance();
9328
+ let depth = 1;
9329
+ while (depth > 0 && !this._check("EOF")) {
9330
+ if (this._check("INDENT")) depth++;
9331
+ else if (this._check("DEDENT")) depth--;
9332
+ this._advance();
9333
+ }
9334
+ }
7485
9335
  // ─── Expressions ────────────────────────────────────────────────────────────
7486
9336
  _expr() {
7487
9337
  return this._ternary();
@@ -7651,6 +9501,20 @@ var PineParser = class {
7651
9501
  const node2 = { kind: "PineNsCall", namespace: ns, fn: member, args, namedArgs, loc };
7652
9502
  return node2;
7653
9503
  }
9504
+ if (this._check("LT")) {
9505
+ this._advance();
9506
+ while (!this._check("GT") && !this._check("EOF") && !this._check("NEWLINE")) {
9507
+ this._advance();
9508
+ }
9509
+ if (this._check("GT")) this._advance();
9510
+ if (this._check("LPAREN")) {
9511
+ this._advance();
9512
+ const { args, namedArgs } = this._argList();
9513
+ this._consume("RPAREN", "Expected ')'");
9514
+ const node2 = { kind: "PineNsCall", namespace: ns, fn: member, args, namedArgs, loc };
9515
+ return node2;
9516
+ }
9517
+ }
7654
9518
  const node = { kind: "PineMember", object: ns, prop: member, loc };
7655
9519
  return node;
7656
9520
  }
@@ -7674,6 +9538,20 @@ var PineParser = class {
7674
9538
  this._consume("RPAREN", "Expected ')'");
7675
9539
  return inner;
7676
9540
  }
9541
+ if (t.kind === "LBRACKET") {
9542
+ this._advance();
9543
+ if (this._check("RBRACKET")) {
9544
+ this._advance();
9545
+ return { kind: "PineNumberLit", value: 0, loc };
9546
+ }
9547
+ const first = this._expr();
9548
+ while (this._match("COMMA")) {
9549
+ if (this._check("RBRACKET")) break;
9550
+ this._expr();
9551
+ }
9552
+ if (this._check("RBRACKET")) this._advance();
9553
+ return first;
9554
+ }
7677
9555
  throw new SyntaxError(`[Pine] Unexpected token '${t.value}' (${t.kind}) at ${t.line}:${t.col}`);
7678
9556
  }
7679
9557
  // ─── Arg list helper ────────────────────────────────────────────────────────
@@ -7746,7 +9624,7 @@ var PineParser = class {
7746
9624
  }
7747
9625
  };
7748
9626
 
7749
- // src/pine/pine-transpiler.ts
9627
+ // src/forgescript/pine/pine-transpiler.ts
7750
9628
  var TA_MAP = {
7751
9629
  sma: "sma",
7752
9630
  ema: "ema",
@@ -7761,7 +9639,25 @@ var TA_MAP = {
7761
9639
  atr: "atr",
7762
9640
  crossover: "crossover",
7763
9641
  crossunder: "crossunder",
7764
- barssince: "barssince"
9642
+ cross: "cross",
9643
+ barssince: "barssince",
9644
+ macd: "macd",
9645
+ bb: "bb",
9646
+ stoch: "stoch",
9647
+ obv: "obv",
9648
+ correlation: "correlation",
9649
+ dev: "dev",
9650
+ variance: "variance",
9651
+ cum: "cum",
9652
+ sum: "sum",
9653
+ valuewhen: "valuewhen",
9654
+ pivothigh: "pivothigh",
9655
+ pivotlow: "pivotlow",
9656
+ tr: "tr",
9657
+ swma: "swma",
9658
+ vwma: "vwma",
9659
+ rising: "rising",
9660
+ falling: "falling"
7765
9661
  };
7766
9662
  var MATH_MAP = {
7767
9663
  abs: "abs",
@@ -7772,9 +9668,21 @@ var MATH_MAP = {
7772
9668
  ceil: "ceil",
7773
9669
  sqrt: "sqrt",
7774
9670
  log: "log",
9671
+ log10: "log10",
7775
9672
  pow: "pow",
7776
9673
  sign: "sign",
7777
- exp: "exp"
9674
+ exp: "exp",
9675
+ avg: "avg",
9676
+ sum: "sum",
9677
+ sin: "sin",
9678
+ cos: "cos",
9679
+ tan: "tan",
9680
+ asin: "asin",
9681
+ acos: "acos",
9682
+ atan: "atan",
9683
+ random: "random",
9684
+ todegrees: "todegrees",
9685
+ toradians: "toradians"
7778
9686
  };
7779
9687
  var COLOR_MAP = {
7780
9688
  red: '"#f23645"',
@@ -7796,21 +9704,25 @@ var COLOR_MAP = {
7796
9704
  fuchsia: '"#ff00ff"'
7797
9705
  };
7798
9706
  var UNSUPPORTED_FN = /* @__PURE__ */ new Set([
7799
- "plotshape",
7800
- "plotarrow",
7801
9707
  "plotbar",
7802
9708
  "plotcandle",
7803
- "barcolor",
7804
- "bgcolor",
7805
9709
  "alertcondition",
7806
9710
  "alert",
7807
9711
  "strategy",
7808
9712
  "label",
7809
9713
  "line",
7810
9714
  "box",
7811
- "table",
7812
9715
  "request"
7813
9716
  ]);
9717
+ var OUTPUT_FN = /* @__PURE__ */ new Set([
9718
+ "plotshape",
9719
+ "plotchar",
9720
+ "plotarrow",
9721
+ "hline",
9722
+ "fill",
9723
+ "bgcolor",
9724
+ "barcolor"
9725
+ ]);
7814
9726
  var PineTranspiler = class {
7815
9727
  _diag;
7816
9728
  _indent = 0;
@@ -7839,18 +9751,32 @@ var PineTranspiler = class {
7839
9751
  return this._exprStmt(s);
7840
9752
  case "PineIf":
7841
9753
  return this._ifStmt(s);
9754
+ case "PineSwitch":
9755
+ return this._switchStmt(s);
9756
+ case "PineWhile":
9757
+ return this._whileStmt(s);
9758
+ case "PineForIn":
9759
+ return this._forInStmt(s);
9760
+ case "PineTypeDef":
9761
+ return;
9762
+ // silently skipped — diagnostic added at parse time
9763
+ case "PineImport":
9764
+ return;
7842
9765
  }
7843
9766
  }
7844
9767
  _indicatorDecl(s) {
7845
9768
  const title = s.args[0];
7846
9769
  const titleStr = title ? this._expr(title) : '"Script"';
7847
- this._line(`indicator(${titleStr})`);
9770
+ const overlay = s.namedArgs?.get("overlay");
9771
+ const overlayStr = overlay ? `, overlay=${this._expr(overlay)}` : "";
9772
+ this._line(`indicator(${titleStr}${overlayStr})`);
7848
9773
  }
7849
9774
  _varDecl(s) {
7850
- this._line(`${s.name} = ${this._expr(s.value)}`);
9775
+ const mod = s.modifier === "varip" ? "varip " : s.modifier === "var" ? "var " : "";
9776
+ this._line(`${mod}${s.name} = ${this._expr(s.value)}`);
7851
9777
  }
7852
9778
  _assign(s) {
7853
- this._line(`${s.name} = ${this._expr(s.value)}`);
9779
+ this._line(`${s.name} := ${this._expr(s.value)}`);
7854
9780
  }
7855
9781
  _exprStmt(s) {
7856
9782
  const expr = s.expr;
@@ -7875,6 +9801,38 @@ var PineTranspiler = class {
7875
9801
  this._indent--;
7876
9802
  }
7877
9803
  }
9804
+ _switchStmt(s) {
9805
+ let first = true;
9806
+ for (const c of s.cases) {
9807
+ if (c.condition === null) {
9808
+ this._line(first ? "if true" : "else");
9809
+ } else {
9810
+ const condStr = s.subject !== null ? `(${this._expr(s.subject)} == ${this._expr(c.condition)})` : this._expr(c.condition);
9811
+ this._line(first ? `if ${condStr}` : `else if ${condStr}`);
9812
+ }
9813
+ this._indent++;
9814
+ for (const st of c.body) this._stmt(st);
9815
+ if (c.body.length === 0) this._line("0");
9816
+ this._indent--;
9817
+ first = false;
9818
+ }
9819
+ }
9820
+ _whileStmt(s) {
9821
+ this._line(`while ${this._expr(s.condition)}`);
9822
+ this._indent++;
9823
+ for (const st of s.body) this._stmt(st);
9824
+ if (s.body.length === 0) this._line("0");
9825
+ this._indent--;
9826
+ }
9827
+ _forInStmt(s) {
9828
+ this._diag.add(
9829
+ "warning",
9830
+ `'for...in' loops are not supported and will be skipped`,
9831
+ s.loc,
9832
+ "PINE_UNSUPPORTED_FN"
9833
+ );
9834
+ this._line(`// for ${s.varName} in ... (for..in not supported)`);
9835
+ }
7878
9836
  // ─── Expressions ────────────────────────────────────────────────────────────
7879
9837
  _expr(e) {
7880
9838
  switch (e.kind) {
@@ -7933,7 +9891,32 @@ var PineTranspiler = class {
7933
9891
  if (ns === "input") {
7934
9892
  return this._inputNsCall(e);
7935
9893
  }
7936
- if (ns === "strategy" || ns === "label" || ns === "line" || ns === "box" || ns === "table" || ns === "request") {
9894
+ if (ns === "str") {
9895
+ return this._strNsCall(e);
9896
+ }
9897
+ if (ns === "array") {
9898
+ const args = this._positionalArgs(e.args);
9899
+ const namedParts = [];
9900
+ for (const [key, val] of e.namedArgs) namedParts.push(`${key}=${this._expr(val)}`);
9901
+ const allArgs = [args, ...namedParts].filter(Boolean).join(", ");
9902
+ return `array.${fn}(${allArgs})`;
9903
+ }
9904
+ if (ns === "matrix" || ns === "map") {
9905
+ this._diag.add("warning", `'${ns}.${fn}()' is not supported (no collection types in ForgeScript)`, e.loc, "PINE_UNSUPPORTED_FN");
9906
+ return `/* ${ns}.${fn} not supported */ 0`;
9907
+ }
9908
+ if (ns === "ticker") {
9909
+ this._diag.add("warning", `'ticker.${fn}()' is not supported`, e.loc, "PINE_UNSUPPORTED_FN");
9910
+ return `/* ticker.${fn} not supported */ 0`;
9911
+ }
9912
+ if (ns === "table") {
9913
+ const args = this._positionalArgs(e.args);
9914
+ const namedParts = [];
9915
+ for (const [key, val] of e.namedArgs) namedParts.push(`${key}=${this._expr(val)}`);
9916
+ const allArgs = [args, ...namedParts].filter(Boolean).join(", ");
9917
+ return `table.${fn}(${allArgs})`;
9918
+ }
9919
+ if (ns === "strategy" || ns === "label" || ns === "line" || ns === "box" || ns === "request") {
7937
9920
  this._diag.add("warning", `'${ns}.${fn}()' is not supported`, e.loc, "PINE_UNSUPPORTED_FN");
7938
9921
  return `/* ${ns}.${fn} not supported */ 0`;
7939
9922
  }
@@ -7944,14 +9927,46 @@ var PineTranspiler = class {
7944
9927
  if (e.fn === "source") {
7945
9928
  return e.args[0] ? this._expr(e.args[0]) : "close";
7946
9929
  }
9930
+ if (e.fn === "text_area" || e.fn === "string") {
9931
+ const defVal2 = e.args[0] ?? e.namedArgs.get("defval");
9932
+ return defVal2 ? `input.${e.fn}(${this._expr(defVal2)})` : `input.${e.fn}("")`;
9933
+ }
7947
9934
  const defVal = e.args[0] ?? e.namedArgs.get("defval");
7948
9935
  return defVal ? `input(${this._expr(defVal)})` : "input(0)";
7949
9936
  }
9937
+ _strNsCall(e) {
9938
+ const SUPPORTED = {
9939
+ tostring: "tostring",
9940
+ tonumber: "tonumber",
9941
+ length: "length",
9942
+ trim: "trim",
9943
+ contains: "contains",
9944
+ substring: "substring",
9945
+ replace_all: "replace_all",
9946
+ upper: "upper",
9947
+ lower: "lower",
9948
+ split: "split"
9949
+ };
9950
+ const mapped = SUPPORTED[e.fn];
9951
+ if (mapped) {
9952
+ const args = this._positionalArgs(e.args);
9953
+ return `str.${mapped}(${args})`;
9954
+ }
9955
+ if (e.fn === "format") {
9956
+ const args = this._positionalArgs(e.args);
9957
+ return `str.format(${args})`;
9958
+ }
9959
+ this._diag.add("info", `'str.${e.fn}()' is not supported and will return 0`, e.loc, "PINE_UNSUPPORTED_FN");
9960
+ return `/* str.${e.fn} not supported */ 0`;
9961
+ }
7950
9962
  _call(e) {
7951
9963
  if (UNSUPPORTED_FN.has(e.fn)) {
7952
9964
  this._diag.add("warning", `'${e.fn}()' is not supported and will be ignored`, e.loc, "PINE_UNSUPPORTED_FN");
7953
9965
  return `/* ${e.fn} not supported */ 0`;
7954
9966
  }
9967
+ if (OUTPUT_FN.has(e.fn)) {
9968
+ return this._outputFnCall(e);
9969
+ }
7955
9970
  if (e.fn === "input") {
7956
9971
  const defVal = e.args[0] ?? e.namedArgs.get("defval");
7957
9972
  return defVal ? `input(${this._expr(defVal)})` : "input(0)";
@@ -7962,15 +9977,29 @@ var PineTranspiler = class {
7962
9977
  if (e.fn === "indicator") {
7963
9978
  const title = e.args[0];
7964
9979
  const titleStr = title ? this._expr(title) : '"Script"';
7965
- return `indicator(${titleStr})`;
9980
+ const overlay = e.namedArgs.get("overlay");
9981
+ const overlayStr = overlay ? `, overlay=${this._expr(overlay)}` : "";
9982
+ return `indicator(${titleStr}${overlayStr})`;
7966
9983
  }
7967
9984
  const args = this._positionalArgs(e.args);
7968
9985
  return `${e.fn}(${args})`;
7969
9986
  }
9987
+ /** Transpile supported output functions, preserving named args */
9988
+ _outputFnCall(e) {
9989
+ const parts = e.args.map((a) => this._expr(a));
9990
+ for (const [key, val] of e.namedArgs) {
9991
+ parts.push(`${key}=${this._expr(val)}`);
9992
+ }
9993
+ return `${e.fn}(${parts.join(", ")})`;
9994
+ }
7970
9995
  _plotCall(e) {
7971
9996
  const series = e.args[0];
7972
9997
  if (!series) return "/* plot() missing series */";
7973
- return `plot(${this._expr(series)})`;
9998
+ const parts = [this._expr(series)];
9999
+ for (const [key, val] of e.namedArgs) {
10000
+ parts.push(`${key}=${this._expr(val)}`);
10001
+ }
10002
+ return `plot(${parts.join(", ")})`;
7974
10003
  }
7975
10004
  _member(e) {
7976
10005
  if (e.object === "color") {
@@ -7982,7 +10011,19 @@ var PineTranspiler = class {
7982
10011
  if (e.object === "line" || e.object === "label" || e.object === "box") {
7983
10012
  return `"${e.prop}"`;
7984
10013
  }
7985
- if (e.object === "timeframe" || e.object === "syminfo" || e.object === "session") {
10014
+ if (e.object === "shape" || e.object === "location" || e.object === "position" || e.object === "size") {
10015
+ return `"${e.prop}"`;
10016
+ }
10017
+ if (e.object === "text") {
10018
+ return `"${e.prop.replace(/^align_/, "")}"`;
10019
+ }
10020
+ if (e.object === "plot") {
10021
+ return `"${e.prop.replace(/^style_/, "")}"`;
10022
+ }
10023
+ if (e.object === "timeframe" || e.object === "syminfo" || e.object === "barstate") {
10024
+ return `${e.object}.${e.prop}`;
10025
+ }
10026
+ if (e.object === "session") {
7986
10027
  this._diag.add("info", `'${e.object}.${e.prop}' is not supported`, e.loc, "PINE_UNSUPPORTED_FN");
7987
10028
  return "0";
7988
10029
  }
@@ -8008,7 +10049,7 @@ var PineTranspiler = class {
8008
10049
  }
8009
10050
  };
8010
10051
 
8011
- // src/pine/diagnostics.ts
10052
+ // src/forgescript/pine/diagnostics.ts
8012
10053
  var DiagnosticBag = class {
8013
10054
  _diags = [];
8014
10055
  add(severity, message, loc, code) {
@@ -8022,16 +10063,32 @@ var DiagnosticBag = class {
8022
10063
  }
8023
10064
  };
8024
10065
 
8025
- // src/pine/PineCompiler.ts
10066
+ // src/forgescript/pine/PineCompiler.ts
8026
10067
  var PineCompiler = class _PineCompiler {
8027
10068
  /**
8028
- * Compile Pine Script source to TScript.
10069
+ * Compile Pine Script source to ForgeScript.
8029
10070
  * Parse errors throw a `SyntaxError`; semantic problems populate diagnostics.
8030
10071
  */
8031
10072
  compile(pineSource) {
8032
10073
  const diag = new DiagnosticBag();
8033
10074
  const parser = new PineParser(pineSource);
8034
10075
  const program = parser.parse();
10076
+ const ver = program.version;
10077
+ if (ver < 5) {
10078
+ diag.add(
10079
+ "warning",
10080
+ `Pine v${ver} is a legacy version; some syntax may not transpile correctly`,
10081
+ { line: 1, col: 1 },
10082
+ "PINE_VERSION_LEGACY"
10083
+ );
10084
+ } else if (ver > 6) {
10085
+ diag.add(
10086
+ "warning",
10087
+ `Pine v${ver} is not fully supported; features beyond v6 may not transpile correctly`,
10088
+ { line: 1, col: 1 },
10089
+ "PINE_VERSION_UNSUPPORTED"
10090
+ );
10091
+ }
8035
10092
  const transpiler = new PineTranspiler(diag);
8036
10093
  const tscript = transpiler.transpile(program);
8037
10094
  return {
@@ -8050,12 +10107,1055 @@ var PineCompiler = class _PineCompiler {
8050
10107
  if (!result.ok) {
8051
10108
  return { plots: [], diagnostics: result.diagnostics };
8052
10109
  }
8053
- const indicator = new TScriptIndicator(result.tscript);
10110
+ const indicator = new ForgeScriptIndicator(result.tscript);
8054
10111
  const plots = indicator.run(bars);
8055
10112
  return { plots, diagnostics: result.diagnostics };
8056
10113
  }
8057
10114
  };
8058
10115
 
10116
+ // src/forgescript/conversion/language-detector.ts
10117
+ var RULES = [
10118
+ // ── Pine Script ───────────────────────────────────────────────────────────
10119
+ { pattern: /\/\/@version\s*=\s*\d/, score: 50, language: "pine" },
10120
+ { pattern: /\bindicator\s*\(/, score: 20, language: "pine" },
10121
+ { pattern: /\bstrategy\s*\(/, score: 20, language: "pine" },
10122
+ { pattern: /\bta\.(sma|ema|rsi|atr|macd)\b/, score: 15, language: "pine" },
10123
+ { pattern: /\bcolor\.(red|green|blue|new)\b/, score: 10, language: "pine" },
10124
+ { pattern: /\bplot(shape|char|arrow)?\s*\(/, score: 10, language: "pine" },
10125
+ { pattern: /\bsyminfo\.\w+/, score: 10, language: "pine" },
10126
+ { pattern: /\brequest\.security\s*\(/, score: 15, language: "pine" },
10127
+ { pattern: /\bvar\s+\w+\s*=/, score: 5, language: "pine" },
10128
+ { pattern: /\bvarip\s+\w+\s*=/, score: 15, language: "pine" },
10129
+ { pattern: /:=/, score: 8, language: "pine" },
10130
+ // ── Python ────────────────────────────────────────────────────────────────
10131
+ { pattern: /\bdef\s+\w+\s*\(/, score: 25, language: "python" },
10132
+ { pattern: /\bimport\s+(numpy|pandas|ta|talib|yfinance)\b/, score: 30, language: "python" },
10133
+ { pattern: /\bfrom\s+\w+\s+import\b/, score: 15, language: "python" },
10134
+ { pattern: /\bclass\s+\w+(\s*\(.*\))?\s*:/, score: 15, language: "python" },
10135
+ { pattern: /\bprint\s*\(/, score: 5, language: "python" },
10136
+ { pattern: /\bself\.\w+/, score: 10, language: "python" },
10137
+ { pattern: /\bif\s+.*:\s*$/m, score: 10, language: "python" },
10138
+ { pattern: /\belif\b/, score: 15, language: "python" },
10139
+ { pattern: /\bfor\s+\w+\s+in\b/, score: 10, language: "python" },
10140
+ { pattern: /^\s*#[^!]/m, score: 3, language: "python" },
10141
+ { pattern: /\bnp\.\w+/, score: 15, language: "python" },
10142
+ { pattern: /\bpd\.DataFrame\b/, score: 20, language: "python" },
10143
+ // ── JavaScript / TypeScript ───────────────────────────────────────────────
10144
+ { pattern: /\bfunction\s+\w+\s*\(/, score: 20, language: "javascript" },
10145
+ { pattern: /\bconst\s+\w+\s*=/, score: 10, language: "javascript" },
10146
+ { pattern: /\blet\s+\w+\s*=/, score: 10, language: "javascript" },
10147
+ { pattern: /=>\s*[{(]/, score: 15, language: "javascript" },
10148
+ { pattern: /\bconsole\.(log|warn|error)\s*\(/, score: 15, language: "javascript" },
10149
+ { pattern: /\brequire\s*\(/, score: 15, language: "javascript" },
10150
+ { pattern: /\bmodule\.exports\b/, score: 15, language: "javascript" },
10151
+ { pattern: /\bimport\s+.*\s+from\s+['"]/, score: 15, language: "javascript" },
10152
+ { pattern: /\bexport\s+(default\s+)?(function|class|const)\b/, score: 15, language: "javascript" },
10153
+ { pattern: /\basync\s+function\b/, score: 10, language: "javascript" },
10154
+ { pattern: /\bawait\s+/, score: 8, language: "javascript" },
10155
+ { pattern: /\bnew\s+Promise\b/, score: 10, language: "javascript" },
10156
+ { pattern: /\.then\s*\(/, score: 8, language: "javascript" },
10157
+ // ── ForgeScript (already canonical) ───────────────────────────────────────
10158
+ { pattern: /\bindicator\s*\("/, score: 10, language: "forgescript" },
10159
+ { pattern: /\bsma\s*\(close\b/, score: 10, language: "forgescript" }
10160
+ ];
10161
+ var CONFIDENCE_THRESHOLDS = {
10162
+ HIGH: 40,
10163
+ MEDIUM: 20};
10164
+ function detectLanguage(source) {
10165
+ const scores = {
10166
+ pine: 0,
10167
+ python: 0,
10168
+ javascript: 0,
10169
+ forgescript: 0,
10170
+ unknown: 0
10171
+ };
10172
+ for (const rule of RULES) {
10173
+ if (rule.pattern.test(source)) {
10174
+ scores[rule.language] += rule.score;
10175
+ }
10176
+ }
10177
+ let best = "unknown";
10178
+ let bestScore = 0;
10179
+ for (const [lang, score] of Object.entries(scores)) {
10180
+ if (lang === "unknown") continue;
10181
+ if (score > bestScore) {
10182
+ bestScore = score;
10183
+ best = lang;
10184
+ }
10185
+ }
10186
+ let confidence = "LOW";
10187
+ if (bestScore >= CONFIDENCE_THRESHOLDS.HIGH) confidence = "HIGH";
10188
+ else if (bestScore >= CONFIDENCE_THRESHOLDS.MEDIUM) confidence = "MEDIUM";
10189
+ if (bestScore === 0) {
10190
+ best = "unknown";
10191
+ confidence = "LOW";
10192
+ }
10193
+ const reason = bestScore === 0 ? "No recognisable language patterns detected" : `Matched ${best} patterns with score ${bestScore}`;
10194
+ return { language: best, confidence, reason, scores };
10195
+ }
10196
+ async function detectLanguageWithFallback(source, aiClassifier) {
10197
+ const heuristic = detectLanguage(source);
10198
+ if (heuristic.language !== "unknown" || !aiClassifier) return heuristic;
10199
+ return aiClassifier(source);
10200
+ }
10201
+
10202
+ // src/forgescript/conversion/validation.ts
10203
+ var BUILTIN_SERIES = /* @__PURE__ */ new Set([
10204
+ "open",
10205
+ "high",
10206
+ "low",
10207
+ "close",
10208
+ "volume",
10209
+ "time",
10210
+ "hl2",
10211
+ "hlc3",
10212
+ "ohlc4",
10213
+ "bar_index"
10214
+ ]);
10215
+ var BUILTIN_FUNCTIONS = /* @__PURE__ */ new Set([
10216
+ // Utility
10217
+ "input",
10218
+ "plot",
10219
+ "na",
10220
+ "nz",
10221
+ // Math (flat)
10222
+ "abs",
10223
+ "max",
10224
+ "min",
10225
+ "round",
10226
+ "floor",
10227
+ "ceil",
10228
+ "sqrt",
10229
+ "log",
10230
+ "exp",
10231
+ "pow",
10232
+ "sign",
10233
+ "sin",
10234
+ "cos",
10235
+ "tan",
10236
+ "asin",
10237
+ "acos",
10238
+ "atan",
10239
+ "todegrees",
10240
+ "toradians",
10241
+ // TA (flat)
10242
+ "sma",
10243
+ "ema",
10244
+ "wma",
10245
+ "rma",
10246
+ "rsi",
10247
+ "stdev",
10248
+ "atr",
10249
+ "highest",
10250
+ "lowest",
10251
+ "change",
10252
+ "mom",
10253
+ "crossover",
10254
+ "crossunder",
10255
+ "cross",
10256
+ "macd",
10257
+ "bb",
10258
+ "stoch",
10259
+ "obv",
10260
+ "correlation",
10261
+ "dev",
10262
+ "variance",
10263
+ "cum",
10264
+ "sum",
10265
+ "valuewhen",
10266
+ "barssince",
10267
+ "pivothigh",
10268
+ "pivotlow",
10269
+ "tr",
10270
+ "swma",
10271
+ "vwma",
10272
+ "rising",
10273
+ "falling",
10274
+ // Output
10275
+ "plotshape",
10276
+ "plotchar",
10277
+ "plotarrow",
10278
+ "hline",
10279
+ "fill",
10280
+ "bgcolor",
10281
+ "barcolor"
10282
+ ]);
10283
+ var NAMESPACE_FUNCTIONS = {
10284
+ ta: /* @__PURE__ */ new Set([
10285
+ "sma",
10286
+ "ema",
10287
+ "wma",
10288
+ "rma",
10289
+ "rsi",
10290
+ "stdev",
10291
+ "atr",
10292
+ "highest",
10293
+ "lowest",
10294
+ "change",
10295
+ "mom",
10296
+ "crossover",
10297
+ "crossunder",
10298
+ "cross",
10299
+ "macd",
10300
+ "bb",
10301
+ "stoch",
10302
+ "obv",
10303
+ "correlation",
10304
+ "dev",
10305
+ "variance",
10306
+ "cum",
10307
+ "sum",
10308
+ "valuewhen",
10309
+ "barssince",
10310
+ "pivothigh",
10311
+ "pivotlow",
10312
+ "tr",
10313
+ "swma",
10314
+ "vwma",
10315
+ "rising",
10316
+ "falling"
10317
+ ]),
10318
+ math: /* @__PURE__ */ new Set([
10319
+ "abs",
10320
+ "max",
10321
+ "min",
10322
+ "round",
10323
+ "floor",
10324
+ "ceil",
10325
+ "sqrt",
10326
+ "log",
10327
+ "exp",
10328
+ "pow",
10329
+ "sign",
10330
+ "sin",
10331
+ "cos",
10332
+ "tan",
10333
+ "asin",
10334
+ "acos",
10335
+ "atan",
10336
+ "todegrees",
10337
+ "toradians",
10338
+ "avg",
10339
+ "sum",
10340
+ "random",
10341
+ "log10"
10342
+ ]),
10343
+ color: /* @__PURE__ */ new Set(["new", "rgb"]),
10344
+ str: /* @__PURE__ */ new Set(["tostring", "format"])
10345
+ };
10346
+ var NAMESPACE_MEMBERS = {
10347
+ color: /* @__PURE__ */ new Set([
10348
+ "red",
10349
+ "green",
10350
+ "blue",
10351
+ "orange",
10352
+ "yellow",
10353
+ "purple",
10354
+ "white",
10355
+ "black",
10356
+ "gray",
10357
+ "silver",
10358
+ "maroon",
10359
+ "navy",
10360
+ "teal",
10361
+ "aqua",
10362
+ "lime",
10363
+ "fuchsia",
10364
+ "olive"
10365
+ ]),
10366
+ syminfo: /* @__PURE__ */ new Set(["tickerid", "ticker", "mintick", "pointvalue", "currency", "type"]),
10367
+ timeframe: /* @__PURE__ */ new Set([
10368
+ "period",
10369
+ "multiplier",
10370
+ "isintraday",
10371
+ "isdaily",
10372
+ "isweekly",
10373
+ "ismonthly"
10374
+ ])
10375
+ };
10376
+ var BUILTIN_CONSTANTS = /* @__PURE__ */ new Set(["true", "false", "na"]);
10377
+ function validateForgeScript(source) {
10378
+ const issues = [];
10379
+ let program;
10380
+ try {
10381
+ const lexer = new Lexer(source);
10382
+ const tokens = lexer.tokenize();
10383
+ const parser = new Parser(source);
10384
+ void tokens;
10385
+ program = parser.parse();
10386
+ } catch (err) {
10387
+ const msg = err instanceof Error ? err.message : String(err);
10388
+ issues.push({
10389
+ severity: "error",
10390
+ code: "SYNTAX_ERROR",
10391
+ message: msg
10392
+ });
10393
+ return { ok: false, issues };
10394
+ }
10395
+ const declaredVars = /* @__PURE__ */ new Set();
10396
+ function walkStmt(stmt) {
10397
+ switch (stmt.kind) {
10398
+ case "IndicatorDecl":
10399
+ break;
10400
+ case "AssignStmt":
10401
+ declaredVars.add(stmt.name);
10402
+ walkExpr(stmt.value);
10403
+ break;
10404
+ case "VarDeclStmt":
10405
+ declaredVars.add(stmt.name);
10406
+ walkExpr(stmt.value);
10407
+ break;
10408
+ case "ReassignStmt":
10409
+ if (!declaredVars.has(stmt.name) && !BUILTIN_SERIES.has(stmt.name)) {
10410
+ issues.push({
10411
+ severity: "warning",
10412
+ code: "REASSIGN_UNDECLARED",
10413
+ message: `Reassigning undeclared variable '${stmt.name}' \u2014 may fail at runtime`
10414
+ });
10415
+ }
10416
+ walkExpr(stmt.value);
10417
+ break;
10418
+ case "ExprStmt":
10419
+ walkExpr(stmt.expr);
10420
+ break;
10421
+ case "IfStmt":
10422
+ walkExpr(stmt.condition);
10423
+ stmt.then.forEach(walkStmt);
10424
+ stmt.else_?.forEach(walkStmt);
10425
+ break;
10426
+ case "WhileStmt":
10427
+ walkExpr(stmt.condition);
10428
+ stmt.body.forEach(walkStmt);
10429
+ break;
10430
+ case "ForStmt":
10431
+ declaredVars.add(stmt.name);
10432
+ walkExpr(stmt.start);
10433
+ walkExpr(stmt.end);
10434
+ if (stmt.step) walkExpr(stmt.step);
10435
+ stmt.body.forEach(walkStmt);
10436
+ break;
10437
+ }
10438
+ }
10439
+ function walkExpr(expr) {
10440
+ switch (expr.kind) {
10441
+ case "CallExpr":
10442
+ validateCall(expr);
10443
+ expr.args.forEach(walkExpr);
10444
+ if (expr.namedArgs) {
10445
+ for (const v of expr.namedArgs.values()) walkExpr(v);
10446
+ }
10447
+ break;
10448
+ case "NsCallExpr":
10449
+ validateNsCall(expr);
10450
+ expr.args.forEach(walkExpr);
10451
+ if (expr.namedArgs) {
10452
+ for (const v of expr.namedArgs.values()) walkExpr(v);
10453
+ }
10454
+ break;
10455
+ case "MemberExpr":
10456
+ validateMember(expr);
10457
+ break;
10458
+ case "Identifier": {
10459
+ const name = expr.name;
10460
+ if (!declaredVars.has(name) && !BUILTIN_SERIES.has(name) && !BUILTIN_CONSTANTS.has(name) && !BUILTIN_FUNCTIONS.has(name)) {
10461
+ issues.push({
10462
+ severity: "warning",
10463
+ code: "UNKNOWN_IDENT",
10464
+ message: `Unknown identifier '${name}' \u2014 may fail at runtime`
10465
+ });
10466
+ }
10467
+ break;
10468
+ }
10469
+ case "BinaryExpr":
10470
+ walkExpr(expr.left);
10471
+ walkExpr(expr.right);
10472
+ break;
10473
+ case "UnaryExpr":
10474
+ walkExpr(expr.operand);
10475
+ break;
10476
+ case "TernaryExpr":
10477
+ walkExpr(expr.condition);
10478
+ walkExpr(expr.consequent);
10479
+ walkExpr(expr.alternate);
10480
+ break;
10481
+ case "LogicalExpr":
10482
+ walkExpr(expr.left);
10483
+ walkExpr(expr.right);
10484
+ break;
10485
+ case "IndexExpr":
10486
+ walkExpr(expr.series);
10487
+ walkExpr(expr.index);
10488
+ break;
10489
+ }
10490
+ }
10491
+ function validateCall(expr) {
10492
+ if (!BUILTIN_FUNCTIONS.has(expr.callee)) {
10493
+ issues.push({
10494
+ severity: "error",
10495
+ code: "UNKNOWN_FUNCTION",
10496
+ message: `Unknown function '${expr.callee}()' \u2014 not a ForgeScript built-in`
10497
+ });
10498
+ }
10499
+ }
10500
+ function validateNsCall(expr) {
10501
+ const ns = expr.namespace;
10502
+ const fn = expr.fn;
10503
+ const allowed = NAMESPACE_FUNCTIONS[ns];
10504
+ if (!allowed) {
10505
+ issues.push({
10506
+ severity: "error",
10507
+ code: "UNKNOWN_NAMESPACE",
10508
+ message: `Unknown namespace '${ns}' \u2014 ForgeScript supports ta, math, color, str`
10509
+ });
10510
+ return;
10511
+ }
10512
+ if (!allowed.has(fn)) {
10513
+ issues.push({
10514
+ severity: "error",
10515
+ code: "UNKNOWN_NS_FUNCTION",
10516
+ message: `Unknown function '${ns}.${fn}()' \u2014 not a supported ${ns}.* built-in`
10517
+ });
10518
+ }
10519
+ }
10520
+ function validateMember(expr) {
10521
+ const obj = expr.object;
10522
+ const prop = expr.prop;
10523
+ const allowed = NAMESPACE_MEMBERS[obj];
10524
+ if (!allowed) {
10525
+ return;
10526
+ }
10527
+ if (!allowed.has(prop)) {
10528
+ issues.push({
10529
+ severity: "error",
10530
+ code: "UNKNOWN_MEMBER",
10531
+ message: `Unknown member '${obj}.${prop}' \u2014 not a recognised property`
10532
+ });
10533
+ }
10534
+ }
10535
+ for (const stmt of program.stmts) {
10536
+ walkStmt(stmt);
10537
+ }
10538
+ const hasErrors = issues.some((i) => i.severity === "error");
10539
+ return { ok: !hasErrors, issues };
10540
+ }
10541
+
10542
+ // src/forgescript/conversion/sandbox.ts
10543
+ function generateSyntheticBars(count) {
10544
+ const bars = [];
10545
+ let close = 100;
10546
+ for (let i = 0; i < count; i++) {
10547
+ const change = (Math.sin(i * 0.1) + Math.cos(i * 0.07)) * 2;
10548
+ close = Math.max(1, close + change);
10549
+ const high = close + Math.abs(Math.sin(i * 0.3)) * 3;
10550
+ const low = close - Math.abs(Math.cos(i * 0.3)) * 3;
10551
+ const open = close + (Math.sin(i * 0.2) - 0.5) * 2;
10552
+ bars.push({
10553
+ time: 1609459200 + i * 60,
10554
+ // 2021-01-01 + 1min bars
10555
+ open: Math.max(low, Math.min(high, open)),
10556
+ high,
10557
+ low,
10558
+ close,
10559
+ volume: 1e3 + Math.floor(Math.abs(Math.sin(i * 0.5)) * 5e3)
10560
+ });
10561
+ }
10562
+ return bars;
10563
+ }
10564
+ var SANDBOX_BARS = generateSyntheticBars(200);
10565
+ var MAX_EXECUTION_MS = 5e3;
10566
+ function runSandbox(source, bars) {
10567
+ const testBars = bars ?? SANDBOX_BARS;
10568
+ let indicator;
10569
+ try {
10570
+ indicator = new ForgeScriptIndicator(source);
10571
+ } catch (err) {
10572
+ return {
10573
+ ok: false,
10574
+ plotCount: 0,
10575
+ barCount: testBars.length,
10576
+ error: `Compile error: ${err instanceof Error ? err.message : String(err)}`,
10577
+ preview: []
10578
+ };
10579
+ }
10580
+ const start = Date.now();
10581
+ try {
10582
+ const result = indicator.runFull(testBars);
10583
+ const elapsed = Date.now() - start;
10584
+ if (elapsed > MAX_EXECUTION_MS) {
10585
+ return {
10586
+ ok: false,
10587
+ plotCount: result.plots.length,
10588
+ barCount: testBars.length,
10589
+ error: `Execution exceeded ${MAX_EXECUTION_MS}ms timeout (took ${elapsed}ms)`,
10590
+ preview: []
10591
+ };
10592
+ }
10593
+ const preview = result.plots.map((plot, i) => {
10594
+ const last = plot.length > 0 ? plot[plot.length - 1] : null;
10595
+ return {
10596
+ title: result.plotMetas[i]?.title ?? `Plot ${i + 1}`,
10597
+ lastValue: last?.value ?? NaN
10598
+ };
10599
+ });
10600
+ return {
10601
+ ok: true,
10602
+ plotCount: result.plots.length,
10603
+ barCount: testBars.length,
10604
+ preview
10605
+ };
10606
+ } catch (err) {
10607
+ return {
10608
+ ok: false,
10609
+ plotCount: 0,
10610
+ barCount: testBars.length,
10611
+ error: `Runtime error: ${err instanceof Error ? err.message : String(err)}`,
10612
+ preview: []
10613
+ };
10614
+ }
10615
+ }
10616
+
10617
+ // src/forgescript/conversion/save-gate.ts
10618
+ function evaluateSaveGate(params) {
10619
+ const { validation, sandbox, warnings, warningsAcknowledged, userConfirmed } = params;
10620
+ const blockers = [];
10621
+ const warningsToAcknowledge = [];
10622
+ if (!validation.ok) {
10623
+ const errors = validation.issues.filter((i) => i.severity === "error");
10624
+ blockers.push(
10625
+ `Validation failed with ${errors.length} error(s): ${errors.map((e) => e.message).join("; ")}`
10626
+ );
10627
+ }
10628
+ if (sandbox !== null && !sandbox.ok) {
10629
+ blockers.push(`Sandbox execution failed: ${sandbox.error ?? "unknown error"}`);
10630
+ }
10631
+ const significantWarnings = warnings.filter((w) => w.severity === "warning" || w.severity === "error");
10632
+ if (significantWarnings.length > 0) {
10633
+ for (const w of significantWarnings) {
10634
+ warningsToAcknowledge.push(w.message);
10635
+ }
10636
+ if (!warningsAcknowledged) {
10637
+ blockers.push(`${significantWarnings.length} warning(s) must be acknowledged before saving`);
10638
+ }
10639
+ }
10640
+ if (!userConfirmed) {
10641
+ blockers.push("User has not confirmed save");
10642
+ }
10643
+ return {
10644
+ canSave: blockers.length === 0,
10645
+ blockers,
10646
+ warningsToAcknowledge
10647
+ };
10648
+ }
10649
+
10650
+ // src/forgescript/conversion/prompts/index.ts
10651
+ var FORGESCRIPT_REFERENCE = `
10652
+ ## ForgeScript Language Reference
10653
+
10654
+ ForgeScript is a domain-specific language for financial technical indicators.
10655
+ It uses a bar-by-bar execution model with persistent series state.
10656
+
10657
+ ### Data Series (always available)
10658
+ open, high, low, close, volume, time, hl2, hlc3, ohlc4, bar_index
10659
+
10660
+ ### Bar State
10661
+ barstate.islast, barstate.isfirst, barstate.isconfirmed, barstate.isnew,
10662
+ barstate.isrealtime (always false), barstate.ishistory (always true)
10663
+
10664
+ ### Syntax
10665
+
10666
+ CRITICAL \u2014 Assignment vs Reassignment:
10667
+ - First assignment MUST use = (equals sign): length = input(14)
10668
+ - Reassignment of an existing variable uses := (colon-equals): length := length + 1
10669
+ - NEVER use := to declare a variable for the first time \u2014 this is a runtime error
10670
+ - var (init-once per symbol): var x = 0 (uses = for the initial declaration)
10671
+ - varip (init-once per session): varip x = 0 (uses = for the initial declaration)
10672
+ - if/else (using indented blocks)
10673
+ - for i = start to end [by step] (indented block)
10674
+ - while condition (indented block)
10675
+ - Ternary: condition ? then : else
10676
+ - Index lookback: series[n] \u2014 access n bars ago
10677
+ - Named arguments: plot(val, title="EMA", color=#FF0000)
10678
+
10679
+ ### Built-in Functions
10680
+
10681
+ #### Technical Analysis (flat or ta.* namespace)
10682
+ sma, ema, wma, rma, rsi, stdev, atr, highest, lowest, change, mom,
10683
+ crossover, crossunder, cross, macd, bb, stoch, obv, correlation,
10684
+ dev, variance, cum, sum, valuewhen, barssince, pivothigh, pivotlow,
10685
+ tr, swma, vwma, rising, falling
10686
+
10687
+ #### Math (flat or math.* namespace)
10688
+ abs, max, min, round, floor, ceil, sqrt, log, exp, pow, sign,
10689
+ sin, cos, tan, asin, acos, atan, todegrees, toradians, avg, sum, random, log10
10690
+
10691
+ #### Color
10692
+ 17 named constants: color.red, color.green, color.blue, etc.
10693
+ color.new(r, g, b, a), color.rgb(r, g, b, a)
10694
+ Hex literals: #RRGGBB or #RRGGBBAA
10695
+
10696
+ #### String
10697
+ str.tostring(value), str.tonumber(value), str.format(template, args...),
10698
+ str.length(s), str.trim(s), str.contains(s, sub), str.substring(s, start, end),
10699
+ str.replace_all(s, target, replacement), str.upper(s), str.lower(s),
10700
+ str.split(s, separator) \u2192 returns an array
10701
+
10702
+ #### Arrays
10703
+ array.new_float(size, initial_value), array.new_int(size, initial_value),
10704
+ array.new_string(size, initial_value), array.new_bool(size, initial_value),
10705
+ array.from(val1, val2, ...) \u2014 create array from values
10706
+ array.size(arr), array.get(arr, index), array.set(arr, index, value),
10707
+ array.push(arr, value), array.pop(arr), array.remove(arr, index),
10708
+ array.clear(arr), array.includes(arr, value), array.indexof(arr, value),
10709
+ array.slice(arr, start, end), array.join(arr, separator),
10710
+ array.sort(arr), array.reverse(arr),
10711
+ array.avg(arr), array.sum(arr), array.min(arr), array.max(arr)
10712
+
10713
+ #### Tables (screen-positioned text panels)
10714
+ table.new(position, columns, rows, bgcolor=, border_color=, border_width=, frame_color=, frame_width=) \u2014 returns table id
10715
+ table.cell(table_id, column, row, text, text_color=, bgcolor=, text_size=, text_halign=, text_valign=)
10716
+ table.clear(table_id, column, row) \u2014 remove a single cell
10717
+ table.delete(table_id) \u2014 remove the entire table
10718
+ Position values: "top_right", "top_left", "bottom_right", "bottom_left", etc.
10719
+ Alignment: text.align_left \u2192 "left", text.align_center \u2192 "center", text.align_right \u2192 "right"
10720
+ text.align_top \u2192 "top", text.align_bottom \u2192 "bottom"
10721
+
10722
+ #### User-defined Functions
10723
+ name(param1, param2) => expression
10724
+ name(param1, param2) =>
10725
+ statement1
10726
+ statement2
10727
+ result_expression
10728
+
10729
+ #### Output
10730
+ plot(value, title=, color=, linewidth=, style=)
10731
+ plotshape(condition, style=, location=, color=, text=, title=)
10732
+ plotchar, plotarrow, hline(price), fill, bgcolor(color), barcolor(color)
10733
+
10734
+ #### Pine Enum Constants (pass through as strings)
10735
+ shape.* \u2192 "circle", "triangleup", "arrowup", etc.
10736
+ location.* \u2192 "abovebar", "belowbar", "top", "bottom", "absolute"
10737
+ plot.style_* \u2192 "line", "histogram", "area", "columns"
10738
+ position.* \u2192 "top_right", "top_left", "bottom_right", "bottom_left", etc.
10739
+ size.* \u2192 "auto", "tiny", "small", "normal", "large", "huge"
10740
+ text.align_* \u2192 "left", "center", "right", "top", "bottom"
10741
+
10742
+ #### Utility
10743
+ input(defval, title=), input.text_area(defval), input.string(defval), na, nz(value, replacement)
10744
+
10745
+ ### NOT SUPPORTED (do NOT use these)
10746
+ - strategy.*, alertcondition, alert
10747
+ - label, line, box, matrix
10748
+ - import/export/module/library
10749
+ - Classes or OOP constructs
10750
+ - Async/await, promises, callbacks
10751
+ - File I/O, network requests
10752
+ - request.security (stub only \u2014 returns 0)
10753
+ `.trim();
10754
+ var PINE_SYSTEM_PROMPT = `
10755
+ You are a Pine Script to ForgeScript converter.
10756
+
10757
+ IMPORTANT RULES:
10758
+ 1. ForgeScript is very similar to Pine Script but NOT identical
10759
+ 2. Remove //@version=N annotations \u2014 ForgeScript does not use them
10760
+ 3. Convert ta.* calls \u2014 they work the same in ForgeScript
10761
+ 4. Convert color.* constants \u2014 ForgeScript uses the same naming
10762
+ 5. Keep indicator() declaration \u2014 this is required
10763
+ 6. Preserve var/varip semantics exactly
10764
+ 7. CRITICAL: Use = for first assignment, := ONLY for reassignment of existing variables
10765
+ - CORRECT: length = input(14) src = close value = rsi(src, length)
10766
+ - WRONG: length := input(14) src := close value := rsi(src, length)
10767
+ - := is ONLY for updating a variable that was already declared with = or var/varip
10768
+ 8. If the script uses strategy.*, you CANNOT convert it \u2014 report it as unsupported
10769
+ 9. If the script uses request.security(), warn that it returns 0 in ForgeScript
10770
+ 10. Remove type annotations (int, float, string, bool prefixes on variable declarations)
10771
+ 11. Convert for..in loops to standard for loops where possible
10772
+ 12. Keep plot(), plotshape(), bgcolor(), barcolor() \u2014 they are supported
10773
+ 13. Convert shape.* constants to plain strings: shape.circle \u2192 "circle", shape.triangleup \u2192 "triangleup", shape.arrowup \u2192 "arrowup", etc.
10774
+ 14. Convert location.* constants to plain strings: location.abovebar \u2192 "abovebar", location.belowbar \u2192 "belowbar", location.top \u2192 "top", location.bottom \u2192 "bottom", location.absolute \u2192 "absolute"
10775
+ 15. Convert plot.style_* constants to plain strings: plot.style_line \u2192 "line", plot.style_histogram \u2192 "histogram", plot.style_area \u2192 "area", plot.style_columns \u2192 "columns"
10776
+ 16. Pine v6 features: type/method/enum/import/export declarations are NOT supported \u2014 remove them and approximate the logic inline
10777
+ 17. Convert barstate.* properties \u2014 they are directly supported in ForgeScript (barstate.islast, barstate.isfirst, barstate.isconfirmed, barstate.isnew, barstate.isrealtime, barstate.ishistory)
10778
+ 18. Convert position.* constants to plain strings: position.top_right \u2192 "top_right", position.bottom_left \u2192 "bottom_left", etc.
10779
+ 19. Convert size.* constants to plain strings: size.large \u2192 "large", size.small \u2192 "small", size.auto \u2192 "auto", etc.
10780
+ 20. str.length(s), str.trim(s), str.contains(s, sub), str.substring(s, start, end), str.replace_all(s, target, rep), str.upper(s), str.lower(s) are all supported
10781
+ 21. array.* functions are supported \u2014 convert Pine array operations directly (array.new_float, array.push, array.get, array.size, etc.)
10782
+ 22. table.* functions are supported \u2014 convert Pine table operations directly (table.new, table.cell, table.clear, table.delete). Preserve frame_color, frame_width, border_color, border_width named args. Preserve text_halign and text_valign named args in table.cell().
10783
+ 23. str.split(s, separator) is supported \u2014 it returns a ForgeScript array
10784
+ 24. User-defined functions use => syntax: myFunc(a, b) => a + b \u2014 convert Pine functions to this form
10785
+ 25. Convert text.align_* constants to plain strings: text.align_left \u2192 "left", text.align_center \u2192 "center", text.align_right \u2192 "right", text.align_top \u2192 "top", text.align_bottom \u2192 "bottom"
10786
+ 26. input.text_area(defval) and input.string(defval) are supported \u2014 they return string values. Convert Pine input.text_area() and input.string() with their default values.
10787
+ 27. input.string() with options= parameter: the options list is informational only \u2014 convert to input.string(defval) keeping just the default value
10788
+
10789
+ ${FORGESCRIPT_REFERENCE}
10790
+ `.trim();
10791
+ var PYTHON_SYSTEM_PROMPT = `
10792
+ You are a Python to ForgeScript converter.
10793
+
10794
+ IMPORTANT RULES:
10795
+ 1. ForgeScript is a bar-by-bar DSL, NOT a general-purpose language
10796
+ 2. Python pandas/numpy operations must be decomposed into per-bar logic
10797
+ 3. Rolling window operations (df.rolling()) map to ForgeScript built-ins (sma, ema, etc.)
10798
+ 4. Python def functions \u2192 convert to ForgeScript => syntax: myFunc(a, b) => a + b
10799
+ 5. Python classes are NOT supported
10800
+ 6. Python list comprehensions \u2192 for loops or array.* functions
10801
+ 7. np.mean over a window \u2192 sma()
10802
+ 8. talib.RSI \u2192 rsi()
10803
+ 9. Any file I/O, HTTP requests, or database calls \u2192 UNSUPPORTED
10804
+ 10. matplotlib/plotly plotting \u2192 plot() calls
10805
+ 11. All state must use var/varip or direct assignment \u2014 no global dicts
10806
+ 12. Convert common ta-lib function names to ForgeScript equivalents:
10807
+ - talib.SMA \u2192 sma, talib.EMA \u2192 ema, talib.RSI \u2192 rsi
10808
+ - talib.BBANDS \u2192 bb, talib.MACD \u2192 macd, talib.STOCH \u2192 stoch
10809
+ - talib.ATR \u2192 atr, talib.OBV \u2192 obv
10810
+ 13. CRITICAL: Use = for first assignment, := ONLY for reassignment of existing variables
10811
+
10812
+ ${FORGESCRIPT_REFERENCE}
10813
+ `.trim();
10814
+ var JAVASCRIPT_SYSTEM_PROMPT = `
10815
+ You are a JavaScript/TypeScript to ForgeScript converter.
10816
+
10817
+ IMPORTANT RULES:
10818
+ 1. ForgeScript is a bar-by-bar DSL \u2014 no event loops, callbacks, or async
10819
+ 2. Convert for/while loops that iterate over bars into per-bar logic with lookback
10820
+ 3. Array.reduce, .map, .filter operations \u2192 for loops, array.* functions, or built-in aggregates
10821
+ 4. Function declarations \u2192 convert to ForgeScript => syntax: myFunc(a, b) => a + b
10822
+ 5. Classes \u2192 NOT SUPPORTED
10823
+ 6. Promises, async/await \u2192 NOT SUPPORTED
10824
+ 7. DOM manipulation \u2192 NOT SUPPORTED
10825
+ 8. console.log \u2192 remove or convert to plot()
10826
+ 9. Convert any technical indicator library calls to ForgeScript equivalents
10827
+ 10. Module imports/exports \u2192 NOT SUPPORTED
10828
+ 11. JSON, fetch, XMLHttpRequest \u2192 NOT SUPPORTED
10829
+ 12. Math.* maps directly to ForgeScript math.* or flat functions
10830
+ 13. CRITICAL: Use = for first assignment, := ONLY for reassignment of existing variables
10831
+
10832
+ ${FORGESCRIPT_REFERENCE}
10833
+ `.trim();
10834
+ var GENERIC_SYSTEM_PROMPT = `
10835
+ You are a script converter that translates arbitrary scripting code into ForgeScript.
10836
+
10837
+ IMPORTANT RULES:
10838
+ 1. First identify what the script is trying to compute (technical indicator, signal, etc.)
10839
+ 2. Re-implement the logic using only ForgeScript constructs
10840
+ 3. Do NOT try to transliterate line-by-line \u2014 understand the intent first
10841
+ 4. Use only supported ForgeScript built-in functions
10842
+ 5. If the script uses concepts that don't map to bar-by-bar execution, report them as unsupported
10843
+ 6. Be conservative \u2014 when in doubt, mark as unsupported rather than guessing
10844
+ 7. CRITICAL: Use = for first assignment, := ONLY for reassignment of existing variables
10845
+
10846
+ ${FORGESCRIPT_REFERENCE}
10847
+ `.trim();
10848
+ function getSystemPrompt(language) {
10849
+ switch (language) {
10850
+ case "pine":
10851
+ return PINE_SYSTEM_PROMPT;
10852
+ case "python":
10853
+ return PYTHON_SYSTEM_PROMPT;
10854
+ case "javascript":
10855
+ return JAVASCRIPT_SYSTEM_PROMPT;
10856
+ case "forgescript":
10857
+ return PINE_SYSTEM_PROMPT;
10858
+ // reuse Pine rules — they're closest
10859
+ default:
10860
+ return GENERIC_SYSTEM_PROMPT;
10861
+ }
10862
+ }
10863
+ function buildConversionPrompt(sourceLanguage, sourceCode) {
10864
+ const langLabel = sourceLanguage === "unknown" ? "an unknown language" : sourceLanguage;
10865
+ return [
10866
+ `Convert the following ${langLabel} script to valid ForgeScript.`,
10867
+ `Use = for new variable declarations, := only for reassignment of existing variables.`,
10868
+ "",
10869
+ "```",
10870
+ sourceCode,
10871
+ "```"
10872
+ ].join("\n");
10873
+ }
10874
+
10875
+ // src/forgescript/conversion/ai-conversion-agent.ts
10876
+ var SECTION_HEADER = /^###\s+SECTION\s+(\d+)\s*[—–-]\s*(.*)/im;
10877
+ function parseAIResponse(raw) {
10878
+ const sections = splitSections(raw);
10879
+ const codeBlock = sections[1] ?? "";
10880
+ const forgeScript = extractCodeBlock(codeBlock);
10881
+ const warnings = parseBullets(sections[2] ?? "").map((msg) => ({
10882
+ severity: "warning",
10883
+ category: categoriseWarning(msg),
10884
+ message: msg
10885
+ }));
10886
+ const assumptions = parseBullets(sections[3] ?? "").map((d) => ({ description: d }));
10887
+ const reviewChecklist = parseBullets(sections[4] ?? "").map((d) => ({ description: d }));
10888
+ const unsupportedFeatures = parseBullets(sections[5] ?? "");
10889
+ const { confidence, explanation } = parseConfidence(sections[6] ?? "");
10890
+ return {
10891
+ forgeScript,
10892
+ warnings,
10893
+ assumptions,
10894
+ reviewChecklist,
10895
+ unsupportedFeatures,
10896
+ confidence,
10897
+ confidenceExplanation: explanation
10898
+ };
10899
+ }
10900
+ async function runAIConversion(source, language, llm) {
10901
+ const systemPrompt = getSystemPrompt(language);
10902
+ const userMessage = buildConversionPrompt(language, source);
10903
+ const messages = [
10904
+ { role: "system", content: systemPrompt },
10905
+ { role: "user", content: userMessage }
10906
+ ];
10907
+ const raw = await llm(messages);
10908
+ if (raw === null) return null;
10909
+ try {
10910
+ const parsed = JSON.parse(raw);
10911
+ if (typeof parsed.forgeScript === "string" && Array.isArray(parsed.warnings)) {
10912
+ return parseStructuredJSON(parsed);
10913
+ }
10914
+ } catch {
10915
+ }
10916
+ return parseAIResponse(raw);
10917
+ }
10918
+ function parseStructuredJSON(obj) {
10919
+ const VALID_SEVERITIES = /* @__PURE__ */ new Set(["info", "warning", "error"]);
10920
+ const VALID_CATEGORIES = /* @__PURE__ */ new Set(["approximation", "unsupported", "semantic_risk", "hallucination", "general"]);
10921
+ const VALID_CONFIDENCE = /* @__PURE__ */ new Set(["HIGH", "MEDIUM", "LOW"]);
10922
+ const rawWarnings = Array.isArray(obj.warnings) ? obj.warnings : [];
10923
+ const warnings = rawWarnings.filter((w) => w !== null && typeof w === "object").map((w) => ({
10924
+ severity: VALID_SEVERITIES.has(String(w.severity)) ? String(w.severity) : "warning",
10925
+ category: VALID_CATEGORIES.has(String(w.category)) ? String(w.category) : "general",
10926
+ message: String(w.message ?? "")
10927
+ }));
10928
+ const toDescList = (arr) => (Array.isArray(arr) ? arr : []).filter((v) => v !== null && typeof v === "object").map((v) => ({ description: String(v.description ?? "") }));
10929
+ const confidence = VALID_CONFIDENCE.has(String(obj.confidence)) ? String(obj.confidence) : "LOW";
10930
+ return {
10931
+ forgeScript: String(obj.forgeScript ?? ""),
10932
+ warnings,
10933
+ assumptions: toDescList(obj.assumptions),
10934
+ reviewChecklist: toDescList(obj.reviewChecklist),
10935
+ unsupportedFeatures: Array.isArray(obj.unsupportedFeatures) ? obj.unsupportedFeatures.map(String) : [],
10936
+ confidence,
10937
+ confidenceExplanation: String(obj.confidenceExplanation ?? "No explanation provided")
10938
+ };
10939
+ }
10940
+ function splitSections(raw) {
10941
+ const out = {};
10942
+ const parts = raw.split(SECTION_HEADER);
10943
+ for (let i = 1; i < parts.length; i += 3) {
10944
+ const num = parseInt(parts[i], 10);
10945
+ const body = (parts[i + 2] ?? "").trim();
10946
+ if (!isNaN(num)) out[num] = body;
10947
+ }
10948
+ if (Object.keys(out).length === 0) {
10949
+ out[1] = raw;
10950
+ }
10951
+ return out;
10952
+ }
10953
+ function extractCodeBlock(text) {
10954
+ const match = text.match(/```(?:forgescript)?\s*\n([\s\S]*?)```/);
10955
+ if (match) return match[1].trim();
10956
+ return text.trim();
10957
+ }
10958
+ function parseBullets(text) {
10959
+ if (text.toLowerCase().trim() === "none") return [];
10960
+ return text.split("\n").map((l) => l.replace(/^[-*•]\s*/, "").trim()).filter((l) => l.length > 0 && l.toLowerCase() !== "none");
10961
+ }
10962
+ function categoriseWarning(msg) {
10963
+ const lower = msg.toLowerCase();
10964
+ if (lower.includes("approximat")) return "approximation";
10965
+ if (lower.includes("unsupported") || lower.includes("not supported")) return "unsupported";
10966
+ if (lower.includes("semantic") || lower.includes("risk") || lower.includes("different")) return "semantic_risk";
10967
+ return "general";
10968
+ }
10969
+ function parseConfidence(text) {
10970
+ const lines = text.split("\n").filter((l) => l.trim().length > 0);
10971
+ let confidence = "LOW";
10972
+ let explanation = "No confidence information provided";
10973
+ for (const line of lines) {
10974
+ const upper = line.trim().toUpperCase();
10975
+ if (upper.startsWith("HIGH")) {
10976
+ confidence = "HIGH";
10977
+ explanation = line.trim().replace(/^HIGH\s*/i, "");
10978
+ } else if (upper.startsWith("MEDIUM")) {
10979
+ confidence = "MEDIUM";
10980
+ explanation = line.trim().replace(/^MEDIUM\s*/i, "");
10981
+ } else if (upper.startsWith("LOW")) {
10982
+ confidence = "LOW";
10983
+ explanation = line.trim().replace(/^LOW\s*/i, "");
10984
+ } else if (!explanation || explanation === "No confidence information provided") {
10985
+ explanation = line.trim();
10986
+ }
10987
+ }
10988
+ return { confidence, explanation: explanation || "No explanation provided" };
10989
+ }
10990
+
10991
+ // src/forgescript/conversion/telemetry.ts
10992
+ var listeners = [];
10993
+ function onConversionEvent(listener) {
10994
+ listeners.push(listener);
10995
+ return () => {
10996
+ const idx = listeners.indexOf(listener);
10997
+ if (idx >= 0) listeners.splice(idx, 1);
10998
+ };
10999
+ }
11000
+ function emitConversionEvent(event) {
11001
+ for (const listener of listeners) {
11002
+ try {
11003
+ listener(event);
11004
+ } catch {
11005
+ }
11006
+ }
11007
+ }
11008
+ function buildTelemetryEvent(params) {
11009
+ return { timestamp: Date.now(), ...params };
11010
+ }
11011
+ function clearTelemetryListeners() {
11012
+ listeners.length = 0;
11013
+ }
11014
+
11015
+ // src/forgescript/conversion/conversion-orchestrator.ts
11016
+ async function convertScript(source, options = {}) {
11017
+ const {
11018
+ llmProvider,
11019
+ skipSandbox = false,
11020
+ acknowledgeWarnings = false,
11021
+ confirmSave = false,
11022
+ forceAI = false
11023
+ } = options;
11024
+ const detectedLanguage = options.sourceLanguage ?? (options.aiClassifier ? (await detectLanguageWithFallback(source, options.aiClassifier)).language : detectLanguage(source).language);
11025
+ let forgeScript = "";
11026
+ let conversionPath = "passthrough";
11027
+ let warnings = [];
11028
+ let assumptions = [];
11029
+ let reviewChecklist = [];
11030
+ let unsupportedFeatures = [];
11031
+ let confidence = "HIGH";
11032
+ let confidenceExplanation = "Source is already ForgeScript";
11033
+ let deterministicOutput;
11034
+ if (detectedLanguage === "forgescript") {
11035
+ forgeScript = source;
11036
+ conversionPath = "passthrough";
11037
+ } else if (detectedLanguage === "pine" && !forceAI) {
11038
+ conversionPath = "deterministic";
11039
+ try {
11040
+ const compiler = new PineCompiler();
11041
+ const result2 = compiler.compile(source);
11042
+ forgeScript = result2.tscript;
11043
+ deterministicOutput = result2.tscript;
11044
+ for (const d of result2.diagnostics) {
11045
+ warnings.push({
11046
+ severity: d.severity === "error" ? "error" : "warning",
11047
+ category: d.code === "PINE_UNSUPPORTED_FN" ? "unsupported" : "general",
11048
+ message: `[Pine] ${d.message}`
11049
+ });
11050
+ }
11051
+ confidence = result2.ok ? "HIGH" : "MEDIUM";
11052
+ confidenceExplanation = result2.ok ? "Deterministic Pine\u2192ForgeScript conversion completed without errors" : "Deterministic conversion completed with warnings \u2014 review recommended";
11053
+ if (llmProvider && !result2.ok) {
11054
+ const aiResult = await runAIConversion(source, "pine", llmProvider);
11055
+ if (aiResult && aiResult.forgeScript.trim().length > 0) {
11056
+ conversionPath = "hybrid";
11057
+ forgeScript = aiResult.forgeScript;
11058
+ warnings = [...warnings, ...aiResult.warnings];
11059
+ assumptions = aiResult.assumptions;
11060
+ reviewChecklist = aiResult.reviewChecklist;
11061
+ unsupportedFeatures = aiResult.unsupportedFeatures;
11062
+ confidence = aiResult.confidence;
11063
+ confidenceExplanation = `Hybrid: deterministic conversion had issues, AI refinement applied. ${aiResult.confidenceExplanation}`;
11064
+ }
11065
+ }
11066
+ } catch (err) {
11067
+ const errMsg = err instanceof Error ? err.message : String(err);
11068
+ warnings.push({
11069
+ severity: "warning",
11070
+ category: "general",
11071
+ message: `Deterministic Pine conversion failed: ${errMsg}`
11072
+ });
11073
+ if (llmProvider) {
11074
+ const aiResult = await runAIConversion(source, "pine", llmProvider);
11075
+ if (aiResult) {
11076
+ conversionPath = "ai";
11077
+ forgeScript = aiResult.forgeScript;
11078
+ warnings = [...warnings, ...aiResult.warnings];
11079
+ assumptions = aiResult.assumptions;
11080
+ reviewChecklist = aiResult.reviewChecklist;
11081
+ unsupportedFeatures = aiResult.unsupportedFeatures;
11082
+ confidence = aiResult.confidence;
11083
+ confidenceExplanation = aiResult.confidenceExplanation;
11084
+ }
11085
+ }
11086
+ }
11087
+ } else {
11088
+ conversionPath = "ai";
11089
+ if (!llmProvider) {
11090
+ warnings.push({
11091
+ severity: "error",
11092
+ category: "general",
11093
+ message: `AI conversion required for ${detectedLanguage} but no LLM provider configured`
11094
+ });
11095
+ confidence = "LOW";
11096
+ confidenceExplanation = "No LLM provider available for non-Pine conversion";
11097
+ } else {
11098
+ const aiResult = await runAIConversion(source, detectedLanguage, llmProvider);
11099
+ if (aiResult) {
11100
+ forgeScript = aiResult.forgeScript;
11101
+ warnings = aiResult.warnings;
11102
+ assumptions = aiResult.assumptions;
11103
+ reviewChecklist = aiResult.reviewChecklist;
11104
+ unsupportedFeatures = aiResult.unsupportedFeatures;
11105
+ confidence = aiResult.confidence;
11106
+ confidenceExplanation = aiResult.confidenceExplanation;
11107
+ } else {
11108
+ warnings.push({
11109
+ severity: "error",
11110
+ category: "general",
11111
+ message: "AI conversion returned no result \u2014 LLM may be unavailable"
11112
+ });
11113
+ confidence = "LOW";
11114
+ confidenceExplanation = "AI conversion failed";
11115
+ }
11116
+ }
11117
+ }
11118
+ const validation = forgeScript.trim().length > 0 ? validateForgeScript(forgeScript) : { ok: false, issues: [{ severity: "error", code: "EMPTY_SCRIPT", message: "No ForgeScript produced" }] };
11119
+ const sandbox = !skipSandbox && validation.ok ? runSandbox(forgeScript) : null;
11120
+ const saveGate = evaluateSaveGate({
11121
+ validation,
11122
+ sandbox,
11123
+ warnings,
11124
+ warningsAcknowledged: acknowledgeWarnings,
11125
+ userConfirmed: confirmSave
11126
+ });
11127
+ emitConversionEvent(buildTelemetryEvent({
11128
+ sourceLanguage: detectedLanguage,
11129
+ detectionConfidence: confidence,
11130
+ conversionPath,
11131
+ validationOk: validation.ok,
11132
+ sandboxOk: sandbox?.ok ?? null,
11133
+ savedByUser: null,
11134
+ // set by the caller after save
11135
+ errorCount: validation.issues.filter((i) => i.severity === "error").length,
11136
+ warningCount: warnings.length,
11137
+ unsupportedCount: unsupportedFeatures.length
11138
+ }));
11139
+ const result = {
11140
+ sourceLanguage: detectedLanguage,
11141
+ conversionPath,
11142
+ forgeScript,
11143
+ validation,
11144
+ sandbox,
11145
+ saveGate,
11146
+ warnings,
11147
+ assumptions,
11148
+ reviewChecklist,
11149
+ unsupportedFeatures,
11150
+ confidence,
11151
+ confidenceExplanation
11152
+ };
11153
+ if (deterministicOutput !== void 0) {
11154
+ result.deterministicOutput = deterministicOutput;
11155
+ }
11156
+ return result;
11157
+ }
11158
+
8059
11159
  // src/api/drawing tools/pointers menu/cursor.ts
8060
11160
  var CURSOR_LABEL = "Arrow";
8061
11161
  var CURSOR_STYLE = "default";
@@ -8236,7 +11336,7 @@ var PixiGridRenderer = class {
8236
11336
  if (x < 0 || x > pw) continue;
8237
11337
  gfx.moveTo(x, 0).lineTo(x, ph).stroke({ color: this._colors.grid, width: 1, alpha: 0.4 });
8238
11338
  gfx.moveTo(x, ph).lineTo(x, ph + TICK_LEN).stroke({ color: this._colors.border, width: 1 });
8239
- const label = formatTimestampLabel(t, step);
11339
+ const label = formatTimestampLabel(t, step, this._timeScale.timezone);
8240
11340
  const tl = new Text({ text: label, style: timeLabelStyle });
8241
11341
  tl.x = x - tl.width / 2;
8242
11342
  tl.y = ph + TICK_LEN + 2;
@@ -8291,7 +11391,7 @@ var PixiGridRenderer = class {
8291
11391
  var UP_COLOR = 2532929;
8292
11392
  var DOWN_COLOR = 16273737;
8293
11393
  var MIN_BODY_H = 1;
8294
- var MAX_BAR_W = 20;
11394
+ var MAX_BAR_W = Number.MAX_SAFE_INTEGER;
8295
11395
  var MIN_BAR_W = 1;
8296
11396
  var PixiCandlestickRenderer = class {
8297
11397
  constructor(layer, _upColor = UP_COLOR, _downColor = DOWN_COLOR) {
@@ -8847,6 +11947,6 @@ var PixiChart = class {
8847
11947
  }
8848
11948
  };
8849
11949
 
8850
- export { CROSSHAIR_LABEL, CROSSHAIR_STYLE, CURSOR_LABEL, CURSOR_STYLE, CandleEngine, Chart, ChartRuntimeResolver, CoordTransform, Crosshair, DEMONSTRATION_COLOR, DEMONSTRATION_FILL, DEMONSTRATION_LABEL, DEMONSTRATION_RADIUS, DEMONSTRATION_STROKE, DEMONSTRATION_STYLE, DOT_COLOR, DOT_LABEL, DOT_RADIUS, DOT_STYLE, DatafeedConnector, DiagnosticBag, DirtyFlags, IndicatorDAG, InteractionManager, LayerName, LicenseManager, ManagedTradingController, PaneManager, PineCompiler, PixiChart, PixiLayerManager, PriceScale, ReferenceAPI, Series, TChart, TScriptIndicator, TScriptRuntime, Series2 as TScriptSeries, TimeScale, TradingOverlayStore, UnmanagedIngestion, assertCanPlaceOrders, assertCanUseBrackets, assertCanUseDraggableOrders, assertCanUseManagedTrading, assertManagedMode, canPlaceBrackets, canPlaceOrders, canRenderBracketControls, canRenderBuySellButtons, canRenderCandles, canRenderDrawings, canRenderExternalOverlayOnlyMode, canRenderFills, canRenderIndicators, canRenderManagedTradingControls, canRenderOrderEntry, canRenderOrderModificationControls, canRenderOrderTicket, canRenderOverlays, canRenderPositions, canUseBracketOrders, canUseDraggable, canUseDraggableOrders, canUseExternalIngestion, canUseIndicators, canUseManagedTrading, canUseManagedTradingHook, canUseOrderEntry, computeEMA, computeEMAFromSeries, computeMACD, computeRSI, computeSMA, computeSMAFromSeries, computeVolume, computeWMAFromSeries, createChart, crosshairTool, cursorTool, demonstrationTool, dotTool, extractField, getBucketStart2 as getBucketStart, getCapabilities, getMissingBarCount, isManagedCapable, isManagedMode, isUnmanagedMode, mergeBars, timeframeToMs2 as timeframeToMs };
11950
+ export { CROSSHAIR_LABEL, CROSSHAIR_STYLE, CURSOR_LABEL, CURSOR_STYLE, CandleEngine, Chart, ChartRuntimeResolver, CoordTransform, Crosshair, DEMONSTRATION_COLOR, DEMONSTRATION_FILL, DEMONSTRATION_LABEL, DEMONSTRATION_RADIUS, DEMONSTRATION_STROKE, DEMONSTRATION_STYLE, DOT_COLOR, DOT_LABEL, DOT_RADIUS, DOT_STYLE, DatafeedConnector, DiagnosticBag, DirtyFlags, FORGESCRIPT_REFERENCE, ForgeScriptIndicator, ForgeScriptRuntime, Series2 as ForgeScriptSeries, IndicatorDAG, InteractionManager, LayerName, LicenseManager, ManagedTradingController, PaneManager, PineCompiler, PixiChart, PixiLayerManager, PriceScale, ReferenceAPI, Series, TChart, TimeScale, TradingOverlayStore, UnmanagedIngestion, assertCanPlaceOrders, assertCanUseBrackets, assertCanUseDraggableOrders, assertCanUseManagedTrading, assertManagedMode, buildConversionPrompt, canPlaceBrackets, canPlaceOrders, canRenderBracketControls, canRenderBuySellButtons, canRenderCandles, canRenderDrawings, canRenderExternalOverlayOnlyMode, canRenderFills, canRenderIndicators, canRenderManagedTradingControls, canRenderOrderEntry, canRenderOrderModificationControls, canRenderOrderTicket, canRenderOverlays, canRenderPositions, canUseBracketOrders, canUseDraggable, canUseDraggableOrders, canUseExternalIngestion, canUseIndicators, canUseManagedTrading, canUseManagedTradingHook, canUseOrderEntry, clearTelemetryListeners, computeEMA, computeEMAFromSeries, computeMACD, computeRSI, computeSMA, computeSMAFromSeries, computeVolume, computeWMAFromSeries, convertScript, createChart, crosshairTool, cursorTool, demonstrationTool, detectLanguage, detectLanguageWithFallback, dotTool, evaluateSaveGate, exchangeNow, extractField, getBucketStart2 as getBucketStart, getCapabilities, getMissingBarCount, getSystemPrompt, initServerClock, isManagedCapable, isManagedMode, isUnmanagedMode, mergeBars, onConversionEvent, parseAIResponse, runAIConversion, runSandbox, serverClockOffset, serverNow, timeframeToMs2 as timeframeToMs, validateForgeScript };
8851
11951
  //# sourceMappingURL=internal.js.map
8852
11952
  //# sourceMappingURL=internal.js.map