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