scripts/js/channels_core.js Repository BOT_EU Original path scripts/js/channels_core.jsRole SOURCE Size 36685 bytes Lines 1245 SHA-256 f03746c236efae73d88d7d7a0c9840541f6a64316e3aa585fec46d797370e26eDisplayed range 1–500 Previous file/page · Project index · Next file/page
// scripts/channels_core.js (ESM)
import axios from "axios";
const BYBIT_BASE = (process.env.BYBIT_TRADE_BASE_URL || process.env.BYBIT_BASE_URL || "https://api.bybit.eu").replace(/\/+$/, "");
function safeNum(x, d = 0) {
const n = Number(x);
return Number.isFinite(n) ? n : d;
}
/** Bybit v5 klines page */
export async function fetchKlinesPage({
category,
symbol,
interval,
limit = 1000,
start = undefined,
end = undefined,
}) {
const url = `${BYBIT_BASE}/v5/market/kline`;
const { data } = await axios.get(url, {
params: {
category,
symbol,
interval,
limit,
...(Number.isFinite(start) ? { start } : {}),
...(Number.isFinite(end) ? { end } : {}),
},
timeout: 12_000,
});
const list = data?.result?.list;
if (!Array.isArray(list)) return [];
const out = list
.map((row) => ({
t: safeNum(row[0]),
open: safeNum(row[1]),
high: safeNum(row[2]),
low: safeNum(row[3]),
close: safeNum(row[4]),
volume: safeNum(row[5]),
}))
.filter((x) => x.t > 0 && x.close > 0);
out.sort((a, b) => a.t - b.t);
return out;
}
/** Paginacja lookback */
export async function fetchKlinesLookback({
category,
symbol,
interval,
wantBars,
endMs = Date.now(),
pageLimit = 1000,
maxPages = 40,
}) {
const out = [];
let end = endMs;
for (let page = 0; page < maxPages; page++) {
if (out.length >= wantBars) break;
const remain = wantBars - out.length;
const lim = Math.max(1, Math.min(pageLimit, remain));
const batch = await fetchKlinesPage({ category, symbol, interval, limit: lim, end });
if (!batch.length) break;
out.push(...batch);
const oldestT = batch[0].t;
end = oldestT - 1;
if (batch.length < lim) break;
}
// dedupe by t
const map = new Map();
for (const c of out) map.set(c.t, c);
const arr = [...map.values()].sort((a, b) => a.t - b.t);
if (arr.length > wantBars) return arr.slice(arr.length - wantBars);
return arr;
}
/** Donchian/range box */
export function computeRangeChannel(candles) {
if (!Array.isArray(candles) || candles.length < 5) {
return { ok: false, upper: null, lower: null, mid: null, widthPct: null, n: candles?.length || 0 };
}
let upper = -Infinity;
let lower = Infinity;
for (const c of candles) {
if (!Number.isFinite(c.high) || !Number.isFinite(c.low)) continue;
upper = Math.max(upper, c.high);
lower = Math.min(lower, c.low);
}
if (!Number.isFinite(upper) || !Number.isFinite(lower) || upper <= 0 || lower <= 0 || upper <= lower) {
return { ok: false, upper: null, lower: null, mid: null, widthPct: null, n: candles.length };
}
const mid = (upper + lower) / 2;
const widthPct = mid > 0 ? ((upper - lower) / mid) * 100 : null;
return { ok: true, upper, lower, mid, widthPct, n: candles.length };
}
export function mean(nums) {
const xs = (nums || []).filter((x) => Number.isFinite(x));
if (!xs.length) return null;
return xs.reduce((a, b) => a + b, 0) / xs.length;
}
/* =========================
Helpers: stats
========================= */
function median(arr) {
const xs = (arr || []).filter((v) => Number.isFinite(v)).sort((a, b) => a - b);
if (!xs.length) return null;
const m = Math.floor(xs.length / 2);
if (xs.length % 2) return xs[m];
return (xs[m - 1] + xs[m]) / 2;
}
function quantile(arr, q) {
const xs = (arr || []).filter((v) => Number.isFinite(v)).sort((a, b) => a - b);
if (!xs.length) return null;
const qq = Math.max(0, Math.min(1, Number(q)));
const pos = (xs.length - 1) * qq;
const lo = Math.floor(pos);
const hi = Math.ceil(pos);
if (lo === hi) return xs[lo];
const w = pos - lo;
return xs[lo] * (1 - w) + xs[hi] * w;
}
function linregSlopeFromPoints(points) {
if (!points || points.length < 2) return 0;
const n = points.length;
let sx = 0,
sy = 0;
for (const p of points) {
sx += p.x;
sy += p.y;
}
const mx = sx / n,
my = sy / n;
let num = 0,
den = 0;
for (const p of points) {
const dx = p.x - mx;
const dy = p.y - my;
num += dx * dy;
den += dx * dx;
}
if (den === 0) return 0;
return num / den;
}
function linregInterceptFromPoints(points, slope) {
if (!points || !points.length) return 0;
let sx = 0,
sy = 0,
n = 0;
for (const p of points) {
if (!Number.isFinite(p?.x) || !Number.isFinite(p?.y)) continue;
sx += p.x;
sy += p.y;
n++;
}
if (!n) return 0;
const mx = sx / n;
const my = sy / n;
return my - slope * mx;
}
/* =========================
ATR / TR
========================= */
function trueRange(c, prevClose) {
const hi = Number(c?.high);
const lo = Number(c?.low);
const pc = Number(prevClose);
if (!Number.isFinite(hi) || !Number.isFinite(lo)) return null;
if (!Number.isFinite(pc)) return hi - lo;
return Math.max(hi - lo, Math.abs(hi - pc), Math.abs(lo - pc));
}
export function computeATR(candles, period = 14) {
if (!Array.isArray(candles) || candles.length < period + 2) {
return { ok: false, atr: null, n: candles?.length || 0 };
}
const trs = [];
for (let i = 1; i < candles.length; i++) {
const tr = trueRange(candles[i], candles[i - 1]?.close);
if (Number.isFinite(tr) && tr > 0) trs.push(tr);
}
if (trs.length < period) return { ok: false, atr: null, n: candles.length };
let atr = trs.slice(0, period).reduce((a, b) => a + b, 0) / period;
for (let i = period; i < trs.length; i++) atr = (atr * (period - 1) + trs[i]) / period;
return { ok: true, atr, n: candles.length };
}
/* =========================
Extrema
========================= */
export function findLocalExtrema(candles, r = 2) {
const highs = [];
const lows = [];
const n = candles.length;
const R = Math.max(1, Math.floor(r));
for (let i = R; i < n - R; i++) {
const hi = Number(candles[i]?.high);
const lo = Number(candles[i]?.low);
if (!Number.isFinite(hi) || !Number.isFinite(lo)) continue;
let isHigh = true;
let isLow = true;
for (let k = 1; k <= R; k++) {
const hL = Number(candles[i - k]?.high);
const hR = Number(candles[i + k]?.high);
const lL = Number(candles[i - k]?.low);
const lR = Number(candles[i + k]?.low);
if (![hL, hR, lL, lR].every(Number.isFinite)) {
isHigh = false;
isLow = false;
break;
}
if (!(hi >= hL && hi > hR)) isHigh = false;
if (!(lo <= lL && lo < lR)) isLow = false;
if (!isHigh && !isLow) break;
}
if (isHigh) highs.push(i);
if (isLow) lows.push(i);
}
return { highs, lows };
}
/**
* MAX OVERLAP SET dla stref [c-halfW, c+halfW]
*/
function maxOverlapSet(centers, halfW) {
const cs = (centers || []).map(Number).filter(Number.isFinite);
if (!cs.length || !(Number.isFinite(halfW) && halfW > 0)) return { bestPoint: null, idxs: [] };
const events = [];
for (let i = 0; i < cs.length; i++) {
const a = cs[i] - halfW;
const b = cs[i] + halfW;
events.push({ x: a, d: +1, i });
events.push({ x: b, d: -1, i });
}
events.sort((p, q) => (p.x - q.x) || (q.d - p.d));
let cur = 0;
let best = 0;
let bestX = null;
for (const e of events) {
cur += e.d;
if (cur > best) {
best = cur;
bestX = e.x;
}
}
if (!Number.isFinite(bestX)) return { bestPoint: null, idxs: [] };
const idxs = [];
for (let i = 0; i < cs.length; i++) {
if (bestX >= cs[i] - halfW && bestX <= cs[i] + halfW) idxs.push(i);
}
return { bestPoint: bestX, idxs };
}
/**
* ITERACYJNE "REGIME LEVELS" z odwróceń
*/
export function computeRegimeLevelsMultiFromReversals(candles, opts = {}) {
const {
extremaRadius = 3,
zonePctOfRange = 0.02,
overrideHalfW = null,
minSwings = 8,
levels = 3,
} = opts;
if (!Array.isArray(candles) || candles.length < 50) {
return { ok: false, reason: "too_few", n: candles?.length || 0 };
}
const rc = computeRangeChannel(candles);
if (!rc.ok) return { ok: false, reason: "bad_range", n: candles.length };
const range = rc.upper - rc.lower;
const halfW =
Number.isFinite(overrideHalfW) && overrideHalfW > 0 ? overrideHalfW : Math.max(1, range * zonePctOfRange);
const ex = findLocalExtrema(candles, extremaRadius);
let swingHighs = ex.highs.map((i) => Number(candles[i]?.high)).filter(Number.isFinite);
let swingLows = ex.lows.map((i) => Number(candles[i]?.low)).filter(Number.isFinite);
if (swingHighs.length < minSwings || swingLows.length < minSwings) {
return {
ok: false,
reason: "too_few_swings",
n: candles.length,
rangeUpper: rc.upper,
rangeLower: rc.lower,
range,
halfW,
swingHighs: swingHighs.length,
swingLows: swingLows.length,
};
}
const outLevels = [];
for (let k = 0; k < Math.max(1, Math.floor(levels)); k++) {
if (swingHighs.length < minSwings || swingLows.length < minSwings) break;
const hi = maxOverlapSet(swingHighs, halfW);
const lo = maxOverlapSet(swingLows, halfW);
const hiCenters = hi.idxs.map((j) => swingHighs[j]).filter(Number.isFinite);
const loCenters = lo.idxs.map((j) => swingLows[j]).filter(Number.isFinite);
const upper = median(hiCenters);
const lower = median(loCenters);
if (!(Number.isFinite(upper) && Number.isFinite(lower) && upper > lower)) break;
outLevels.push({
upper,
lower,
countHi: hiCenters.length,
countLo: loCenters.length,
bestPointHi: hi.bestPoint,
bestPointLo: lo.bestPoint,
halfW,
});
const hiUsedSet = new Set(hiCenters.map((x) => Number(x)));
const loUsedSet = new Set(loCenters.map((x) => Number(x)));
swingHighs = swingHighs.filter((v) => !hiUsedSet.has(Number(v)));
swingLows = swingLows.filter((v) => !loUsedSet.has(Number(v)));
}
return {
ok: outLevels.length > 0,
n: candles.length,
rangeUpper: rc.upper,
rangeLower: rc.lower,
range,
halfW,
levels: outLevels,
};
}
/* =========================
Dominant wedge (trend channel) — bez zmian
========================= */
export function computeRegimeWedgeChannel(candles, opts = {}) {
const {
extremaRadius = 2,
recentIgnoreFracUpper = 0.12,
trimHighFrac = 0.10,
trimLowFrac = 0.10,
upperQ = 0.82,
lowerQ = 0.18,
slopeSteps = 31,
slopeRangeMult = 1.1,
slopeFallbackAbs = 4.0,
touchTolPct = 0.002,
minTouches = 12,
minInlierRatio = 0.62,
wInlier = 40.0,
wTouches = 1.5,
wWidth = 0.55,
wOut = 10.0,
wRecentHigh = 6.0,
} = opts;
if (!Array.isArray(candles) || candles.length < 160) {
return { ok: false, reason: "too_few", n: candles?.length || 0 };
}
const n = candles.length;
const ext = findLocalExtrema(candles, extremaRadius);
const hiIdxAll = ext.highs;
const loIdxAll = ext.lows;
const cutUpper = Math.floor(n * (1 - Math.max(0, Math.min(0.49, recentIgnoreFracUpper))));
const hiIdx = hiIdxAll.filter((i) => i < cutUpper);
const loIdx = loIdxAll;
const hiPts = hiIdx.map((i) => ({ x: i, y: Number(candles[i].high) })).filter((p) => Number.isFinite(p.y));
const loPts = loIdx.map((i) => ({ x: i, y: Number(candles[i].low) })).filter((p) => Number.isFinite(p.y));
const mU0 = hiPts.length >= 2 ? linregSlopeFromPoints(hiPts) : 0;
const mL0 = loPts.length >= 2 ? linregSlopeFromPoints(loPts) : 0;
const absBase = Math.max(Math.abs(mU0), Math.abs(mL0));
const range = absBase > 1e-9 ? absBase * slopeRangeMult : slopeFallbackAbs;
const steps = Math.max(11, Math.floor(slopeSteps));
const half = Math.floor(steps / 2);
function slopeAt(m0, k) {
return m0 + (k * range) / Math.max(1, half);
}
function trimmedQuantile(arr, q, trimLoFrac = 0, trimHiFrac = 0) {
const xs = (arr || []).filter((v) => Number.isFinite(v)).sort((a, b) => a - b);
if (!xs.length) return null;
const lo = Math.floor(xs.length * Math.max(0, Math.min(0.49, trimLoFrac)));
const hi = Math.ceil(xs.length * (1 - Math.max(0, Math.min(0.49, trimHiFrac))));
const ys = xs.slice(lo, Math.max(lo + 1, hi));
return quantile(ys, q);
}
let best = null;
const closes = new Array(n);
for (let i = 0; i < n; i++) closes[i] = Number(candles[i]?.close);
const recentStart = Math.floor(n * 0.85);
const recentHighs = [];
for (let i = recentStart; i < n; i++) {
const h = Number(candles[i]?.high);
if (Number.isFinite(h)) recentHighs.push(h);
}
const recentHighMax = recentHighs.length ? Math.max(...recentHighs) : null;
for (let ku = -half; ku <= half; ku++) {
const mU = slopeAt(mU0, ku);
const upperArr = [];
for (const i of hiIdx) {
const h = Number(candles[i]?.high);
if (Number.isFinite(h)) upperArr.push(h - mU * i);
}
if (upperArr.length < 10) continue;
const bU = trimmedQuantile(upperArr, upperQ, 0, trimHighFrac);
if (!Number.isFinite(bU)) continue;
for (let kl = -half; kl <= half; kl++) {
const mL = slopeAt(mL0, kl);
const lowerArr = [];
for (const i of loIdx) {
const l = Number(candles[i]?.low);
if (Number.isFinite(l)) lowerArr.push(l - mL * i);
}
if (lowerArr.length < 10) continue;
const bL = trimmedQuantile(lowerArr, lowerQ, trimLowFrac, 0);
if (!Number.isFinite(bL)) continue;
const up0 = mU * 0 + bU;
const dn0 = mL * 0 + bL;
const up1 = mU * (n - 1) + bU;
const dn1 = mL * (n - 1) + bL;
if (!(up0 > dn0 && up1 > dn1 && up0 > 0 && dn0 > 0 && up1 > 0 && dn1 > 0)) continue;
let okC = 0;
let inC = 0;
let outDist = 0;
for (let i = 0; i < n; i++) {
const cl = closes[i];