scripts/js/channels_demo.html Repository BOT_EU Original path scripts/js/channels_demo.htmlRole SOURCE Size 142548 bytes Lines 4086 SHA-256 7e528e63dfeb6cc53efe82fd51aed56a548de52e8bcd317f9de4f5f285aab9d3Displayed range 2001–2500 Previous file/page · Project index · Next file/page
col.querySelectorAll("input[data-group-toggle]").forEach((el) => {
el.addEventListener("change", () => {
const checked = !!el.checked;
const keys = [...new Set(items.map((r) => r.key).filter(Boolean))];
keys.forEach((k) => {
if (checked) hiddenLineKeys.delete(k);
else hiddenLineKeys.add(k);
});
draw();
});
});
col.appendChild(colsWrap);
return col;
};
const colA = buildBigCol(`Główny TF ${bigTf}`, mainBigVals.slice(0, 32), "main");
const colB = buildBigCol("Pozostałe interwały", otherBigVals.slice(0, 32), "other");
split.appendChild(colA);
split.appendChild(colB);
wide.appendChild(split);
bigWrap.appendChild(wide);
}
}
function buildFeatureToggles() {
toggleGridEl.innerHTML = "";
FEATURES.forEach((f) => {
const lab = document.createElement("label");
lab.className = "tick";
const input = document.createElement("input");
input.type = "checkbox";
input.checked = !!featureState[f.key];
input.addEventListener("change", () => {
featureState[f.key] = input.checked;
saveFeatureState();
draw();
});
const tx = document.createElement("div");
tx.innerHTML = `<strong>${f.label}</strong><small>${f.desc}</small>`;
lab.appendChild(input);
lab.appendChild(tx);
toggleGridEl.appendChild(lab);
});
}
const view = {
i0: 0,
i1: 0,
yMid: null,
ySpan: null,
dragging: false,
dragX: 0,
dragY: 0,
drag_i0: 0,
drag_i1: 0,
drag_yMid: 0,
};
const Y_MARGIN_PCT = 0.20;
function clamp(x, a, b) { return Math.max(a, Math.min(b, x)); }
function clampInt(x, a, b) { return Math.max(a, Math.min(b, Math.floor(x))); }
function detectPriceDecimals(x) {
const v = Math.abs(Number(x));
if (!Number.isFinite(v)) return 2;
let dec = 2;
if (v < 100) dec = 3;
if (v < 10) dec = 4;
if (v < 1) dec = 5;
if (v < 0.1) dec = 6;
if (v < 0.01) dec = 7;
if (v < 0.001) dec = 8;
if (Number.isFinite(view?.ySpan) && view.ySpan > 0) {
const step = view.ySpan / 6;
if (step > 0) {
const spanDec = Math.ceil(-Math.log10(step)) + 1;
dec = Math.max(dec, clampInt(spanDec, 2, 10));
}
}
return clampInt(dec, 2, 10);
}
function fmt2(x) {
if (!(typeof x === "number" && isFinite(x))) return "—";
const dec = detectPriceDecimals(x);
let s = x.toFixed(dec);
if (dec > 2) {
s = s.replace(/(\.\d*?[1-9])0+$/, "$1").replace(/\.0+$/, "");
}
return s;
}
const SUDDEN_KEYS = [
"momentumScore",
"pressureScore",
"shockScore",
"botSignal",
"cci",
"adx14",
"atrPct",
"emaSpreadPct",
];
const SUDDEN_BASE_THRESHOLDS = {
momentumScore: 0.42,
pressureScore: 0.42,
shockScore: 0.42,
botSignal: 0.55,
cci: 22,
adx14: 2.1,
atrPct: 0.045,
emaSpreadPct: 0.07,
};
function getTf10Pack() {
const tfs = DATA?.tfs || {};
const pack = tfs["10"] || tfs["10m"] || tfs["10M"] || null;
if (!pack || !Array.isArray(pack.regimes) || !pack.regimes.length) return null;
const r = pack.regimes[0] || {};
const upper = Number(r.upper);
const lower = Number(r.lower);
if (!Number.isFinite(upper) || !Number.isFinite(lower)) return null;
return { upper, lower };
}
function percentile(sortedValues, p) {
if (!Array.isArray(sortedValues) || !sortedValues.length) return 0;
const pp = clamp(Number(p), 0, 1);
const pos = (sortedValues.length - 1) * pp;
const i0 = Math.floor(pos);
const i1 = Math.min(sortedValues.length - 1, i0 + 1);
const frac = pos - i0;
return sortedValues[i0] * (1 - frac) + sortedValues[i1] * frac;
}
function getMetricDeltaAbsWindow(rows, idx, key, lookback = 36) {
const out = [];
const start = Math.max(1, idx - lookback);
for (let i = start; i <= idx; i++) {
const v1 = Number(rows[i]?.[key]);
const v0 = Number(rows[i - 1]?.[key]);
if (!Number.isFinite(v1) || !Number.isFinite(v0)) continue;
out.push(Math.abs(v1 - v0));
}
out.sort((a, b) => a - b);
return out;
}
function classifyBreakout10(price, prevPrice, nextPrice, tf10) {
if (!tf10 || !Number.isFinite(price)) return { dir: 0, confirmed: false };
if (price > tf10.upper) {
const confirmed =
(Number.isFinite(prevPrice) && prevPrice > tf10.upper) ||
(Number.isFinite(nextPrice) && nextPrice > tf10.upper);
return { dir: 1, confirmed };
}
if (price < tf10.lower) {
const confirmed =
(Number.isFinite(prevPrice) && prevPrice < tf10.lower) ||
(Number.isFinite(nextPrice) && nextPrice < tf10.lower);
return { dir: -1, confirmed };
}
return { dir: 0, confirmed: false };
}
function buildSuddenFlags() {
const rows = Array.isArray(indicatorStore.rows) ? indicatorStore.rows : [];
if (rows.length < 3) return [];
const tf10 = getTf10Pack();
if (!tf10) return [];
const events = [];
for (let i = 1; i < rows.length - 1; i++) {
const cur = rows[i] || {};
const prev = rows[i - 1] || {};
const next = rows[i + 1] || {};
const price = Number(cur.price);
const prevPrice = Number(prev.price);
const nextPrice = Number(next.price);
const br = classifyBreakout10(price, prevPrice, nextPrice, tf10);
if (!br.confirmed || br.dir === 0) continue;
let count = 0;
for (const key of SUDDEN_KEYS) {
const currVal = Number(cur[key]);
const prevVal = Number(prev[key]);
if (!Number.isFinite(currVal) || !Number.isFinite(prevVal)) continue;
const delta = currVal - prevVal;
if (delta * br.dir <= 0) continue;
const recent = getMetricDeltaAbsWindow(rows, i - 1, key, 36);
const p85 = recent.length >= 8 ? percentile(recent, 0.85) : 0;
const threshold = Math.max(Number(SUDDEN_BASE_THRESHOLDS[key] || 0.3), p85 * 1.15);
if (Math.abs(delta) >= threshold) count += 1;
}
if (count <= 0) continue;
const tsMs = Number(cur.tsMs);
if (!Number.isFinite(tsMs)) continue;
const prevEv = events[events.length - 1];
if (prevEv && Math.abs(tsMs - prevEv.tsMs) <= 2 * 60_000 && prevEv.dir === br.dir) {
if (count > prevEv.count) prevEv.count = count;
continue;
}
events.push({ tsMs, price, dir: br.dir, count });
}
return events;
}
function getSuddenFlagsCached() {
const rows = Array.isArray(indicatorStore.rows) ? indicatorStore.rows : [];
const lastTs = Number(rows.at(-1)?.tsMs || 0);
const tf10 = getTf10Pack();
const key = [
rows.length,
lastTs,
tf10 ? `${tf10.upper}|${tf10.lower}` : "no10",
DATA?.updatedAt || "",
].join("|");
if (key !== suddenFlagsCacheKey) {
suddenFlagsCache = buildSuddenFlags();
suddenFlagsCacheKey = key;
}
return suddenFlagsCache;
}
function drawSuddenFlags(close, times) {
if (!suddenFlagsEnabled) return;
const flags = getSuddenFlagsCached();
if (!flags.length) return;
const n = view.i1 - view.i0;
if (n < 2) return;
g.save();
for (const ev of flags) {
const idx = nearestIndexByTs(times, Number(ev.tsMs));
if (idx < view.i0 || idx >= view.i1) continue;
const p = Number.isFinite(Number(ev.price)) ? Number(ev.price) : Number(close[idx]);
if (!Number.isFinite(p)) continue;
const x = xOf(idx - view.i0, n);
const y = yOf(p);
const up = ev.dir > 0;
const markerColor = up ? "rgba(56, 211, 159, 0.78)" : "rgba(255, 96, 96, 0.78)";
const textColor = up ? "rgba(210, 255, 236, 0.98)" : "rgba(255, 230, 230, 0.98)";
const yOffset = up ? -13 : 13;
g.beginPath();
g.fillStyle = markerColor;
g.arc(x, y + yOffset, 7.5, 0, Math.PI * 2);
g.fill();
g.fillStyle = textColor;
g.font = "700 10px system-ui, Arial";
g.textAlign = "center";
g.textBaseline = "middle";
g.fillText(String(ev.count), x, y + yOffset + 0.5);
}
g.restore();
}
function minPriceSpan(refPrice) {
const p = Math.abs(Number(refPrice));
if (!Number.isFinite(p) || p <= 0) return 0.0001;
return Math.max(p * 0.003, 0.0001); // min ~0.3% ceny
}
function minMax(arr) {
let mn = Infinity, mx = -Infinity;
for (const x of arr) {
if (typeof x !== "number" || !isFinite(x)) continue;
mn = Math.min(mn, x);
mx = Math.max(mx, x);
}
if (!isFinite(mn) || !isFinite(mx)) return { mn: 0, mx: 1 };
if (mn === mx) return { mn: mn * 0.999, mx: mx * 1.001 };
return { mn, mx };
}
function nearestIndexByTs(times, ts) {
if (!Array.isArray(times) || !times.length || !Number.isFinite(ts)) return -1;
let lo = 0;
let hi = times.length - 1;
while (lo < hi) {
const mid = Math.floor((lo + hi) / 2);
if (times[mid] < ts) lo = mid + 1;
else hi = mid;
}
const idx = lo;
if (idx <= 0) return 0;
const prev = idx - 1;
return Math.abs(times[idx] - ts) < Math.abs(times[prev] - ts) ? idx : prev;
}
function fitYToVisibleClose() {
if (!DATA?.plot?.close?.length) return;
const i0 = clamp(view.i0, 0, DATA.plot.close.length - 2);
const i1 = clamp(view.i1, i0 + 2, DATA.plot.close.length);
const slice = DATA.plot.close.slice(i0, i1);
const mm = minMax(slice);
const baseRange = Math.max(mm.mx - mm.mn, 1e-12);
const paddedMin = mm.mn - baseRange * Y_MARGIN_PCT;
const paddedMax = mm.mx + baseRange * Y_MARGIN_PCT;
const mid = (paddedMin + paddedMax) / 2;
const span = Math.max(minPriceSpan(mid), paddedMax - paddedMin);
view.yMid = mid;
view.ySpan = span;
}
function mouseToCanvas(e) {
const rect = cv.getBoundingClientRect();
const sx = cv.width / Math.max(1, rect.width);
const sy = cv.height / Math.max(1, rect.height);
return {
x: (e.clientX - rect.left) * sx,
y: (e.clientY - rect.top) * sy,
};
}
function updateTimelineInfo() {
if (!DATA?.plot?.t?.length) {
timelineFromInfoEl.textContent = "Od —";
timelineInfoEl.textContent = "Do —";
return;
}
const t = DATA.plot.t;
const i0 = clamp(Number(timelineStartEl.value || 0), 0, t.length - 1);
const i1 = clamp(Number(timelineEndEl.value || t.length - 1), 0, t.length - 1);
const d0 = new Date(t[i0]);
const d1 = new Date(t[i1]);
const f = (d) => `${String(d.getDate()).padStart(2,"0")}.${String(d.getMonth()+1).padStart(2,"0")} ${String(d.getHours()).padStart(2,"0")}:${String(d.getMinutes()).padStart(2,"0")}`;
timelineFromInfoEl.textContent = `Od ${f(d0)}`;
timelineInfoEl.textContent = `Do ${f(d1)}`;
}
function syncTimelineFromView() {
if (!DATA?.plot?.t?.length) return;
const max = DATA.plot.t.length - 1;
timelineStartEl.max = String(max);
timelineEndEl.max = String(max);
timelineStartEl.value = String(clamp(view.i0, 0, max));
timelineEndEl.value = String(clamp(view.i1 - 1, 0, max));
updateTimelineInfo();
}
function applyTimelineRange(source) {
if (!DATA?.plot?.t?.length) return;
const max = DATA.plot.t.length - 1;
const minBars = 40;
let s = clamp(Number(timelineStartEl.value || 0), 0, max);
let e = clamp(Number(timelineEndEl.value || max), 0, max);
if (source === "start" && s > e - minBars) s = Math.max(0, e - minBars);
if (source === "end" && e < s + minBars) e = Math.min(max, s + minBars);
if (e <= s) e = Math.min(max, s + 1);
timelineStartEl.value = String(s);
timelineEndEl.value = String(e);
view.i0 = s;
view.i1 = e + 1;
fitYToVisibleClose();
updateTimelineInfo();
draw();
}
const PAD_L = 104, PAD_R = 20, PAD_T = 18, PAD_B = 40;
function xOf(i, n) {
const w = cv.width - PAD_L - PAD_R;
const t = (i / Math.max(1, n - 1));
return PAD_L + t * w;
}
function yOf(p) {
const h = cv.height - PAD_T - PAD_B;
const mn = view.yMid - view.ySpan / 2;
const mx = view.yMid + view.ySpan / 2;
const t = (p - mn) / (mx - mn);
return (cv.height - PAD_B) - t * h;
}
function pOfY(y) {
const h = cv.height - PAD_T - PAD_B;
const mn = view.yMid - view.ySpan / 2;
const mx = view.yMid + view.ySpan / 2;
const t = ((cv.height - PAD_B) - y) / h;
return mn + t * (mx - mn);
}
function zoomPriceByFactor(factor, anchorPrice = null) {
if (!DATA) return;
const pAt = Number.isFinite(anchorPrice) ? anchorPrice : view.yMid;
const minSpan = minPriceSpan(Math.abs(pAt) > 0 ? pAt : view.yMid);
const newSpan = clamp(view.ySpan * factor, minSpan, 1e9);
const k = (pAt - view.yMid) / (view.ySpan / 2);
const newMid = pAt - k * (newSpan / 2);
view.ySpan = newSpan;
view.yMid = newMid;
}
function zoomTimelineByFactor(factor, anchorX = (PAD_L + (cv.width - PAD_R)) / 2) {
if (!DATA?.plot?.t?.length) return;
const total = DATA.plot.t.length;
const n = view.i1 - view.i0;
const tt = clamp((anchorX - PAD_L) / (cv.width - PAD_L - PAD_R), 0, 1);
const center = view.i0 + tt * n;
const minBars = 40;
const newN = clamp(Math.round(n * factor), minBars, total);
let i0 = Math.round(center - tt * newN);
let i1 = i0 + newN;
if (i0 < 0) {
i1 -= i0;
i0 = 0;
}
if (i1 > total) {
const d = i1 - total;
i0 -= d;
i1 -= d;
}
i0 = clamp(i0, 0, total - 2);
i1 = clamp(i1, i0 + 2, total);
view.i0 = i0;
view.i1 = i1;
syncTimelineFromView();
}
function drawGrid() {
g.clearRect(0,0,cv.width,cv.height);
g.fillStyle = "#000000";
g.fillRect(0,0,cv.width,cv.height);
g.strokeStyle = "rgba(255,255,255,0.10)";
g.lineWidth = 1;
const n = view.i1 - view.i0;
const step = Math.max(50, Math.floor(n / 10));
for (let k=0; k<=10; k++) {
const i = view.i0 + k * step;
if (i >= view.i1) break;
const x = xOf(i - view.i0, n);
g.beginPath();
g.moveTo(x, PAD_T);
g.lineTo(x, cv.height - PAD_B);
g.stroke();
}
const ySteps = 6;
for (let k=0; k<=ySteps; k++) {
const y = PAD_T + k * ((cv.height - PAD_T - PAD_B) / ySteps);
g.beginPath();
g.moveTo(PAD_L, y);
g.lineTo(cv.width - PAD_R, y);
g.stroke();
}
}
function drawAxesLabels(times) {
g.fillStyle = "rgba(238,246,255,0.98)";
g.font = "700 14px system-ui, Arial";
g.textAlign = "right";
g.shadowColor = "rgba(0,0,0,0.85)";
g.shadowBlur = 2;
g.shadowOffsetX = 0;
g.shadowOffsetY = 1;
const ySteps = 6;
for (let k=0; k<=ySteps; k++) {
const y = PAD_T + k * ((cv.height - PAD_T - PAD_B) / ySteps);
const p = pOfY(y);
g.fillText(fmt2(p), PAD_L - 10, y + 4);
}