@forgecharts/sdk 1.1.32 → 1.1.36

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