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 1001–1500 Previous file/page · Project index · Next file/page
"profit_quote",
"retain_pct",
"step_pct",
"momentum_mult",
"order_id",
"order_link_id",
"mode",
"zone",
"regime",
"counter_status",
"counter_side",
"counter_price",
"counter_qty",
"counter_order_id",
"counter_reason",
"reason_codes",
];
const filteredHeaders = wanted.filter((h) => headers.includes(h));
const filteredRows = rows.map((r) => {
const o = {};
for (const h of filteredHeaders) o[h] = r[h] ?? "";
return o;
});
// ▶ FIX 8.0: compute fill-level summary from actual data
let totalFills = filteredRows.length;
let buyFills = 0, sellFills = 0, loopFills = 0, gridFills = 0, counterFills = 0;
let totalProfitQuote = 0, totalVolumeQuote = 0;
let profitableLoops = 0, unprofitableLoops = 0;
for (const r of filteredRows) {
const side = String(r.side || "").toLowerCase();
if (side === "buy") buyFills++;
if (side === "sell") sellFills++;
const kind = String(r.kind || "").toUpperCase();
if (kind.includes("LOOP")) loopFills++;
else if (kind.includes("COUNTER")) counterFills++;
else gridFills++;
const profit = Number(r.profit_quote || 0);
if (Number.isFinite(profit) && profit !== 0) {
totalProfitQuote += profit;
if (profit > 0) profitableLoops++;
else unprofitableLoops++;
}
const vol = Number(r.quote_amount || 0);
if (Number.isFinite(vol)) totalVolumeQuote += vol;
}
const computedSummary = {
"total_fills": totalFills,
"buy_fills": buyFills,
"sell_fills": sellFills,
"loop_fills": loopFills,
"grid_fills": gridFills,
"counter_fills": counterFills,
"total_profit_quote": totalProfitQuote.toFixed(6),
"profitable_loops": profitableLoops,
"unprofitable_loops": unprofitableLoops,
"total_volume_quote": totalVolumeQuote.toFixed(2),
"avg_profit_per_loop": loopFills > 0 ? (totalProfitQuote / loopFills).toFixed(6) : "0",
};
return { summary: computedSummary, headers: filteredHeaders, rows: filteredRows };
}
async function parseRunSummaryTsv(runName) {
try {
const abs = path.resolve(RUNS_DIR, runName, "run_summary.tsv");
const raw = await fs.readFile(abs, "utf8");
const lines = String(raw || "").split(/\r?\n/).filter((l) => String(l).trim().length > 0);
if (lines.length < 2) return {};
const headers = splitTsvLine(lines[0]);
const values = splitTsvLine(lines[1]);
const out = {};
for (let i = 0; i < headers.length; i += 1) {
const k = headers[i];
if (!k) continue;
out[k] = values[i] ?? "";
}
return out;
} catch {
return {};
}
}
function rowsFromJson(raw) {
try {
const parsed = JSON.parse(raw);
const arr = Array.isArray(parsed) ? parsed : [];
const rows = arr
.map((r) => ({
ts: r.ts || toIso(safeNum(r.tsMs, nowMs())),
tsMs: safeNum(r.tsMs, new Date(r.ts || Date.now()).getTime()),
symbol: String(r.symbol || state.symbol),
category: String(r.category || state.category),
price: safeNum(r.price, NaN),
regime: r.regime ?? null,
cci: numOrNull(r.cci),
adx14: numOrNull(r.adx14),
atrPct: numOrNull(r.atrPct),
emaSpreadPct: numOrNull(r.emaSpreadPct),
momentumScore: numOrNull(r.momentumScore),
pressureScore: numOrNull(r.pressureScore),
shockScore: numOrNull(r.shockScore),
shockDir: numOrNull(r.shockDir),
botSignal: numOrNull(r.botSignal),
}))
.filter((r) => Number.isFinite(r.tsMs) && Number.isFinite(r.price));
rows.sort((a, b) => a.tsMs - b.tsMs);
return rows;
} catch {
return [];
}
}
async function loadHistoryRowsFromFile(filePath) {
const ext = path.extname(filePath).toLowerCase();
const raw = await fs.readFile(filePath, "utf8");
if (ext === ".csv") return rowsFromCsv(raw);
if (ext === ".jsonl") return rowsFromJsonl(raw);
if (ext === ".json") return rowsFromJson(raw);
// fallback heurystyczny
if (raw.trim().startsWith("{")) return rowsFromJsonl(raw);
if (raw.trim().startsWith("[")) return rowsFromJson(raw);
return rowsFromCsv(raw);
}
function buildIndicatorRowsFromHistoryRows(rows, symbol, category) {
const sorted = [...rows].sort((a, b) => safeNum(a.tsMs, 0) - safeNum(b.tsMs, 0));
const out = [];
for (let i = 0; i < sorted.length; i++) {
const row = sorted[i];
const windowRows = sorted
.slice(Math.max(0, i - 300), i + 1)
.map((r) => ({ tsMs: r.tsMs, price: r.price }));
const signal = buildSignalFromHistoryRow(row);
const analytics = computePlacementAnalytics(windowRows, signal);
out.push(
buildIndicatorSnapshot({
symbol: String(row.symbol || symbol || state.symbol),
category: String(row.category || category || state.category),
signal,
placements: analytics.placements,
placementComponents: analytics.placementComponents,
})
);
}
return out;
}
async function fetchBybitKlines({ category, symbol, interval = "5", limit = 1000, startMs = null, endMs = null }) {
const bybitBase = String(
process.env.BYBIT_TRADE_BASE_URL || process.env.BYBIT_BASE_URL || "https://api.bybit.eu"
).replace(/\/+$/, "");
const url = new URL(`${bybitBase}/v5/market/kline`);
url.searchParams.set("category", category);
url.searchParams.set("symbol", symbol);
url.searchParams.set("interval", String(interval));
url.searchParams.set("limit", String(limit));
if (Number.isFinite(startMs)) url.searchParams.set("start", String(Math.floor(startMs)));
if (Number.isFinite(endMs)) url.searchParams.set("end", String(Math.floor(endMs)));
const res = await fetch(url.toString());
if (!res.ok) {
throw new Error(`Bybit HTTP ${res.status}`);
}
const json = await res.json();
if (json?.retCode !== 0) {
throw new Error(`Bybit retCode=${json?.retCode} ${json?.retMsg || ""}`.trim());
}
const rows = Array.isArray(json?.result?.list) ? json.result.list : [];
return rows
.map((r) => ({
startMs: safeNum(r[0], 0),
open: safeNum(r[1], NaN),
high: safeNum(r[2], NaN),
low: safeNum(r[3], NaN),
close: safeNum(r[4], NaN),
}))
.filter((r) => r.startMs > 0 && Number.isFinite(r.close))
.sort((a, b) => a.startMs - b.startMs);
}
async function fetchBybitKlinesLookback({ category, symbol, interval = "5", lookbackMs = LIVE_DEFAULT_LOOKBACK_MS }) {
const targetStart = nowMs() - Math.max(60_000, safeNum(lookbackMs, LIVE_DEFAULT_LOOKBACK_MS));
let cursorEnd = nowMs();
const out = [];
const seen = new Set();
for (let page = 0; page < 8; page++) {
const batch = await fetchBybitKlines({
category,
symbol,
interval,
limit: 1000,
endMs: cursorEnd,
});
if (!batch.length) break;
for (const r of batch) {
if (r.startMs < targetStart) continue;
if (seen.has(r.startMs)) continue;
seen.add(r.startMs);
out.push(r);
}
const earliest = safeNum(batch[0]?.startMs, 0);
if (!earliest || earliest <= targetStart) break;
cursorEnd = earliest - 1;
}
out.sort((a, b) => a.startMs - b.startMs);
return out;
}
function barsForLookbackDays(tf, days) {
const mins = Number(tf);
const totalMin = Number(days) * 24 * 60;
if (!Number.isFinite(mins) || mins <= 0) return 0;
return Math.ceil(totalMin / mins);
}
async function fetchBybitKlinesBars({ category, symbol, interval = "5", wantBars = 1000 }) {
const out = [];
const seen = new Set();
let cursorEnd = nowMs();
for (let page = 0; page < 60; page += 1) {
if (out.length >= wantBars) break;
const remain = Math.max(1, wantBars - out.length);
const lim = Math.max(1, Math.min(1000, remain));
const batch = await fetchBybitKlines({
category,
symbol,
interval,
limit: lim,
endMs: cursorEnd,
});
if (!batch.length) break;
for (const r of batch) {
if (seen.has(r.startMs)) continue;
seen.add(r.startMs);
out.push(r);
}
const oldest = safeNum(batch[0]?.startMs, 0);
if (!oldest) break;
cursorEnd = oldest - 1;
if (batch.length < lim) break;
}
out.sort((a, b) => a.startMs - b.startMs);
if (out.length > wantBars) return out.slice(out.length - wantBars);
return out;
}
async function fetchBybitKlinesRange({ category, symbol, interval = "5", fromMs, toMs }) {
const out = [];
const seen = new Set();
const from = safeNum(fromMs, NaN);
const to = safeNum(toMs, NaN);
if (!Number.isFinite(from) || !Number.isFinite(to) || to <= from) return out;
let cursorEnd = to;
for (let page = 0; page < 50; page++) {
const batch = await fetchBybitKlines({
category,
symbol,
interval,
limit: 1000,
endMs: cursorEnd,
});
if (!batch.length) break;
for (const row of batch) {
if (row.startMs < from || row.startMs > to) continue;
if (seen.has(row.startMs)) continue;
seen.add(row.startMs);
out.push(row);
}
const oldest = safeNum(batch[0]?.startMs, 0);
if (!oldest || oldest <= from) break;
cursorEnd = oldest - 1;
}
out.sort((a, b) => a.startMs - b.startMs);
return out;
}
function toKanalarzCandles(rows) {
return (rows || [])
.map((r) => ({
t: safeNum(r?.startMs, 0),
open: safeNum(r?.open, NaN),
high: safeNum(r?.high, NaN),
low: safeNum(r?.low, NaN),
close: safeNum(r?.close, NaN),
}))
.filter((r) => r.t > 0 && Number.isFinite(r.close) && Number.isFinite(r.high) && Number.isFinite(r.low));
}
async function fetchTfCandlesForAnalytics({ category, symbol, lookbackDays = 7, side4hDays = 90, hasRange, fromMs, toMs }) {
const required = ["1", "5", "60"];
const out = {};
const cacheKey = `${String(category || "").toLowerCase()}:${String(symbol || "").toUpperCase()}`;
if (analyticsTfFetchCache.key !== cacheKey) {
analyticsTfFetchCache.key = cacheKey;
analyticsTfFetchCache.perTf.clear();
}
async function fetchOrReuseTf(tf, wantBars) {
const bucket = tfBucketNow(tf);
const cached = analyticsTfFetchCache.perTf.get(tf);
if (!hasRange && cached && cached.bucket === bucket && cached.wantBars === wantBars && Array.isArray(cached.candles) && cached.candles.length >= 10) {
return cached.candles;
}
const rows = hasRange
? await fetchBybitKlinesRange({ category, symbol, interval: tf, fromMs, toMs })
: await fetchBybitKlinesBars({
category,
symbol,
interval: tf,
wantBars,
});
const candles = toKanalarzCandles(rows);
if (!hasRange) {
analyticsTfFetchCache.perTf.set(tf, { bucket, wantBars, candles });
}
return candles;
}
for (const tf of required) {
try {
const wantBars = tf === "1"
? Math.min(barsForLookbackDays(tf, lookbackDays), 6000)
: Math.min(barsForLookbackDays(tf, lookbackDays), 5000);
const candles = await fetchOrReuseTf(tf, wantBars);
if (candles.length >= 10) out[tf] = candles;
} catch {
// ignore per-TF errors; analytics will fallback
}
}
try {
const wantBars4h = Math.max(120, barsForLookbackDays("240", side4hDays));
const c4 = await fetchOrReuseTf("240", wantBars4h);
if (c4.length >= 10) out["240"] = c4;
} catch {
// ignore 4h errors
}
return out;
}
function computePlacementAnalytics(rows, signal = null, externalTfCandles = null) {
const candlesPlot = (rows || [])
.map((r, i) => {
const close = safeNum(r?.price, NaN);
if (!Number.isFinite(close) || close <= 0) return null;
const high = safeNum(r?.high, close);
const low = safeNum(r?.low, close);
const open = safeNum(r?.open, close);
return {
t: safeNum(r?.tsMs, nowMs() + i),
open: Number.isFinite(open) ? open : close,
high: Number.isFinite(high) ? Math.max(high, close) : close,
low: Number.isFinite(low) ? Math.min(low, close) : close,
close,
};
})
.filter(Boolean);
if (candlesPlot.length < 5) {
return {
placements: {},
placementComponents: {},
micro: {},
avg: {},
box: {},
tfs: {},
bigLow: {},
bigSmall: {},
dominant: {},
dominantMid: {},
impulseTrend: {},
side4h: {},
};
}
const close = candlesPlot.map((c) => c.close);
const now = candlesPlot.at(-1)?.t ?? nowMs();
function toRegimeArr(levels) {
return (levels || []).map((lv, idx) => ({
idx: idx + 1,
upper: lv.upper,
lower: lv.lower,
countHi: lv.countHi,
countLo: lv.countLo,
halfW: lv.halfW,
bestPointHi: lv.bestPointHi,
bestPointLo: lv.bestPointLo,
}));
}
function rangesOverlap(aLow, aHigh, bLow, bHigh) {
if (![aLow, aHigh, bLow, bHigh].every((v) => Number.isFinite(v))) return false;
const lo = Math.max(aLow, bLow);
const hi = Math.min(aHigh, bHigh);
return hi >= lo;
}
function inferBaseTfMinutes(candles) {
if (!Array.isArray(candles) || candles.length < 3) return 5;
const diffs = [];
for (let i = 1; i < candles.length; i += 1) {
const d = safeNum(candles[i]?.t, 0) - safeNum(candles[i - 1]?.t, 0);
if (d > 0) diffs.push(d);
}
if (!diffs.length) return 5;
diffs.sort((a, b) => a - b);
const ms = diffs[Math.floor(diffs.length / 2)] || 300000;
return Math.max(1, Math.round(ms / 60000));
}
function aggregateCandles(candles, targetTfMin, baseTfMin) {
if (!Array.isArray(candles) || !candles.length) return [];
if (!(Number.isFinite(targetTfMin) && Number.isFinite(baseTfMin))) return [];
if (targetTfMin <= baseTfMin) return candles.slice();
const ratio = targetTfMin / baseTfMin;
if (Math.abs(ratio - Math.round(ratio)) > 1e-9) return [];
const step = Math.max(1, Math.round(ratio));
const out = [];
for (let i = 0; i < candles.length; i += step) {
const chunk = candles.slice(i, i + step);
if (!chunk.length) continue;
const first = chunk[0];
const last = chunk[chunk.length - 1];
let high = -Infinity;
let low = Infinity;
for (const c of chunk) {
if (Number.isFinite(c.high)) high = Math.max(high, c.high);
if (Number.isFinite(c.low)) low = Math.min(low, c.low);
}
if (!Number.isFinite(high) || !Number.isFinite(low)) continue;
out.push({
t: first.t,
open: first.open,
high,
low,
close: last.close,
});
}
return out;
}
const tfMap = new Map();
if (externalTfCandles && typeof externalTfCandles === "object") {
for (const [tf, arr] of Object.entries(externalTfCandles)) {
const key = String(tf);
const candles = Array.isArray(arr) ? arr : [];
if (candles.length >= 10) tfMap.set(key, candles);
}
}
const baseTfMin = tfMap.size
? Number([...tfMap.keys()].sort((a, b) => Number(a) - Number(b))[0])
: inferBaseTfMinutes(candlesPlot);
if (!tfMap.size) {
tfMap.set(String(baseTfMin), candlesPlot);
for (const tf of [15, 60, 240]) {
if (tf < baseTfMin) continue;
const agg = aggregateCandles(candlesPlot, tf, baseTfMin);
if (agg.length >= 40) tfMap.set(String(tf), agg);