desic-okx-agent 0.3.1 → 0.3.3
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/README.en.md +1 -1
- package/README.md +1 -1
- package/dist/i18n/messages.d.ts +27 -0
- package/dist/i18n/messages.js +55 -1
- package/dist/i18n/messages.js.map +1 -1
- package/dist/report/brand-font.d.ts +1 -0
- package/dist/report/brand-font.js +4 -0
- package/dist/report/brand-font.js.map +1 -0
- package/dist/report/chart-script.d.ts +3 -8
- package/dist/report/chart-script.js +735 -115
- package/dist/report/chart-script.js.map +1 -1
- package/dist/report/compare-html.d.ts +1 -6
- package/dist/report/compare-html.js +177 -122
- package/dist/report/compare-html.js.map +1 -1
- package/dist/report/compare-script.d.ts +1 -11
- package/dist/report/compare-script.js +310 -64
- package/dist/report/compare-script.js.map +1 -1
- package/dist/report/fetch.d.ts +32 -0
- package/dist/report/fetch.js +42 -0
- package/dist/report/fetch.js.map +1 -1
- package/dist/report/html.d.ts +1 -1
- package/dist/report/html.js +846 -268
- package/dist/report/html.js.map +1 -1
- package/dist/strategy/service.js +4 -1
- package/dist/strategy/service.js.map +1 -1
- package/dist/strategy/store.d.ts +12 -1
- package/dist/strategy/store.js +32 -1
- package/dist/strategy/store.js.map +1 -1
- package/dist/tools/catalog.js +20 -1
- package/dist/tools/catalog.js.map +1 -1
- package/dist/tui/app.js +106 -35
- package/dist/tui/app.js.map +1 -1
- package/dist/tui/commands.d.ts +33 -0
- package/dist/tui/commands.js +59 -14
- package/dist/tui/commands.js.map +1 -1
- package/dist/tui/file-completion.d.ts +13 -0
- package/dist/tui/file-completion.js +57 -0
- package/dist/tui/file-completion.js.map +1 -0
- package/docs/strategy-research.md +16 -3
- package/package.json +1 -1
- package/python/desic_strategy/context.py +23 -5
- package/python/desic_strategy/engine.py +4 -1
- package/python/desic_strategy/live.py +1 -1
- package/python/desic_strategy/policy.py +51 -0
- package/python/desic_strategy/runner.py +36 -9
- package/python/desic_strategy/timeframe.py +17 -0
- package/skills/okx-strategy-research/SKILL.md +55 -1
- package/skills/okx-strategy-research/agents/openai.yaml +1 -1
- package/skills/okx-strategy-research/references/python-api.md +7 -0
- package/skills/okx-strategy-research/references/tools-and-data.md +20 -0
|
@@ -1,45 +1,183 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Interaction layer for the self-contained backtest observatory.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* beyond one IIFE. That keeps the "opens offline, sends nothing" property of the
|
|
7
|
-
* report intact while making individual values readable.
|
|
8
|
-
*
|
|
9
|
-
* Written as a plain string rather than compiled TypeScript so the report stays a
|
|
10
|
-
* single self-contained file with no build step of its own.
|
|
4
|
+
* The report stays offline: this script reads only data embedded in the document
|
|
5
|
+
* and updates its SVG viewports, readouts, filters, and navigation in place.
|
|
11
6
|
*/
|
|
12
7
|
export const CHART_SCRIPT = String.raw `
|
|
13
8
|
(function () {
|
|
14
9
|
var chart = document.getElementById("equity-chart");
|
|
15
10
|
var drawdownChart = document.getElementById("drawdown-chart");
|
|
16
|
-
|
|
11
|
+
var frame = document.querySelector(".observatory-frame");
|
|
12
|
+
var themeToggle = document.querySelector("[data-theme-toggle]");
|
|
13
|
+
if (themeToggle) {
|
|
14
|
+
var systemDark = matchMedia("(prefers-color-scheme: dark)").matches;
|
|
15
|
+
themeToggle.setAttribute("aria-pressed", systemDark ? "true" : "false");
|
|
16
|
+
themeToggle.addEventListener("click", function () {
|
|
17
|
+
var explicit = document.body.getAttribute("data-theme");
|
|
18
|
+
var dark = explicit ? explicit === "dark" : matchMedia("(prefers-color-scheme: dark)").matches;
|
|
19
|
+
document.body.setAttribute("data-theme", dark ? "light" : "dark");
|
|
20
|
+
themeToggle.setAttribute("aria-pressed", dark ? "false" : "true");
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
var orbit = document.querySelector(".orbit-visual");
|
|
25
|
+
if (orbit) {
|
|
26
|
+
var orbitCore = orbit.querySelector(".orbit-core");
|
|
27
|
+
var orbitLabel = orbitCore ? orbitCore.querySelector("span") : null;
|
|
28
|
+
var orbitValue = orbitCore ? orbitCore.querySelector("strong") : null;
|
|
29
|
+
var orbitDetail = orbitCore ? orbitCore.querySelector("small") : null;
|
|
30
|
+
var orbitHits = Array.from(orbit.querySelectorAll("[data-orbit-key]"));
|
|
31
|
+
var orbitShapes = Array.from(orbit.querySelectorAll("[data-orbit-shape]"));
|
|
32
|
+
var orbitLegends = Array.from(document.querySelectorAll("[data-orbit-legend]"));
|
|
33
|
+
var pinnedOrbitKey = "";
|
|
34
|
+
|
|
35
|
+
function pulseOrbitCore() {
|
|
36
|
+
if (!orbitCore || matchMedia("(prefers-reduced-motion: reduce)").matches || !orbitCore.animate) return;
|
|
37
|
+
if (orbitCore.__orbitAnimation) orbitCore.__orbitAnimation.cancel();
|
|
38
|
+
orbitCore.__orbitAnimation = orbitCore.animate([{ opacity: 0.48 }, { opacity: 1 }], {
|
|
39
|
+
duration: 180,
|
|
40
|
+
easing: "cubic-bezier(0.25, 1, 0.5, 1)"
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function resetOrbit() {
|
|
45
|
+
pinnedOrbitKey = "";
|
|
46
|
+
orbit.classList.remove("is-exploring");
|
|
47
|
+
orbitShapes.forEach(function (shape) { shape.classList.remove("is-active"); });
|
|
48
|
+
orbitLegends.forEach(function (legend) { legend.classList.remove("is-active"); });
|
|
49
|
+
orbitHits.forEach(function (hit) { hit.setAttribute("aria-pressed", "false"); });
|
|
50
|
+
if (orbitLabel) orbitLabel.textContent = orbit.getAttribute("data-default-label") || "";
|
|
51
|
+
if (orbitValue) orbitValue.textContent = orbit.getAttribute("data-default-value") || "";
|
|
52
|
+
if (orbitDetail) orbitDetail.textContent = orbit.getAttribute("data-default-detail") || "";
|
|
53
|
+
pulseOrbitCore();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function showOrbit(hit, pin) {
|
|
57
|
+
var key = hit.getAttribute("data-orbit-key") || "";
|
|
58
|
+
if (pin) pinnedOrbitKey = key;
|
|
59
|
+
orbit.classList.add("is-exploring");
|
|
60
|
+
orbitShapes.forEach(function (shape) {
|
|
61
|
+
shape.classList.toggle("is-active", shape.getAttribute("data-orbit-shape") === key);
|
|
62
|
+
});
|
|
63
|
+
orbitLegends.forEach(function (legend) {
|
|
64
|
+
legend.classList.toggle("is-active", legend.getAttribute("data-orbit-legend") === key);
|
|
65
|
+
});
|
|
66
|
+
orbitHits.forEach(function (item) {
|
|
67
|
+
item.setAttribute("aria-pressed", pin && item === hit ? "true" : "false");
|
|
68
|
+
});
|
|
69
|
+
if (orbitLabel) orbitLabel.textContent = hit.getAttribute("data-orbit-label") || "";
|
|
70
|
+
if (orbitValue) orbitValue.textContent = hit.getAttribute("data-orbit-value") || "";
|
|
71
|
+
if (orbitDetail) orbitDetail.textContent = hit.getAttribute("data-orbit-detail") || "";
|
|
72
|
+
pulseOrbitCore();
|
|
73
|
+
}
|
|
17
74
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
75
|
+
orbitHits.forEach(function (hit) {
|
|
76
|
+
hit.addEventListener("pointerenter", function () {
|
|
77
|
+
if (!pinnedOrbitKey) showOrbit(hit, false);
|
|
78
|
+
});
|
|
79
|
+
hit.addEventListener("pointerleave", function () {
|
|
80
|
+
if (!pinnedOrbitKey && !orbit.matches(":focus-within")) resetOrbit();
|
|
81
|
+
});
|
|
82
|
+
hit.addEventListener("focus", function () { showOrbit(hit, pinnedOrbitKey === hit.getAttribute("data-orbit-key")); });
|
|
83
|
+
hit.addEventListener("blur", function () {
|
|
84
|
+
requestAnimationFrame(function () {
|
|
85
|
+
if (!pinnedOrbitKey && !orbit.matches(":focus-within")) resetOrbit();
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
hit.addEventListener("click", function (event) {
|
|
89
|
+
event.stopPropagation();
|
|
90
|
+
var key = hit.getAttribute("data-orbit-key") || "";
|
|
91
|
+
if (pinnedOrbitKey === key) resetOrbit();
|
|
92
|
+
else showOrbit(hit, true);
|
|
93
|
+
});
|
|
94
|
+
hit.addEventListener("keydown", function (event) {
|
|
95
|
+
if (event.key === "Escape") {
|
|
96
|
+
event.preventDefault();
|
|
97
|
+
resetOrbit();
|
|
98
|
+
} else if (event.key === "Enter" || event.key === " ") {
|
|
99
|
+
event.preventDefault();
|
|
100
|
+
hit.click();
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
document.addEventListener("pointerdown", function (event) {
|
|
105
|
+
if (pinnedOrbitKey && !orbit.contains(event.target)) resetOrbit();
|
|
106
|
+
});
|
|
23
107
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
108
|
+
|
|
109
|
+
var spectrum = document.querySelector(".pnl-spectrum");
|
|
110
|
+
if (spectrum) {
|
|
111
|
+
var spectrumOutput = spectrum.querySelector(".spectrum-readout");
|
|
112
|
+
var spectrumRange = spectrumOutput ? spectrumOutput.querySelector("strong") : null;
|
|
113
|
+
var spectrumDetail = spectrumOutput ? spectrumOutput.querySelector("span") : null;
|
|
114
|
+
var spectrumBins = Array.from(spectrum.querySelectorAll(".spectrum-bin"));
|
|
115
|
+
var pinnedSpectrumBin = null;
|
|
116
|
+
|
|
117
|
+
function resetSpectrum() {
|
|
118
|
+
pinnedSpectrumBin = null;
|
|
119
|
+
spectrum.classList.remove("is-exploring");
|
|
120
|
+
spectrumBins.forEach(function (bin) {
|
|
121
|
+
bin.classList.remove("is-active");
|
|
122
|
+
bin.setAttribute("aria-pressed", "false");
|
|
123
|
+
});
|
|
124
|
+
if (spectrumOutput) spectrumOutput.setAttribute("hidden", "");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function showSpectrum(bin, pin) {
|
|
128
|
+
if (pin) pinnedSpectrumBin = bin;
|
|
129
|
+
spectrum.classList.add("is-exploring");
|
|
130
|
+
spectrumBins.forEach(function (item) {
|
|
131
|
+
item.classList.toggle("is-active", item === bin);
|
|
132
|
+
item.setAttribute("aria-pressed", pin && item === bin ? "true" : "false");
|
|
133
|
+
});
|
|
134
|
+
if (spectrumRange) spectrumRange.textContent = bin.getAttribute("data-spectrum-range") || "";
|
|
135
|
+
if (spectrumDetail) spectrumDetail.textContent = bin.getAttribute("data-spectrum-detail") || "";
|
|
136
|
+
if (spectrumOutput) {
|
|
137
|
+
spectrumOutput.removeAttribute("hidden");
|
|
138
|
+
var bounds = spectrum.getBoundingClientRect();
|
|
139
|
+
var binBounds = bin.getBoundingClientRect();
|
|
140
|
+
var half = (spectrumOutput.offsetWidth || 120) / 2;
|
|
141
|
+
var center = binBounds.left + binBounds.width / 2 - bounds.left;
|
|
142
|
+
spectrumOutput.style.setProperty("--spectrum-x", Math.max(half + 4, Math.min(bounds.width - half - 4, center)) + "px");
|
|
143
|
+
if (!matchMedia("(prefers-reduced-motion: reduce)").matches && spectrumOutput.animate) {
|
|
144
|
+
spectrumOutput.animate([{ opacity: 0.45 }, { opacity: 1 }], { duration: 160, easing: "cubic-bezier(0.25, 1, 0.5, 1)" });
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
spectrumBins.forEach(function (bin) {
|
|
150
|
+
bin.addEventListener("pointerenter", function () {
|
|
151
|
+
if (!pinnedSpectrumBin) showSpectrum(bin, false);
|
|
152
|
+
});
|
|
153
|
+
bin.addEventListener("pointerleave", function () {
|
|
154
|
+
if (!pinnedSpectrumBin && !spectrum.matches(":focus-within")) resetSpectrum();
|
|
155
|
+
});
|
|
156
|
+
bin.addEventListener("focus", function () { showSpectrum(bin, pinnedSpectrumBin === bin); });
|
|
157
|
+
bin.addEventListener("blur", function () {
|
|
158
|
+
requestAnimationFrame(function () {
|
|
159
|
+
if (!pinnedSpectrumBin && !spectrum.matches(":focus-within")) resetSpectrum();
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
bin.addEventListener("click", function (event) {
|
|
163
|
+
event.stopPropagation();
|
|
164
|
+
if (pinnedSpectrumBin === bin) resetSpectrum();
|
|
165
|
+
else showSpectrum(bin, true);
|
|
166
|
+
});
|
|
167
|
+
bin.addEventListener("keydown", function (event) {
|
|
168
|
+
if (event.key === "Escape") {
|
|
169
|
+
event.preventDefault();
|
|
170
|
+
resetSpectrum();
|
|
171
|
+
} else if (event.key === "Enter" || event.key === " ") {
|
|
172
|
+
event.preventDefault();
|
|
173
|
+
bin.click();
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
document.addEventListener("pointerdown", function (event) {
|
|
178
|
+
if (pinnedSpectrumBin && !spectrum.contains(event.target)) resetSpectrum();
|
|
179
|
+
});
|
|
41
180
|
}
|
|
42
|
-
var span = maximum - minimum || 1;
|
|
43
181
|
|
|
44
182
|
function fmt(value, digits) {
|
|
45
183
|
return value.toLocaleString("en-US", { minimumFractionDigits: digits, maximumFractionDigits: digits });
|
|
@@ -49,98 +187,580 @@ export const CHART_SCRIPT = String.raw `
|
|
|
49
187
|
return new Date(ms).toISOString().slice(0, 16).replace("T", " ");
|
|
50
188
|
}
|
|
51
189
|
|
|
52
|
-
function
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
var
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
marker.setAttribute("cx", vx);
|
|
65
|
-
marker.setAttribute("cy", vy);
|
|
66
|
-
marker.removeAttribute("hidden");
|
|
67
|
-
if (ddCrosshair) {
|
|
68
|
-
ddCrosshair.setAttribute("x1", vx);
|
|
69
|
-
ddCrosshair.setAttribute("x2", vx);
|
|
70
|
-
ddCrosshair.removeAttribute("hidden");
|
|
190
|
+
function tone(element, value) {
|
|
191
|
+
if (!element) return;
|
|
192
|
+
element.classList.remove("up", "down", "flat");
|
|
193
|
+
element.classList.add(value > 0 ? "up" : value < 0 ? "down" : "flat");
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (chart) {
|
|
197
|
+
var series;
|
|
198
|
+
try {
|
|
199
|
+
series = JSON.parse(chart.getAttribute("data-series") || "");
|
|
200
|
+
} catch (error) {
|
|
201
|
+
series = null;
|
|
71
202
|
}
|
|
72
203
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
204
|
+
if (series && series.t && series.t.length >= 2) {
|
|
205
|
+
var svg = chart.querySelector("svg");
|
|
206
|
+
var ddSvg = drawdownChart ? drawdownChart.querySelector("svg") : null;
|
|
207
|
+
var tooltip = chart.querySelector(".tooltip");
|
|
208
|
+
var crosshair = chart.querySelector(".crosshair");
|
|
209
|
+
var marker = chart.querySelector(".marker");
|
|
210
|
+
var ddCrosshair = drawdownChart ? drawdownChart.querySelector(".crosshair") : null;
|
|
211
|
+
var count = series.t.length;
|
|
212
|
+
var viewWidth = 1000;
|
|
213
|
+
var equityHeight = 300;
|
|
214
|
+
var drawdownHeight = 120;
|
|
215
|
+
var currentStart = 0;
|
|
216
|
+
var currentEnd = count - 1;
|
|
217
|
+
var viewTop = -8;
|
|
218
|
+
var viewHeight = equityHeight + 16;
|
|
219
|
+
var ddViewHeight = drawdownHeight;
|
|
220
|
+
var minimum = Infinity;
|
|
221
|
+
var maximum = -Infinity;
|
|
222
|
+
var worst = 0;
|
|
223
|
+
var i;
|
|
89
224
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
225
|
+
for (i = 0; i < count; i += 1) {
|
|
226
|
+
if (series.e[i] < minimum) minimum = series.e[i];
|
|
227
|
+
if (series.e[i] > maximum) maximum = series.e[i];
|
|
228
|
+
if (series.d[i] > worst) worst = series.d[i];
|
|
229
|
+
}
|
|
230
|
+
var span = maximum - minimum || 1;
|
|
231
|
+
worst = worst || 1;
|
|
232
|
+
var timeline = series.timeline;
|
|
233
|
+
if (timeline && Array.isArray(timeline.d) && Array.isArray(timeline.v) && timeline.d.length === timeline.v.length && timeline.q > 0) {
|
|
234
|
+
timeline.i = [];
|
|
235
|
+
timeline.e = [];
|
|
236
|
+
var restoredIndex = 0;
|
|
237
|
+
var restoredEquity = 0;
|
|
238
|
+
timeline.d.forEach(function (delta, index) {
|
|
239
|
+
restoredIndex += delta;
|
|
240
|
+
restoredEquity += timeline.v[index];
|
|
241
|
+
timeline.i.push(restoredIndex);
|
|
242
|
+
timeline.e.push(restoredEquity / timeline.q);
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
if (!timeline || !Array.isArray(timeline.i) || !Array.isArray(timeline.e) || timeline.i.length !== timeline.e.length || timeline.i.length === 0 || timeline.s <= 0 || timeline.n < 2) timeline = null;
|
|
246
|
+
var timelineDrawdowns = [];
|
|
247
|
+
if (timeline) {
|
|
248
|
+
var timelinePeak = series.initial;
|
|
249
|
+
timeline.e.forEach(function (equity) {
|
|
250
|
+
timelinePeak = Math.max(timelinePeak, equity);
|
|
251
|
+
timelineDrawdowns.push(timelinePeak > 0 ? ((timelinePeak - equity) / timelinePeak) * 100 : 0);
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
var hoverCount = timeline ? timeline.n : count;
|
|
255
|
+
|
|
256
|
+
var firstTime = series.t[0];
|
|
257
|
+
var totalTime = Math.max(1, series.t[count - 1] - firstTime);
|
|
258
|
+
var minimumTimeGap = totalTime;
|
|
259
|
+
for (i = 1; i < count; i += 1) {
|
|
260
|
+
var gap = series.t[i] - series.t[i - 1];
|
|
261
|
+
if (gap > 0) minimumTimeGap = Math.min(minimumTimeGap, gap);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function xForTime(timeMs) {
|
|
265
|
+
return ((timeMs - firstTime) / totalTime) * viewWidth;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function xFor(index) {
|
|
269
|
+
return xForTime(series.t[index]);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function yFor(equity) {
|
|
273
|
+
return equityHeight - ((equity - minimum) / span) * equityHeight;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function renderVisiblePaths(start, end) {
|
|
277
|
+
var hoverStart = hoverIndexAtTime(series.t[start]);
|
|
278
|
+
var hoverEnd = hoverIndexAtTime(series.t[end]);
|
|
279
|
+
var pixelWidth = Math.max(240, svg.getBoundingClientRect().width);
|
|
280
|
+
var targetPoints = Math.max(120, Math.min(1200, Math.floor(pixelWidth / 2)));
|
|
281
|
+
var indexStep = Math.max(1, Math.ceil((hoverEnd - hoverStart) / targetPoints));
|
|
282
|
+
chart.setAttribute("data-render-step", String(indexStep));
|
|
283
|
+
chart.setAttribute("data-render-points", String(Math.ceil((hoverEnd - hoverStart) / indexStep) + 1));
|
|
284
|
+
var indexes = [hoverStart];
|
|
285
|
+
for (var index = hoverStart + indexStep; index < hoverEnd; index += indexStep) indexes.push(index);
|
|
286
|
+
if (indexes[indexes.length - 1] !== hoverEnd) indexes.push(hoverEnd);
|
|
287
|
+
|
|
288
|
+
var equityPath = "";
|
|
289
|
+
var drawdownPath = "";
|
|
290
|
+
indexes.forEach(function (hoverIndex, pathIndex) {
|
|
291
|
+
var point = hoverPointAt(hoverIndex);
|
|
292
|
+
var command = pathIndex === 0 ? "M" : "L";
|
|
293
|
+
var x = xForTime(hoverTimeAt(hoverIndex)).toFixed(4);
|
|
294
|
+
equityPath += command + x + "," + yFor(point.equity).toFixed(2);
|
|
295
|
+
drawdownPath += command + x + "," + ((point.drawdown / worst) * drawdownHeight).toFixed(2);
|
|
296
|
+
});
|
|
297
|
+
var firstX = xForTime(hoverTimeAt(hoverStart)).toFixed(4);
|
|
298
|
+
var lastX = xForTime(hoverTimeAt(hoverEnd)).toFixed(4);
|
|
299
|
+
var equityLine = chart.querySelector(".equity-line");
|
|
300
|
+
var equityArea = chart.querySelector(".equity-area");
|
|
301
|
+
var drawdownLine = drawdownChart ? drawdownChart.querySelector(".drawdown-line") : null;
|
|
302
|
+
var drawdownArea = drawdownChart ? drawdownChart.querySelector(".drawdown-area") : null;
|
|
303
|
+
if (equityLine) equityLine.setAttribute("d", equityPath);
|
|
304
|
+
if (equityArea) equityArea.setAttribute("d", equityPath + "L" + lastX + "," + equityHeight + "L" + firstX + "," + equityHeight + "Z");
|
|
305
|
+
if (drawdownLine) drawdownLine.setAttribute("d", drawdownPath);
|
|
306
|
+
if (drawdownArea) drawdownArea.setAttribute("d", drawdownPath + "L" + lastX + ",0L" + firstX + ",0Z");
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function findIndex(timeMs) {
|
|
310
|
+
var low = 0;
|
|
311
|
+
var high = count - 1;
|
|
312
|
+
while (low < high) {
|
|
313
|
+
var middle = Math.floor((low + high) / 2);
|
|
314
|
+
if (series.t[middle] < timeMs) low = middle + 1;
|
|
315
|
+
else high = middle;
|
|
316
|
+
}
|
|
317
|
+
if (low > 0 && Math.abs(series.t[low - 1] - timeMs) < Math.abs(series.t[low] - timeMs)) return low - 1;
|
|
318
|
+
return low;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function hoverIndexAtTime(timeMs) {
|
|
322
|
+
if (!timeline) return findIndex(timeMs);
|
|
323
|
+
return Math.max(0, Math.min(hoverCount - 1, Math.round((timeMs - timeline.t) / timeline.s)));
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function hoverTimeAt(index) {
|
|
327
|
+
return timeline ? timeline.t + timeline.s * index : series.t[index];
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function hoverPointAt(index) {
|
|
331
|
+
if (!timeline) return { equity: series.e[index], drawdown: series.d[index] };
|
|
332
|
+
var low = 0;
|
|
333
|
+
var high = timeline.i.length - 1;
|
|
334
|
+
while (low < high) {
|
|
335
|
+
var middle = Math.ceil((low + high) / 2);
|
|
336
|
+
if (timeline.i[middle] <= index) low = middle;
|
|
337
|
+
else high = middle - 1;
|
|
338
|
+
}
|
|
339
|
+
return { equity: timeline.e[low], drawdown: timelineDrawdowns[low] };
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function setWindowReadouts(start, end) {
|
|
343
|
+
var startEquity = series.e[start];
|
|
344
|
+
var endEquity = series.e[end];
|
|
345
|
+
var change = startEquity > 0 ? (endEquity / startEquity - 1) * 100 : 0;
|
|
346
|
+
var peak = startEquity;
|
|
347
|
+
var maxDrawdown = 0;
|
|
348
|
+
for (var index = start; index <= end; index += 1) {
|
|
349
|
+
peak = Math.max(peak, series.e[index]);
|
|
350
|
+
if (peak > 0) maxDrawdown = Math.max(maxDrawdown, ((peak - series.e[index]) / peak) * 100);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
var period = document.getElementById("window-period");
|
|
354
|
+
var returned = document.getElementById("window-return");
|
|
355
|
+
var drawdown = document.getElementById("window-drawdown");
|
|
356
|
+
var equity = document.getElementById("window-equity");
|
|
357
|
+
if (period) period.textContent = isoMinute(series.t[start]) + " — " + isoMinute(series.t[end]);
|
|
358
|
+
if (returned) {
|
|
359
|
+
returned.textContent = (change > 0 ? "+" : "") + fmt(change, 2) + "%";
|
|
360
|
+
tone(returned, change);
|
|
361
|
+
}
|
|
362
|
+
if (drawdown) drawdown.textContent = fmt(maxDrawdown, 2) + "%";
|
|
363
|
+
if (equity) equity.textContent = fmt(endEquity, 2) + " USDT";
|
|
364
|
+
|
|
365
|
+
var range = document.querySelector(".chart-range");
|
|
366
|
+
if (range) range.textContent = isoMinute(series.t[start]) + " → " + isoMinute(series.t[end]) + " UTC · " + (end - start + 1);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function updateEventChips(start, end) {
|
|
370
|
+
var chips = chart.querySelectorAll("[data-point-index]");
|
|
371
|
+
chips.forEach(function (chip) {
|
|
372
|
+
var index = Number(chip.getAttribute("data-point-index"));
|
|
373
|
+
if (index < start || index > end) {
|
|
374
|
+
chip.setAttribute("hidden", "");
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
chip.removeAttribute("hidden");
|
|
378
|
+
chip.style.setProperty("--event-x", (((xFor(index) - xFor(start)) / Math.max(1, xFor(end) - xFor(start))) * 100).toFixed(2) + "%");
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function sizePointMarkers() {
|
|
383
|
+
var bounds = svg.getBoundingClientRect();
|
|
384
|
+
if (bounds.width <= 0 || bounds.height <= 0) return;
|
|
385
|
+
var box = (svg.getAttribute("viewBox") || "0 -8 1000 316").split(/\s+/).map(Number);
|
|
386
|
+
var visibleWidth = Number.isFinite(box[2]) ? box[2] : viewWidth;
|
|
387
|
+
var visibleHeight = Number.isFinite(box[3]) ? box[3] : viewHeight;
|
|
388
|
+
chart.querySelectorAll("[data-marker-radius]").forEach(function (point) {
|
|
389
|
+
var radius = Number(point.getAttribute("data-marker-radius")) || 4;
|
|
390
|
+
point.setAttribute("rx", Math.max(0.001, (radius * visibleWidth) / bounds.width));
|
|
391
|
+
point.setAttribute("ry", Math.max(0.001, (radius * visibleHeight) / bounds.height));
|
|
392
|
+
});
|
|
393
|
+
}
|
|
96
394
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
395
|
+
function animateViewBox(element, target, immediate) {
|
|
396
|
+
if (!element) return;
|
|
397
|
+
var reduced = matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
398
|
+
if (immediate) {
|
|
399
|
+
if (element.__viewBoxFrame) cancelAnimationFrame(element.__viewBoxFrame);
|
|
400
|
+
element.__viewBoxFrame = 0;
|
|
401
|
+
element.setAttribute("viewBox", target.join(" "));
|
|
402
|
+
if (element === svg) sizePointMarkers();
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
var from = (element.getAttribute("viewBox") || target.join(" ")).split(/\s+/).map(Number);
|
|
406
|
+
if (reduced || from.length !== 4 || from.some(function (value) { return !Number.isFinite(value); })) {
|
|
407
|
+
element.setAttribute("viewBox", target.join(" "));
|
|
408
|
+
if (element === svg) sizePointMarkers();
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
if (element.__viewBoxFrame) cancelAnimationFrame(element.__viewBoxFrame);
|
|
412
|
+
var started = performance.now();
|
|
413
|
+
function frameViewBox(now) {
|
|
414
|
+
var progress = Math.min(1, (now - started) / 320);
|
|
415
|
+
var eased = 1 - Math.pow(1 - progress, 4);
|
|
416
|
+
var values = from.map(function (value, index) { return value + (target[index] - value) * eased; });
|
|
417
|
+
element.setAttribute("viewBox", values.join(" "));
|
|
418
|
+
if (element === svg) sizePointMarkers();
|
|
419
|
+
if (progress < 1) element.__viewBoxFrame = requestAnimationFrame(frameViewBox);
|
|
420
|
+
else element.__viewBoxFrame = 0;
|
|
421
|
+
}
|
|
422
|
+
element.__viewBoxFrame = requestAnimationFrame(frameViewBox);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function setRange(start, end, source, immediate) {
|
|
426
|
+
currentStart = Math.max(0, Math.min(count - 2, start));
|
|
427
|
+
currentEnd = Math.max(currentStart + 1, Math.min(count - 1, end));
|
|
428
|
+
var xStart = xFor(currentStart);
|
|
429
|
+
var xEnd = xFor(currentEnd);
|
|
430
|
+
renderVisiblePaths(currentStart, currentEnd);
|
|
431
|
+
var localMinimum = Infinity;
|
|
432
|
+
var localMaximum = -Infinity;
|
|
433
|
+
var localDrawdown = 0;
|
|
434
|
+
for (var index = currentStart; index <= currentEnd; index += 1) {
|
|
435
|
+
localMinimum = Math.min(localMinimum, series.e[index]);
|
|
436
|
+
localMaximum = Math.max(localMaximum, series.e[index]);
|
|
437
|
+
localDrawdown = Math.max(localDrawdown, series.d[index]);
|
|
438
|
+
}
|
|
439
|
+
var localCenter = (localMaximum + localMinimum) / 2;
|
|
440
|
+
var observedValueRange = Math.max(0, localMaximum - localMinimum);
|
|
441
|
+
var minimumValueRange = Math.max(1, Math.abs(series.initial) * 0.04);
|
|
442
|
+
var visibleValueRange = Math.max(minimumValueRange, observedValueRange * 1.3);
|
|
443
|
+
var visibleMaximum = localCenter + visibleValueRange / 2;
|
|
444
|
+
var visibleMinimum = localCenter - visibleValueRange / 2;
|
|
445
|
+
viewTop = yFor(visibleMaximum);
|
|
446
|
+
viewHeight = Math.max(1, yFor(visibleMinimum) - viewTop);
|
|
447
|
+
var visibleDrawdown = Math.max(1, localDrawdown * 1.2);
|
|
448
|
+
ddViewHeight = Math.max(12, (visibleDrawdown / worst) * drawdownHeight);
|
|
449
|
+
var visibleWidth = Math.max(1, xEnd - xStart);
|
|
450
|
+
|
|
451
|
+
animateViewBox(svg, [xStart, viewTop, visibleWidth, viewHeight], immediate);
|
|
452
|
+
animateViewBox(ddSvg, [xStart, 0, visibleWidth, ddViewHeight], immediate);
|
|
453
|
+
var axis = chart.querySelectorAll(".axis span");
|
|
454
|
+
if (axis[0]) axis[0].textContent = fmt(visibleMaximum, 0);
|
|
455
|
+
if (axis[1]) axis[1].textContent = fmt(visibleMinimum, 0);
|
|
456
|
+
var ddAxis = drawdownChart ? drawdownChart.querySelectorAll(".axis span") : [];
|
|
457
|
+
if (ddAxis[1]) ddAxis[1].textContent = "-" + fmt(visibleDrawdown, 1) + "%";
|
|
458
|
+
|
|
459
|
+
document.querySelectorAll("[data-range-days]").forEach(function (button) {
|
|
460
|
+
button.setAttribute("aria-pressed", button === source ? "true" : "false");
|
|
461
|
+
});
|
|
462
|
+
document.querySelectorAll("[data-month-start]").forEach(function (button) {
|
|
463
|
+
button.setAttribute("aria-pressed", button === source ? "true" : "false");
|
|
464
|
+
});
|
|
465
|
+
updateEventChips(currentStart, currentEnd);
|
|
466
|
+
setWindowReadouts(currentStart, currentEnd);
|
|
467
|
+
hide();
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function clientXForTime(timeMs, bounds) {
|
|
471
|
+
var ratio = (xForTime(timeMs) - xFor(currentStart)) / Math.max(1, xFor(currentEnd) - xFor(currentStart));
|
|
472
|
+
return bounds.left + ratio * bounds.width;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
function show(index, clientX) {
|
|
477
|
+
var hoverStart = hoverIndexAtTime(series.t[currentStart]);
|
|
478
|
+
var hoverEnd = hoverIndexAtTime(series.t[currentEnd]);
|
|
479
|
+
index = Math.max(hoverStart, Math.min(hoverEnd, index));
|
|
480
|
+
var point = hoverPointAt(index);
|
|
481
|
+
var equity = point.equity;
|
|
482
|
+
var drawdown = point.drawdown;
|
|
483
|
+
var timeMs = hoverTimeAt(index);
|
|
484
|
+
var change = series.initial > 0 ? (equity / series.initial - 1) * 100 : 0;
|
|
485
|
+
var vx = xForTime(timeMs);
|
|
486
|
+
var vy = yFor(equity);
|
|
487
|
+
|
|
488
|
+
crosshair.setAttribute("x1", vx);
|
|
489
|
+
crosshair.setAttribute("x2", vx);
|
|
490
|
+
crosshair.setAttribute("y1", viewTop);
|
|
491
|
+
crosshair.setAttribute("y2", viewTop + viewHeight);
|
|
492
|
+
crosshair.removeAttribute("hidden");
|
|
493
|
+
marker.setAttribute("cx", vx);
|
|
494
|
+
marker.setAttribute("cy", vy);
|
|
495
|
+
marker.removeAttribute("hidden");
|
|
496
|
+
if (ddCrosshair) {
|
|
497
|
+
ddCrosshair.setAttribute("x1", vx);
|
|
498
|
+
ddCrosshair.setAttribute("x2", vx);
|
|
499
|
+
ddCrosshair.setAttribute("y2", ddViewHeight);
|
|
500
|
+
ddCrosshair.removeAttribute("hidden");
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
var changeSign = change > 0 ? "+" : "";
|
|
504
|
+
tooltip.innerHTML =
|
|
505
|
+
'<div class="row"><span>' + series.labels.time + '</span><b>' + isoMinute(timeMs) + "</b></div>" +
|
|
506
|
+
'<div class="row"><span>' + series.labels.equity + '</span><b>' + fmt(equity, 2) + "</b></div>" +
|
|
507
|
+
'<div class="row"><span>' + series.labels.change + '</span><b class="' + (change >= 0 ? "up" : "down") + '">' + changeSign + fmt(change, 2) + "%</b></div>" +
|
|
508
|
+
'<div class="row"><span>' + series.labels.drawdown + '</span><b class="' + (drawdown > 0 ? "down" : "") + '">-' + fmt(drawdown, 2) + "%</b></div>";
|
|
509
|
+
tooltip.removeAttribute("hidden");
|
|
510
|
+
|
|
511
|
+
var bounds = chart.getBoundingClientRect();
|
|
512
|
+
var offset = clientX - bounds.left;
|
|
513
|
+
var tipWidth = tooltip.offsetWidth || 190;
|
|
514
|
+
var left = offset + 16;
|
|
515
|
+
if (left + tipWidth > bounds.width) left = offset - tipWidth - 16;
|
|
516
|
+
tooltip.style.left = Math.max(6, left) + "px";
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function hide() {
|
|
520
|
+
tooltip.setAttribute("hidden", "");
|
|
521
|
+
crosshair.setAttribute("hidden", "");
|
|
522
|
+
marker.setAttribute("hidden", "");
|
|
523
|
+
if (ddCrosshair) ddCrosshair.setAttribute("hidden", "");
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function indexFromClientX(clientX) {
|
|
527
|
+
var bounds = svg.getBoundingClientRect();
|
|
528
|
+
if (bounds.width <= 0) return currentStart;
|
|
529
|
+
var edgeSnap = Math.min(8, bounds.width / 4);
|
|
530
|
+
var ratio = (clientX + 0.5 - bounds.left - edgeSnap) / Math.max(1, bounds.width - edgeSnap * 2);
|
|
531
|
+
ratio = Math.max(0, Math.min(1, ratio));
|
|
532
|
+
var targetTime = series.t[currentStart] + ratio * (series.t[currentEnd] - series.t[currentStart]);
|
|
533
|
+
var hoverStart = hoverIndexAtTime(series.t[currentStart]);
|
|
534
|
+
var hoverEnd = hoverIndexAtTime(series.t[currentEnd]);
|
|
535
|
+
return Math.max(hoverStart, Math.min(hoverEnd, hoverIndexAtTime(targetTime)));
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
var dragState = null;
|
|
539
|
+
|
|
540
|
+
function boundedTimeWindow(startTime, duration) {
|
|
541
|
+
duration = Math.max(minimumTimeGap * Math.min(8, count - 1), Math.min(totalTime, duration));
|
|
542
|
+
if (startTime < firstTime) startTime = firstTime;
|
|
543
|
+
if (startTime + duration > firstTime + totalTime) startTime = firstTime + totalTime - duration;
|
|
544
|
+
return [startTime, startTime + duration];
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function setTimeWindow(startTime, endTime, immediate) {
|
|
548
|
+
var start = findIndex(startTime);
|
|
549
|
+
var end = findIndex(endTime);
|
|
550
|
+
if (end <= start) end = Math.min(count - 1, start + 1);
|
|
551
|
+
if (end <= start) start = Math.max(0, end - 1);
|
|
552
|
+
setRange(start, end, null, immediate);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function zoomWindow(clientX, factor) {
|
|
556
|
+
var currentTimeStart = series.t[currentStart];
|
|
557
|
+
var currentDuration = series.t[currentEnd] - currentTimeStart;
|
|
558
|
+
var targetDuration = Math.max(minimumTimeGap * Math.min(8, count - 1), Math.min(totalTime, currentDuration * factor));
|
|
559
|
+
if (targetDuration === currentDuration) return;
|
|
560
|
+
var bounds = svg.getBoundingClientRect();
|
|
561
|
+
var ratio = Math.max(0, Math.min(1, (clientX - bounds.left) / Math.max(1, bounds.width)));
|
|
562
|
+
var anchor = currentTimeStart + ratio * currentDuration;
|
|
563
|
+
var window = boundedTimeWindow(anchor - ratio * targetDuration, targetDuration);
|
|
564
|
+
setTimeWindow(window[0], window[1], false);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function endPan(event) {
|
|
568
|
+
if (!dragState || (typeof event.pointerId === "number" && event.pointerId !== dragState.pointerId)) return;
|
|
569
|
+
var surface = dragState.surface;
|
|
570
|
+
var pointerId = dragState.pointerId;
|
|
571
|
+
dragState = null;
|
|
572
|
+
surface.classList.remove("is-dragging");
|
|
573
|
+
if (surface.hasPointerCapture && surface.hasPointerCapture(pointerId)) surface.releasePointerCapture(pointerId);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
var pendingWheelDelta = 0;
|
|
577
|
+
var pendingWheelX = 0;
|
|
578
|
+
var wheelFrame = 0;
|
|
579
|
+
|
|
580
|
+
function queueWheelZoom(event) {
|
|
581
|
+
if (!event.deltaY) return;
|
|
582
|
+
event.preventDefault();
|
|
583
|
+
var delta = event.deltaY;
|
|
584
|
+
if (event.deltaMode === 1) delta *= 16;
|
|
585
|
+
else if (event.deltaMode === 2) delta *= Math.max(1, svg.getBoundingClientRect().height);
|
|
586
|
+
pendingWheelDelta += delta;
|
|
587
|
+
pendingWheelX = event.clientX;
|
|
588
|
+
if (wheelFrame) return;
|
|
589
|
+
wheelFrame = requestAnimationFrame(function () {
|
|
590
|
+
var boundedDelta = Math.max(-80, Math.min(80, pendingWheelDelta));
|
|
591
|
+
pendingWheelDelta = 0;
|
|
592
|
+
wheelFrame = 0;
|
|
593
|
+
zoomWindow(pendingWheelX, Math.exp(boundedDelta * 0.0015));
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function bindRangeNavigation(surface) {
|
|
598
|
+
if (!surface) return;
|
|
599
|
+
surface.addEventListener("wheel", queueWheelZoom, { passive: false });
|
|
600
|
+
surface.addEventListener("pointerdown", function (event) {
|
|
601
|
+
if (event.pointerType !== "mouse" || event.button !== 0) return;
|
|
602
|
+
if (event.target.closest && event.target.closest("button")) return;
|
|
603
|
+
dragState = {
|
|
604
|
+
surface: surface,
|
|
605
|
+
pointerId: event.pointerId,
|
|
606
|
+
clientX: event.clientX,
|
|
607
|
+
startTime: series.t[currentStart],
|
|
608
|
+
endTime: series.t[currentEnd]
|
|
609
|
+
};
|
|
610
|
+
surface.classList.add("is-dragging");
|
|
611
|
+
surface.setPointerCapture(event.pointerId);
|
|
612
|
+
hide();
|
|
613
|
+
event.preventDefault();
|
|
614
|
+
});
|
|
615
|
+
surface.addEventListener("pointermove", function (event) {
|
|
616
|
+
if (!dragState || dragState.surface !== surface || event.pointerId !== dragState.pointerId) return;
|
|
617
|
+
var bounds = svg.getBoundingClientRect();
|
|
618
|
+
var duration = dragState.endTime - dragState.startTime;
|
|
619
|
+
var shift = ((dragState.clientX - event.clientX) / Math.max(1, bounds.width)) * duration;
|
|
620
|
+
var window = boundedTimeWindow(dragState.startTime + shift, duration);
|
|
621
|
+
setTimeWindow(window[0], window[1], true);
|
|
622
|
+
event.preventDefault();
|
|
623
|
+
});
|
|
624
|
+
surface.addEventListener("pointerup", endPan);
|
|
625
|
+
surface.addEventListener("pointercancel", endPan);
|
|
626
|
+
surface.addEventListener("dblclick", function (event) {
|
|
627
|
+
if (event.target.closest && event.target.closest("button")) return;
|
|
628
|
+
setRange(0, count - 1, document.querySelector('[data-range-days="all"]'), false);
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
bindRangeNavigation(chart);
|
|
633
|
+
bindRangeNavigation(drawdownChart);
|
|
634
|
+
addEventListener("resize", function () {
|
|
635
|
+
sizePointMarkers();
|
|
636
|
+
renderVisiblePaths(currentStart, currentEnd);
|
|
637
|
+
}, { passive: true });
|
|
638
|
+
|
|
639
|
+
function track(event) {
|
|
640
|
+
if (dragState) return;
|
|
641
|
+
var clientX = event.touches && event.touches.length ? event.touches[0].clientX : event.clientX;
|
|
642
|
+
show(indexFromClientX(clientX), clientX);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
chart.addEventListener("mousemove", track);
|
|
646
|
+
chart.addEventListener("mouseleave", hide);
|
|
647
|
+
if (drawdownChart) {
|
|
648
|
+
drawdownChart.addEventListener("mousemove", track);
|
|
649
|
+
drawdownChart.addEventListener("mouseleave", hide);
|
|
650
|
+
}
|
|
651
|
+
chart.addEventListener("touchstart", track, { passive: true });
|
|
652
|
+
chart.addEventListener("touchmove", track, { passive: true });
|
|
653
|
+
chart.addEventListener("touchend", hide);
|
|
654
|
+
chart.setAttribute("tabindex", "0");
|
|
655
|
+
|
|
656
|
+
var cursor = -1;
|
|
657
|
+
chart.addEventListener("keydown", function (event) {
|
|
658
|
+
var hoverStart = hoverIndexAtTime(series.t[currentStart]);
|
|
659
|
+
var hoverEnd = hoverIndexAtTime(series.t[currentEnd]);
|
|
660
|
+
var step = event.shiftKey ? Math.max(1, Math.round((hoverEnd - hoverStart + 1) / 20)) : 1;
|
|
661
|
+
if (event.key === "ArrowRight") cursor = Math.min(hoverEnd, (cursor < hoverStart ? hoverStart - 1 : cursor) + step);
|
|
662
|
+
else if (event.key === "ArrowLeft") cursor = Math.max(hoverStart, (cursor > hoverEnd || cursor < 0 ? hoverEnd + 1 : cursor) - step);
|
|
663
|
+
else if (event.key === "Home") cursor = hoverStart;
|
|
664
|
+
else if (event.key === "End") cursor = hoverEnd;
|
|
665
|
+
else if (event.key === "Escape") {
|
|
666
|
+
cursor = -1;
|
|
667
|
+
hide();
|
|
668
|
+
return;
|
|
669
|
+
} else return;
|
|
670
|
+
event.preventDefault();
|
|
671
|
+
var bounds = svg.getBoundingClientRect();
|
|
672
|
+
show(cursor, clientXForTime(hoverTimeAt(cursor), bounds));
|
|
673
|
+
});
|
|
674
|
+
chart.addEventListener("blur", hide);
|
|
675
|
+
|
|
676
|
+
document.querySelectorAll("[data-range-days]").forEach(function (button) {
|
|
677
|
+
button.addEventListener("click", function () {
|
|
678
|
+
var value = button.getAttribute("data-range-days");
|
|
679
|
+
var start = 0;
|
|
680
|
+
if (value !== "all") start = findIndex(series.t[count - 1] - Number(value) * 86400000);
|
|
681
|
+
setRange(start, count - 1, button);
|
|
682
|
+
});
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
document.querySelectorAll("[data-month-start]").forEach(function (button) {
|
|
686
|
+
button.addEventListener("click", function () {
|
|
687
|
+
var start = findIndex(Number(button.getAttribute("data-month-start")));
|
|
688
|
+
var end = findIndex(Number(button.getAttribute("data-month-end")));
|
|
689
|
+
setRange(start, Math.max(start + 1, end), button);
|
|
690
|
+
document.getElementById("observatory")?.scrollIntoView({ behavior: "smooth", block: "start" });
|
|
691
|
+
});
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
chart.querySelectorAll("[data-point-index]").forEach(function (button) {
|
|
695
|
+
button.addEventListener("click", function () {
|
|
696
|
+
var index = Number(button.getAttribute("data-point-index"));
|
|
697
|
+
var timeMs = series.t[index];
|
|
698
|
+
var bounds = svg.getBoundingClientRect();
|
|
699
|
+
show(hoverIndexAtTime(timeMs), clientXForTime(timeMs, bounds));
|
|
700
|
+
});
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
var allRange = document.querySelector('[data-range-days="all"]');
|
|
704
|
+
setRange(0, count - 1, allRange);
|
|
705
|
+
}
|
|
107
706
|
}
|
|
108
707
|
|
|
109
|
-
function
|
|
110
|
-
|
|
111
|
-
|
|
708
|
+
document.querySelectorAll("[data-chart-mode]").forEach(function (button) {
|
|
709
|
+
button.addEventListener("click", function () {
|
|
710
|
+
var mode = button.getAttribute("data-chart-mode") || "overview";
|
|
711
|
+
if (frame) frame.setAttribute("data-mode", mode);
|
|
712
|
+
document.querySelectorAll("[data-chart-mode]").forEach(function (item) {
|
|
713
|
+
item.setAttribute("aria-pressed", item === button ? "true" : "false");
|
|
714
|
+
});
|
|
715
|
+
});
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
var tradeRows = Array.from(document.querySelectorAll(".tape-row"));
|
|
719
|
+
var tradeCount = document.getElementById("trade-visible-count");
|
|
720
|
+
document.querySelectorAll("[data-trade-filter]").forEach(function (button) {
|
|
721
|
+
button.addEventListener("click", function () {
|
|
722
|
+
var filter = button.getAttribute("data-trade-filter") || "all";
|
|
723
|
+
var visible = 0;
|
|
724
|
+
tradeRows.forEach(function (row) {
|
|
725
|
+
var matches = filter === "all" || row.getAttribute("data-side") === filter || row.getAttribute("data-outcome") === filter;
|
|
726
|
+
if (matches) {
|
|
727
|
+
row.removeAttribute("hidden");
|
|
728
|
+
visible += 1;
|
|
729
|
+
} else row.setAttribute("hidden", "");
|
|
730
|
+
});
|
|
731
|
+
document.querySelectorAll("[data-trade-filter]").forEach(function (item) {
|
|
732
|
+
item.setAttribute("aria-pressed", item === button ? "true" : "false");
|
|
733
|
+
});
|
|
734
|
+
if (tradeCount) tradeCount.textContent = String(visible);
|
|
735
|
+
});
|
|
736
|
+
});
|
|
737
|
+
|
|
738
|
+
var sectionLinks = Array.from(document.querySelectorAll("[data-section-link]"));
|
|
739
|
+
if ("IntersectionObserver" in window) {
|
|
740
|
+
var observer = new IntersectionObserver(function (entries) {
|
|
741
|
+
entries.forEach(function (entry) {
|
|
742
|
+
if (!entry.isIntersecting) return;
|
|
743
|
+
sectionLinks.forEach(function (link) {
|
|
744
|
+
link.classList.toggle("active", link.getAttribute("data-section-link") === entry.target.id);
|
|
745
|
+
});
|
|
746
|
+
});
|
|
747
|
+
}, { rootMargin: "-20% 0px -65% 0px", threshold: 0 });
|
|
748
|
+
document.querySelectorAll("[data-report-section]").forEach(function (section) { observer.observe(section); });
|
|
112
749
|
}
|
|
113
750
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
751
|
+
var progress = document.querySelector(".scroll-progress");
|
|
752
|
+
var ticking = false;
|
|
753
|
+
function updateProgress() {
|
|
754
|
+
var maximum = Math.max(1, document.documentElement.scrollHeight - innerHeight);
|
|
755
|
+
if (progress) progress.style.transform = "scaleX(" + Math.min(1, scrollY / maximum) + ")";
|
|
756
|
+
ticking = false;
|
|
119
757
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
chart.setAttribute("tabindex", "0");
|
|
127
|
-
var cursor = -1;
|
|
128
|
-
chart.addEventListener("keydown", function (event) {
|
|
129
|
-
var step = event.shiftKey ? Math.max(1, Math.round(count / 20)) : 1;
|
|
130
|
-
if (event.key === "ArrowRight") cursor = Math.min(count - 1, (cursor < 0 ? -1 : cursor) + step);
|
|
131
|
-
else if (event.key === "ArrowLeft") cursor = Math.max(0, (cursor < 0 ? count : cursor) - step);
|
|
132
|
-
else if (event.key === "Home") cursor = 0;
|
|
133
|
-
else if (event.key === "End") cursor = count - 1;
|
|
134
|
-
else if (event.key === "Escape") {
|
|
135
|
-
cursor = -1;
|
|
136
|
-
hide();
|
|
137
|
-
return;
|
|
138
|
-
} else return;
|
|
139
|
-
event.preventDefault();
|
|
140
|
-
var bounds = chart.getBoundingClientRect();
|
|
141
|
-
show(cursor, bounds.left + (cursor / (count - 1)) * bounds.width);
|
|
142
|
-
});
|
|
143
|
-
chart.addEventListener("blur", hide);
|
|
758
|
+
addEventListener("scroll", function () {
|
|
759
|
+
if (ticking) return;
|
|
760
|
+
ticking = true;
|
|
761
|
+
requestAnimationFrame(updateProgress);
|
|
762
|
+
}, { passive: true });
|
|
763
|
+
updateProgress();
|
|
144
764
|
})();
|
|
145
765
|
`;
|
|
146
766
|
//# sourceMappingURL=chart-script.js.map
|