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