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