scripts/js/momentum_regime_server.js Repository BOT_EU Original path scripts/js/momentum_regime_server.jsRole SOURCE Size 82265 bytes Lines 2565 SHA-256 996c68e49cf2fc80fc8a5b8fd856b7740f159b65278bca47f087e6e983b50b2aDisplayed range 1501–2000 Previous file/page · Project index · Next file/page
}
} else if (!tfMap.has(String(baseTfMin))) {
tfMap.set(String(baseTfMin), candlesPlot);
}
const tfKeys = [...tfMap.keys()].sort((a, b) => Number(a) - Number(b));
const coreTf = ["1", "5", "60"].filter((tf) => tfMap.has(tf));
const orderedCore = coreTf.length ? coreTf : tfKeys;
const bigTf = orderedCore.includes("60") ? "60" : orderedCore[orderedCore.length - 1];
const lowTf = orderedCore[0];
const smallTf = orderedCore[Math.min(1, orderedCore.length - 1)] || lowTf;
const midTf = orderedCore[Math.floor(orderedCore.length / 2)] || bigTf;
const channels = {};
for (const tf of tfKeys) {
const arr = tfMap.get(tf) || [];
const ch = computeRangeChannel(arr);
channels[tf] = {
tf,
ok: !!ch.ok,
rangeUpper: ch.upper,
rangeLower: ch.lower,
rangeMid: ch.mid,
rangeWidthPct: ch.widthPct,
upper: ch.upper,
lower: ch.lower,
mid: ch.mid,
widthPct: ch.widthPct,
n: ch.n,
lastClose: arr.at(-1)?.close ?? null,
};
}
const bigArr = tfMap.get(bigTf) || candlesPlot;
const bigReg = computeRegimeLevelsMultiFromReversals(bigArr, {
extremaRadius: 3,
zonePctOfRange: 0.02,
minSwings: 8,
levels: 3,
});
if (bigReg.ok && channels[bigTf]?.ok) {
channels[bigTf].regimes = toRegimeArr(bigReg.levels);
channels[bigTf].regHalfW = bigReg.halfW;
const lv1 = channels[bigTf].regimes[0];
if (lv1?.upper && lv1?.lower) {
channels[bigTf].upper = lv1.upper;
channels[bigTf].lower = lv1.lower;
channels[bigTf].mid = (lv1.upper + lv1.lower) / 2;
channels[bigTf].widthPct = ((lv1.upper - lv1.lower) / Math.max(1e-8, channels[bigTf].mid)) * 100;
}
}
const lowArr = tfMap.get(lowTf) || candlesPlot;
const lowReg = computeRegimeLevelsMultiFromReversals(lowArr, {
extremaRadius: 2,
zonePctOfRange: 0.018,
minSwings: 10,
levels: 2,
});
const smallArr = tfMap.get(smallTf) || candlesPlot;
const smallReg = computeRegimeLevelsMultiFromReversals(smallArr, {
extremaRadius: 2,
zonePctOfRange: 0.018,
minSwings: 9,
levels: 3,
});
const microTf = "5";
const microTfMin = Number(microTf);
const microBars = Math.max(20, Math.ceil((12 * 60) / microTfMin));
const microSource = tfMap.get(microTf) || candlesPlot;
const microCandles = microSource.slice(-microBars);
const microCh = computeRangeChannel(microCandles);
const coreUppers = orderedCore
.map((tf) => channels[tf]?.rangeUpper)
.filter((v) => Number.isFinite(v));
const coreLowers = orderedCore
.map((tf) => channels[tf]?.rangeLower)
.filter((v) => Number.isFinite(v));
const upperAvg = meanKanalarz(coreUppers);
const lowerAvg = meanKanalarz(coreLowers);
const closeNow = close.at(-1);
const boxRaw = detectBreakout({
closeNow,
upper: channels[bigTf]?.upper,
lower: channels[bigTf]?.lower,
breakoutPct: 0.001,
});
const box = {
state: String(boxRaw?.state || "UNKNOWN").replace("BREAKOUT_UP", "UP_BIAS").replace("BREAKOUT_DOWN", "DOWN_BIAS").replace("IN_CHANNEL", "NEUTRAL"),
upThr: boxRaw?.upThr ?? null,
dnThr: boxRaw?.dnThr ?? null,
};
const dom = findDominantRegimeWedge(candlesPlot, {
minBars: Math.max(240, Math.floor(candlesPlot.length * 0.22)),
maxBars: candlesPlot.length,
stepBars: 60,
maxWidthPct: 12,
lowerStartShiftFrac: 0.18,
lengthPenalty: 6,
wedgeOpts: {
extremaRadius: 2,
recentIgnoreFracUpper: 0.12,
trimHighFrac: 0.1,
trimLowFrac: 0.1,
upperQ: 0.82,
lowerQ: 0.18,
slopeSteps: 31,
slopeRangeMult: 1.1,
touchTolPct: 0.002,
minTouches: 12,
minInlierRatio: 0.62,
},
});
let domBreakout = { state: "UNKNOWN" };
if (dom.ok) {
const sliceBars = dom.endIndex - dom.startIndex + 1;
domBreakout = detectWedgeBreakout({
closeNow,
x: sliceBars - 1,
upperSlope: dom.upperSlope,
lowerSlope: dom.lowerSlope,
bUpper: dom.bUpper,
bLower: dom.bLower,
breakoutPct: 0.001,
});
}
const dominant = dom.ok ? { ...dom, breakout: domBreakout } : { ok: false, breakout: domBreakout };
const midArr = tfMap.get(midTf) || candlesPlot;
const domMidRaw = findDominantRegimeWedge(midArr, {
minBars: Math.max(120, Math.floor(midArr.length * 0.25)),
maxBars: midArr.length,
stepBars: Math.max(10, Math.floor(midArr.length / 30)),
maxWidthPct: 12,
lowerStartShiftFrac: 0.18,
lengthPenalty: 6,
wedgeOpts: {
extremaRadius: 2,
recentIgnoreFracUpper: 0.12,
trimHighFrac: 0.1,
trimLowFrac: 0.1,
upperQ: 0.82,
lowerQ: 0.18,
slopeSteps: 31,
slopeRangeMult: 1.1,
touchTolPct: 0.002,
minTouches: 8,
minInlierRatio: 0.58,
},
});
let domMidBreakout = { state: "UNKNOWN" };
if (domMidRaw.ok) {
const sliceBars = domMidRaw.endIndex - domMidRaw.startIndex + 1;
domMidBreakout = detectWedgeBreakout({
closeNow: midArr.at(-1)?.close,
x: sliceBars - 1,
upperSlope: domMidRaw.upperSlope,
lowerSlope: domMidRaw.lowerSlope,
bUpper: domMidRaw.bUpper,
bLower: domMidRaw.bLower,
breakoutPct: 0.001,
});
}
const dominantMid = domMidRaw.ok
? {
...domMidRaw,
ok: true,
tf: `${midTf}m`,
breakout: domMidBreakout,
}
: { ok: false, tf: `${midTf}m`, breakout: domMidBreakout };
const impulseTrend = detectBoxImpulseTrend(candlesPlot, {
preBars: 520,
minInsideFrac: 0.84,
maxSidewaysWidthPct: 8.5,
breakoutPct: 0.001,
atrPeriod: 14,
postMin: 220,
postMax: 1200,
recencyLookbackBars: 1200,
});
let side4h = { ok: false, tf: "240" };
const c240 = tfMap.get("240") || [];
if (c240.length >= 80) {
const side4hRaw = detectSidewaysTrendFrames(c240, {
minBars: 70,
maxBars: 260,
lengthStep: 10,
scanStep: 3,
maxFrames: 7,
maxSlopePctPerBar: 0.0013,
maxWidthPct: 14,
minInlierRatio: 0.67,
narrowPct: 0.0035,
});
if (side4hRaw.ok) {
const currentRanges = [];
for (const r of channels[bigTf]?.regimes || []) {
if (Number.isFinite(r?.lower) && Number.isFinite(r?.upper)) currentRanges.push([r.lower, r.upper]);
}
if (Number.isFinite(channels[bigTf]?.lower) && Number.isFinite(channels[bigTf]?.upper)) {
currentRanges.push([channels[bigTf].lower, channels[bigTf].upper]);
}
if (Number.isFinite(microCh?.lower) && Number.isFinite(microCh?.upper)) {
currentRanges.push([microCh.lower, microCh.upper]);
}
if (Number.isFinite(upperAvg) && Number.isFinite(lowerAvg)) {
currentRanges.push([lowerAvg, upperAvg]);
}
side4h = {
ok: true,
tf: "240",
narrowPct: side4hRaw?.params?.narrowPct,
frames: (side4hRaw.frames || []).map((f, idx) => {
const cStart = c240[f.startIndex];
const cEnd = c240[f.endIndex];
const lowEdge = Math.min(f.lower0, f.lower1);
const highEdge = Math.max(f.upper0, f.upper1);
const overlapsCurrent = currentRanges.some(([lo, hi]) => rangesOverlap(lowEdge, highEdge, lo, hi));
return {
idx: idx + 1,
...f,
startTs: cStart?.t ?? (now - 4 * 60 * 60 * 1000),
endTs: cEnd?.t ?? now,
overlapsCurrent,
};
}),
};
}
}
const edgePct = 0.001;
const placements = {
bigTf,
buySuggest: channels[bigTf]?.lower ? channels[bigTf].lower * (1 + edgePct) : null,
sellSuggest: channels[bigTf]?.upper ? channels[bigTf].upper * (1 - edgePct) : null,
microBuySuggest: microCh.lower ? microCh.lower * (1 + edgePct) : null,
microSellSuggest: microCh.upper ? microCh.upper * (1 - edgePct) : null,
dominantBuySuggest: null,
dominantSellSuggest: null,
};
if (dominant.ok) {
const xNow = dominant.endIndex - dominant.startIndex;
const upNow = dominant.upperSlope * xNow + dominant.bUpper;
const dnNow = dominant.lowerSlope * xNow + dominant.bLower;
placements.dominantBuySuggest = dnNow * (1 + edgePct);
placements.dominantSellSuggest = upNow * (1 - edgePct);
}
const placementComponents = {
buyBig: [
{ label: "Big low", value: channels[bigTf]?.lower ?? null },
{ label: "Range", value: (channels[bigTf]?.upper ?? 0) - (channels[bigTf]?.lower ?? 0) },
{ label: "Trend bias", value: safeNum(signal?.derived?.pressureScore, 0) },
{ label: "Volatility", value: safeNum(signal?.atrPct, 0) },
],
sellBig: [
{ label: "Big high", value: channels[bigTf]?.upper ?? null },
{ label: "Range", value: (channels[bigTf]?.upper ?? 0) - (channels[bigTf]?.lower ?? 0) },
{ label: "Trend bias", value: safeNum(signal?.derived?.pressureScore, 0) },
{ label: "Volatility", value: safeNum(signal?.atrPct, 0) },
],
buyMicro: [
{ label: "Micro low", value: microCh.lower ?? null },
{ label: "Micro range", value: (microCh.upper ?? 0) - (microCh.lower ?? 0) },
{ label: "Momentum", value: safeNum(signal?.momentum?.score, 0) },
{ label: "15m trend %", value: pctChange(close[Math.max(0, close.length - 4)], closeNow) },
],
sellMicro: [
{ label: "Micro high", value: microCh.upper ?? null },
{ label: "Micro range", value: (microCh.upper ?? 0) - (microCh.lower ?? 0) },
{ label: "Momentum", value: safeNum(signal?.momentum?.score, 0) },
{ label: "60m trend %", value: pctChange(close[Math.max(0, close.length - 13)], closeNow) },
],
};
const micro = {
tf: `${microTfMin}m`,
hours: Math.max(1, Math.round((microCandles.length * microTfMin) / 60)),
upper: microCh.upper,
lower: microCh.lower,
};
const avg = {
upper: upperAvg,
lower: lowerAvg,
};
const tfs = channels;
const bigLowPack = lowReg.ok
? { ok: true, tf: String(lowTf), regHalfW: lowReg.halfW, regimes: toRegimeArr(lowReg.levels), primary: toRegimeArr(lowReg.levels)[0] || null }
: { ok: false, tf: String(lowTf), fail: lowReg };
const bigSmallPack = smallReg.ok
? { ok: true, tf: String(smallTf), regHalfW: smallReg.halfW, regimes: toRegimeArr(smallReg.levels), primary: toRegimeArr(smallReg.levels)[0] || null }
: { ok: false, tf: String(smallTf), fail: smallReg };
return {
placements,
placementComponents,
micro,
avg,
box,
tfs,
bigLow: bigLowPack,
bigSmall: bigSmallPack,
dominant,
dominantMid,
impulseTrend,
side4h,
};
}
function buildIndicatorSnapshot({ symbol, category, signal, placements, placementComponents }) {
const tsMs = safeNum(signal?.tsMs, nowMs());
return {
ts: signal?.ts || toIso(tsMs),
tsMs,
symbol,
category,
price: safeNum(signal?.price, 0),
regime: String(signal?.regime || "UNKNOWN"),
cci: safeNum(signal?.cci, 0),
adx14: safeNum(signal?.adx14, 0),
atrPct: safeNum(signal?.atrPct, 0),
emaSpreadPct: safeNum(signal?.emaSpreadPct, 0),
momentumScore: safeNum(signal?.momentum?.score, 0),
pressureScore: safeNum(signal?.derived?.pressureScore, 0),
shockScore: safeNum(signal?.derived?.shockScore, 0),
shockDir: safeNum(signal?.derived?.shockDir, 0),
botSignal: safeNum(signal?.derived?.botSignal, 0),
priceChange1mPct: safeNum(signal?.priceChange1mPct, 0),
priceChange3mPct: safeNum(signal?.priceChange3mPct, 0),
momentumDelta1m: safeNum(signal?.momentumDelta1m, 0),
momentumDelta3m: safeNum(signal?.momentumDelta3m, 0),
placements,
placementComponents,
};
}
function buildChartPayloadFromRows({ symbol, category, rows, plotTf = "5", signal = null, tfCandles = null }) {
const t = rows.map((r) => r.tsMs);
const close = rows.map((r) => r.price);
const first = t[0] || nowMs();
const last = t[t.length - 1] || first;
const lookbackDays = Math.max(1, Math.ceil((last - first) / (24 * 60 * 60 * 1000)));
const analyticsKey = buildAnalyticsCacheKey({
symbol,
category,
plotTf,
rows,
tfCandles,
});
let analytics = null;
if (analyticsComputeCache.key === analyticsKey && analyticsComputeCache.value) {
analytics = analyticsComputeCache.value;
} else {
analytics = computePlacementAnalytics(rows, signal, tfCandles);
analyticsComputeCache.key = analyticsKey;
analyticsComputeCache.value = analytics;
}
return {
symbol,
category,
updatedAt: new Date().toISOString(),
lookbackDays,
plotTf,
plot: { t, close },
placements: analytics.placements,
placementComponents: analytics.placementComponents,
tfs: analytics.tfs,
bigLow: analytics.bigLow,
bigSmall: analytics.bigSmall,
micro: analytics.micro,
avg: analytics.avg,
box: analytics.box,
dominant: analytics.dominant,
dominantMid: analytics.dominantMid,
impulseTrend: analytics.impulseTrend,
side4h: analytics.side4h,
};
}
async function buildDataJson(reqUrl) {
const symbol = normalizeSymbol(reqUrl.searchParams.get("symbol") || state.symbol) || state.symbol;
const tf = String(reqUrl.searchParams.get("tf") || reqUrl.searchParams.get("interval") || "5");
const fromMs = safeNum(reqUrl.searchParams.get("fromMs"), NaN);
const toMs = safeNum(reqUrl.searchParams.get("toMs"), NaN);
const hasRange = Number.isFinite(fromMs) && Number.isFinite(toMs) && toMs > fromMs;
if (state.mode === "history") {
const rows = state.selectedHistoryRows.length ? state.selectedHistoryRows : [];
if (rows.length < 2) {
return buildChartPayloadFromRows({
symbol: state.symbol,
category: state.category,
rows: [
{ tsMs: nowMs() - 300_000, price: 0 },
{ tsMs: nowMs(), price: 0 },
],
plotTf: tf,
});
}
const filtered = hasRange
? rows.filter((r) => safeNum(r.tsMs, 0) >= fromMs && safeNum(r.tsMs, 0) <= toMs)
: rows;
const baseRows = filtered.length >= 2 ? filtered : rows;
const series = baseRows.map((r) => ({ tsMs: r.tsMs, price: r.price }));
return buildChartPayloadFromRows({
symbol: symbol || state.symbol,
category: state.category,
rows: series,
plotTf: tf,
signal: getMarketSignal(),
});
}
// LIVE
let bybitRows = [];
try {
if (hasRange) {
bybitRows = await fetchBybitKlinesRange({
category: state.category,
symbol,
interval: tf,
fromMs,
toMs,
});
} else {
bybitRows = await fetchBybitKlinesLookback({
category: state.category,
symbol,
interval: tf,
lookbackMs: LIVE_DEFAULT_LOOKBACK_MS,
});
}
} catch {
bybitRows = [];
}
if (!bybitRows.length) {
const last = getMarketSignal();
const fallbackPrice = safeNum(last?.price, 0);
const now = nowMs();
const rows = [
{ tsMs: now - 300_000, price: fallbackPrice },
{ tsMs: now, price: fallbackPrice },
];
return buildChartPayloadFromRows({ symbol, category: state.category, rows, plotTf: tf, signal: getMarketSignal() });
}
const rows = bybitRows.map((r) => ({ tsMs: r.startMs, price: r.close, high: r.high, low: r.low }));
const firstTs = rows[0]?.tsMs;
const lastTs = rows[rows.length - 1]?.tsMs;
const lookbackDays = Number.isFinite(firstTs) && Number.isFinite(lastTs)
? Math.max(1, Math.ceil((lastTs - firstTs) / (24 * 60 * 60 * 1000)))
: 7;
const side4hDays = Math.max(lookbackDays * 2, 90);
let analyticsTfCandles = null;
try {
analyticsTfCandles = await fetchTfCandlesForAnalytics({
category: state.category,
symbol,
lookbackDays,
side4hDays,
hasRange,
fromMs,
toMs,
});
} catch {
analyticsTfCandles = null;
}
return buildChartPayloadFromRows({
symbol,
category: state.category,
rows,
plotTf: tf,
signal: getMarketSignal(),
tfCandles: analyticsTfCandles,
});