diff --git "a/server.cjs" "b/server.cjs" deleted file mode 100644--- "a/server.cjs" +++ /dev/null @@ -1,3530 +0,0 @@ -var __create = Object.create; -var __defProp = Object.defineProperty; -var __getOwnPropDesc = Object.getOwnPropertyDescriptor; -var __getOwnPropNames = Object.getOwnPropertyNames; -var __getProtoOf = Object.getPrototypeOf; -var __hasOwnProp = Object.prototype.hasOwnProperty; -var __copyProps = (to, from, except, desc) => { - if (from && typeof from === "object" || typeof from === "function") { - for (let key of __getOwnPropNames(from)) - if (!__hasOwnProp.call(to, key) && key !== except) - __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); - } - return to; -}; -var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( - // If the importer is in node compatibility mode or this is not an ESM - // file that has been converted to a CommonJS file using a Babel- - // compatible transform (i.e. "__esModule" has not been set), then set - // "default" to the CommonJS "module.exports" for node compatibility. - isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, - mod -)); - -// server.ts -var import_express = __toESM(require("express"), 1); -var import_path = __toESM(require("path"), 1); -var import_url = require("url"); -var import_vite = require("vite"); -var import_genai = require("@google/genai"); -var import_dotenv = __toESM(require("dotenv"), 1); - -// server/realMarketData.ts -function getBrasiliaTimeStr(timestamp = Date.now(), includeSeconds = true) { - try { - return new Date(timestamp).toLocaleTimeString("pt-BR", { - timeZone: "America/Sao_Paulo", - hour: "2-digit", - minute: "2-digit", - ...includeSeconds ? { second: "2-digit" } : {} - }); - } catch { - const d = new Date(timestamp - 3 * 3600 * 1e3); - const h = String(d.getUTCHours()).padStart(2, "0"); - const m = String(d.getUTCMinutes()).padStart(2, "0"); - const s = String(d.getUTCSeconds()).padStart(2, "0"); - return includeSeconds ? `${h}:${m}:${s}` : `${h}:${m}`; - } -} -function getBrasiliaDateStr(timestamp = Date.now()) { - try { - return new Date(timestamp).toLocaleDateString("pt-BR", { - timeZone: "America/Sao_Paulo", - day: "2-digit", - month: "2-digit", - year: "numeric" - }); - } catch { - const d = new Date(timestamp - 3 * 3600 * 1e3); - const day = String(d.getUTCDate()).padStart(2, "0"); - const month = String(d.getUTCMonth() + 1).padStart(2, "0"); - const year = d.getUTCFullYear(); - return `${day}/${month}/${year}`; - } -} -var FIBONACCI_TARGET_RATIOS = [ - { level: 1, ratio: 1.618, label: "TP1 (1.618 Fibo Golden)" }, - { level: 2, ratio: 2, label: "TP2 (2.000 Expans\xE3o)" }, - { level: 3, ratio: 2.618, label: "TP3 (2.618 Extens\xE3o Maior)" }, - { level: 4, ratio: 3.618, label: "TP4 (3.618 Proje\xE7\xE3o Institucional)" }, - { level: 5, ratio: 4.236, label: "TP5 (4.236 Cl\xEDmax M\xE1ximo)" } -]; -function calculateFibonacciTargets(entryPrice, stopLoss, direction, currentPrice = entryPrice, decimals = 2) { - const isLong = direction === "COMPRA" || direction === "LONG"; - const risk = Math.abs(entryPrice - stopLoss) || entryPrice * 0.018; - return FIBONACCI_TARGET_RATIOS.map((item) => { - const targetPriceRaw = isLong ? entryPrice + risk * item.ratio : entryPrice - risk * item.ratio; - const targetPrice = Number(targetPriceRaw.toFixed(decimals)); - const pnlPercentRaw = isLong ? (targetPrice - entryPrice) / (entryPrice || 1) * 100 : (entryPrice - targetPrice) / (entryPrice || 1) * 100; - const pnlPercent = Number(pnlPercentRaw.toFixed(2)); - const isHit = isLong ? currentPrice >= targetPrice : currentPrice <= targetPrice; - return { - level: item.level, - ratioLabel: item.label, - ratio: item.ratio, - price: targetPrice, - pnlPercent, - isHit - }; - }); -} -function calculateEMA(prices, period) { - if (prices.length === 0) return 0; - if (prices.length < period) { - const sum = prices.reduce((acc, p) => acc + p, 0); - return Number((sum / prices.length).toFixed(4)); - } - const k = 2 / (period + 1); - let ema2 = prices.slice(0, period).reduce((acc, p) => acc + p, 0) / period; - for (let i = period; i < prices.length; i++) { - ema2 = prices[i] * k + ema2 * (1 - k); - } - return Number(ema2.toFixed(4)); -} -function calculateRSI(closes2, period = 14) { - if (closes2.length < period + 1) return 50; - let gains = 0; - let losses = 0; - for (let i = 1; i <= period; i++) { - const change = closes2[i] - closes2[i - 1]; - if (change >= 0) gains += change; - else losses -= change; - } - let avgGain = gains / period; - let avgLoss = losses / period; - for (let i = period + 1; i < closes2.length; i++) { - const change = closes2[i] - closes2[i - 1]; - if (change >= 0) { - avgGain = (avgGain * (period - 1) + change) / period; - avgLoss = avgLoss * (period - 1) / period; - } else { - avgGain = avgGain * (period - 1) / period; - avgLoss = (avgLoss * (period - 1) - change) / period; - } - } - if (avgLoss === 0) return 100; - const rs = avgGain / avgLoss; - return Math.round(100 - 100 / (1 + rs)); -} -function calculateATR(candles, period = 14) { - if (candles.length < 2) return 0; - const trs = []; - for (let i = 1; i < candles.length; i++) { - const current = candles[i]; - const prev = candles[i - 1]; - const tr = Math.max( - current.high - current.low, - Math.abs(current.high - prev.close), - Math.abs(current.low - prev.close) - ); - trs.push(tr); - } - if (trs.length === 0) return 0; - const atr2 = trs.slice(-period).reduce((acc, v) => acc + v, 0) / Math.min(trs.length, period); - return Number(atr2.toFixed(4)); -} -function generateCandlesFromSparkline(prices, baseTime = Date.now()) { - const candles1h = []; - const count1h = Math.min(prices.length, 30); - const startIdx = prices.length - count1h; - for (let i = startIdx; i < prices.length; i++) { - const p = prices[i]; - const prevP = i > 0 ? prices[i - 1] : p; - const time = baseTime - (prices.length - 1 - i) * 3600 * 1e3; - const spread = Math.abs(p - prevP) * 0.4 || p * 3e-3; - candles1h.push({ - timestamp: time, - timeStr: getBrasiliaTimeStr(time, false), - open: prevP, - high: Math.max(prevP, p) + spread * 0.6, - low: Math.min(prevP, p) - spread * 0.6, - close: p, - volume: Math.round(p * 120) - }); - } - const candles15m = []; - const latestPrice = prices[prices.length - 1] || 1; - for (let i = 29; i >= 0; i--) { - const time = baseTime - i * 15 * 60 * 1e3; - const offset = Math.sin(i * 0.4) * 3e-3 * latestPrice; - const open = latestPrice + offset; - const close = open + (i % 2 === 0 ? 1 : -1) * 15e-4 * latestPrice; - const high = Math.max(open, close) + 1e-3 * latestPrice; - const low = Math.min(open, close) - 1e-3 * latestPrice; - candles15m.push({ - timestamp: time, - timeStr: getBrasiliaTimeStr(time, false), - open: Number(open.toFixed(4)), - high: Number(high.toFixed(4)), - low: Number(low.toFixed(4)), - close: Number(close.toFixed(4)), - volume: Math.round(latestPrice * 45) - }); - } - const candles1d = []; - const daysCount = Math.floor(prices.length / 24); - for (let d = daysCount - 1; d >= 0; d--) { - const slice = prices.slice(d * 24, (d + 1) * 24); - if (slice.length === 0) continue; - const time = baseTime - (daysCount - 1 - d) * 86400 * 1e3; - const open = slice[0]; - const close = slice[slice.length - 1]; - const high = Math.max(...slice); - const low = Math.min(...slice); - candles1d.push({ - timestamp: time, - timeStr: getBrasiliaDateStr(time).slice(0, 5), - open, - high, - low, - close, - volume: Math.round(close * 2500) - }); - } - return { candles15m, candles1h, candles1d }; -} -var cachedTop100Signals = []; -var lastTop100FetchTime = 0; -async function fetchTop100Cryptos() { - const now = Date.now(); - if (cachedTop100Signals.length >= 80 && now - lastTop100FetchTime < 3e4) { - return cachedTop100Signals; - } - try { - const url = "https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&order=market_cap_desc&per_page=120&page=1&sparkline=true"; - const res = await fetch(url, { - headers: { "User-Agent": "GodProtocol-Quantitative/4.0" }, - signal: AbortSignal.timeout(1e4) - }); - if (!res.ok) { - throw new Error(`CoinGecko status: ${res.status}`); - } - const rawCoins = await res.json(); - const stableSymbols = /* @__PURE__ */ new Set([ - "USDT", - "USDC", - "USDS", - "DAI", - "FDUSD", - "USDE", - "PYUSD", - "TUSD", - "USDD", - "FRAX", - "USD0", - "BUSD", - "EURC", - "GUSD", - "USDG" - ]); - const filteredTop100 = rawCoins.filter((c) => !stableSymbols.has(c.symbol.toUpperCase())).slice(0, 100); - const generatedSignals = []; - for (let index = 0; index < filteredTop100.length; index++) { - const coin = filteredTop100[index]; - const symbol = `${coin.symbol.toUpperCase()}/USDT`; - const name = coin.name; - const currentPrice = Number(coin.current_price) || 1e-4; - const change24h = Number(Number(coin.price_change_percentage_24h || 0).toFixed(2)); - const volume24h = Math.round(coin.total_volume || 0); - const marketCap = coin.market_cap || 0; - const marketCapRank = coin.market_cap_rank || index + 1; - let decimals = 2; - if (currentPrice < 1e-3) decimals = 6; - else if (currentPrice < 1) decimals = 4; - else if (currentPrice < 10) decimals = 3; - const sparkPrices = coin.sparkline_in_7d?.price || []; - const change7d = sparkPrices.length > 0 && sparkPrices[0] > 0 ? Number(((currentPrice - sparkPrices[0]) / sparkPrices[0] * 100).toFixed(2)) : Number(Number(coin.price_change_percentage_7d_in_currency || change24h * 2.8).toFixed(2)); - const { candles15m, candles1h, candles1d } = generateCandlesFromSparkline( - sparkPrices.length >= 10 ? sparkPrices : [currentPrice * 0.98, currentPrice * 0.99, currentPrice], - now - ); - const closes2 = sparkPrices.length >= 14 ? sparkPrices : [currentPrice]; - const ema9 = Number(calculateEMA(closes2, 9).toFixed(decimals)); - const ema21 = Number(calculateEMA(closes2, 21).toFixed(decimals)); - const ema50 = Number(calculateEMA(closes2, 50).toFixed(decimals)); - const ema200 = Number(calculateEMA(closes2, 200).toFixed(decimals)); - const isBullishCross = ema9 > ema21; - const rsiValue = calculateRSI(closes2, 14); - const rawAtr = calculateATR(candles15m, 14); - const atrValue = rawAtr > 0 ? Number(rawAtr.toFixed(decimals)) : Number((currentPrice * 0.018).toFixed(decimals)); - const htfTrend = change24h > 1 && currentPrice >= ema50 ? "ALTA (BULLISH)" : change24h < -1 && currentPrice <= ema50 ? "BAIXA (BEARISH)" : "LATERAL"; - const patterns = [ - "Ombro-Cabe\xE7a-Ombro Invertido (Bullish)", - "Fundo Duplo em Suporte Institucional", - "Canal de Reacumula\xE7\xE3o Rompendo para Cima", - "Ombro-Cabe\xE7a-Ombro Tradicional (Bearish)", - "Topo Duplo com Exaust\xE3o de Compradores", - "Tri\xE2ngulo Ascendente em Compress\xE3o de Volatilidade", - "Bandeira de Alta p\xF3s-Impulso Institucional" - ]; - const mtfPattern = isBullishCross ? patterns[0] : patterns[3]; - let currentPhase = "Consolida\xE7\xE3o Neutra"; - if (isBullishCross && htfTrend === "ALTA (BULLISH)") currentPhase = "Reexpans\xE3o (Markup)"; - else if (rsiValue < 38) currentPhase = "Acumula\xE7\xE3o (Spring/Test)"; - else if (!isBullishCross && htfTrend === "BAIXA (BEARISH)") currentPhase = "Markdown (Queda Livre)"; - else if (rsiValue > 68) currentPhase = "Distribui\xE7\xE3o (UTAD)"; - let taScore = 60; - if (isBullishCross) taScore += 16; - if (currentPrice > ema50) taScore += 12; - if (rsiValue >= 40 && rsiValue <= 60) taScore += 10; - taScore = Math.min(95, Math.max(45, taScore)); - let smcScore = 65; - const sweepDetected = rsiValue > 65 || rsiValue < 35; - if (sweepDetected) smcScore += 15; - smcScore = Math.min(95, Math.max(45, smcScore)); - let wyckoffScore = 62; - if (currentPhase.includes("Acumula\xE7\xE3o") || currentPhase.includes("Reexpans\xE3o")) wyckoffScore += 18; - wyckoffScore = Math.min(95, Math.max(45, wyckoffScore)); - let fundingRate = 8e-3; - if (change24h > 4) fundingRate = 0.022; - else if (change24h < -4) fundingRate = -6e-3; - let sentimentScore = 65; - if (fundingRate >= -5e-3 && fundingRate <= 0.015) sentimentScore += 15; - else if (fundingRate < -5e-3) sentimentScore += 20; - else sentimentScore -= 10; - sentimentScore = Math.min(95, Math.max(45, sentimentScore)); - let rawConfluence = Math.round( - taScore * 0.25 + smcScore * 0.3 + wyckoffScore * 0.25 + sentimentScore * 0.2 - ); - if (rawConfluence >= 82 && (isBullishCross || currentPrice > ema50)) { - if (index <= 5 || marketCapRank <= 10 || Math.abs(change24h) >= 3) { - rawConfluence = Math.min(96, Math.max(90, rawConfluence + 8)); - } - } - const confluenceAverage = Math.min(98, Math.max(45, rawConfluence)); - const isLongSetup = (htfTrend === "ALTA (BULLISH)" || isBullishCross) && currentPrice >= ema50 * 0.99; - const isShortSetup = (htfTrend === "BAIXA (BEARISH)" || !isBullishCross) && currentPrice <= ema50 * 1.01; - let decision = "AGUARDAR"; - let stopLoss = 0; - let riskReward = 0; - let breakevenTrigger = 0; - const passesConfidence = confluenceAverage >= 75; - if (passesConfidence && isLongSetup) { - decision = "COMPRA"; - stopLoss = Number((currentPrice - atrValue * 1.5).toFixed(decimals)); - const risk = Math.max(currentPrice - stopLoss, currentPrice * 5e-3); - let calculatedRR = 2; - if (confluenceAverage >= 90) { - const highRROptions = [5, 6, 7.5, 8, 10]; - calculatedRR = highRROptions[(marketCapRank || 1) % highRROptions.length]; - } else if (confluenceAverage >= 82) { - const medRROptions = [3, 3.5, 4, 4.5, 5]; - calculatedRR = medRROptions[(marketCapRank || 1) % medRROptions.length]; - } else { - calculatedRR = Number((2 + (marketCapRank || 1) * 3 % 11 * 0.1).toFixed(1)); - } - riskReward = calculatedRR; - breakevenTrigger = Number((currentPrice + risk * 1).toFixed(decimals)); - } else if (passesConfidence && isShortSetup) { - decision = "VENDA"; - stopLoss = Number((currentPrice + atrValue * 1.5).toFixed(decimals)); - const risk = Math.max(stopLoss - currentPrice, currentPrice * 5e-3); - let calculatedRR = 2; - if (confluenceAverage >= 90) { - const highRROptions = [5, 6, 7.5, 8, 10]; - calculatedRR = highRROptions[(marketCapRank || 1) % highRROptions.length]; - } else if (confluenceAverage >= 82) { - const medRROptions = [3, 3.5, 4, 4.5, 5]; - calculatedRR = medRROptions[(marketCapRank || 1) % medRROptions.length]; - } else { - calculatedRR = Number((2 + (marketCapRank || 1) * 5 % 11 * 0.1).toFixed(1)); - } - riskReward = calculatedRR; - breakevenTrigger = Number((currentPrice - risk * 1).toFixed(decimals)); - } else { - decision = "AGUARDAR"; - const waitRR = Number((1.2 + (marketCapRank || 1) * 3 % 9 * 0.1).toFixed(2)); - riskReward = waitRR; - stopLoss = Number((currentPrice * 0.98).toFixed(decimals)); - breakevenTrigger = Number((currentPrice * 1.015).toFixed(decimals)); - } - const fiboTargets = calculateFibonacciTargets(currentPrice, stopLoss, decision, currentPrice, decimals); - const takeProfit1 = fiboTargets[0].price; - const takeProfit2 = fiboTargets[1].price; - const takeProfit3 = fiboTargets[2].price; - const takeProfit4 = fiboTargets[3].price; - const takeProfit5 = fiboTargets[4].price; - const passedFilter = decision !== "AGUARDAR" && confluenceAverage >= 75 && riskReward >= 2; - generatedSignals.push({ - id: `sig-${coin.symbol.toUpperCase()}-${now}`, - symbol, - name, - marketCapRank, - marketCap, - currentPrice, - change24h, - change7d, - sparkline7d: sparkPrices.length >= 7 ? sparkPrices : candles1d.map((c) => c.close), - volume24h, - timestamp: now, - timeStr: getBrasiliaTimeStr(now, true), - decision, - confidence: confluenceAverage, - riskReward, - entryPrice: currentPrice, - stopLoss, - takeProfit1, - takeProfit2, - takeProfit3, - takeProfit4, - takeProfit5, - fibonacciTargets: fiboTargets, - breakevenTrigger, - atrValue, - passedFilter, - tripleScreen: { - htf: { - timeframe: "1D (Di\xE1rio)", - trend: htfTrend, - ema50, - ema200, - description: `Tend\xEAncia ${htfTrend}. Rank #${marketCapRank} por Market Cap com varia\xE7\xE3o 24h de ${change24h > 0 ? "+" : ""}${change24h}%.`, - candles: candles1d - }, - mtf: { - timeframe: "1H (1 Hora)", - pattern: mtfPattern, - ema50, - dynamicSupportResistance: currentPrice > ema50 ? "Suporte na EMA 50" : "Resist\xEAncia na EMA 50", - candles: candles1h - }, - ltf: { - timeframe: "15m (15 Minutos)", - ema9, - ema21, - emaCross: isBullishCross ? "Cruzamento de Alta (9 > 21)" : "Cruzamento de Baixa (9 < 21)", - rsi: rsiValue, - rsiStatus: rsiValue > 70 ? "Sobrecomprado (>70)" : rsiValue < 30 ? "Sobrevendido (<30)" : "Momentum Neutro/Saud\xE1vel", - atr: atrValue, - candles: candles15m - } - }, - fourPillars: { - classicTA: { - score: taScore, - status: taScore >= 75 ? "Favor\xE1vel" : taScore >= 60 ? "Neutro" : "Desfavor\xE1vel", - emaAlignment: isBullishCross ? "Alta (9>21>50>200)" : "Baixa (9<21<50<200)", - rsiValue, - rsiInterpretation: rsiValue > 60 ? "Press\xE3o compradora sem exaust\xE3o" : "Zona neutra de consolida\xE7\xE3o", - patternDetected: mtfPattern, - details: `EMA 9 ($${ema9}) e EMA 21 ($${ema21}) calculadas sobre hist\xF3rico de pre\xE7os hor\xE1rio.` - }, - smc: { - score: smcScore, - status: smcScore >= 75 ? "Favor\xE1vel" : smcScore >= 60 ? "Neutro" : "Desfavor\xE1vel", - liquiditySweep: { - detected: sweepDetected, - type: sweepDetected ? "Sell Side Liquidity (SSL) Capturada" : "Nenhum Sweep Recente", - priceLevel: Number((currentPrice * 0.985).toFixed(decimals)) - }, - imbalanceFVG: { - present: true, - zone: `${(currentPrice * 0.992).toFixed(decimals)} - ${(currentPrice * 0.996).toFixed(decimals)}` - }, - orderBlock: { - type: isLongSetup ? "Bullish OB" : "Bearish OB", - zone: `${(currentPrice * 0.988).toFixed(decimals)} (1H Institucional)` - }, - details: "Detec\xE7\xE3o institucional de Fair Value Gap e varredura de liquidez em n\xEDveis chave." - }, - wyckoff: { - score: wyckoffScore, - status: wyckoffScore >= 75 ? "Favor\xE1vel" : wyckoffScore >= 60 ? "Neutro" : "Desfavor\xE1vel", - currentPhase, - effortVsResult: "Volume Alto com Absor\xE7\xE3o (Institucional Atuando)", - volumeRatio: 1.45, - details: `Fase de ${currentPhase} confirmada por fluxo e VSA institucional.` - }, - sentiment: { - score: sentimentScore, - status: sentimentScore >= 75 ? "Favor\xE1vel" : sentimentScore >= 60 ? "Neutro" : "Desfavor\xE1vel", - openInterest: Math.round(currentPrice * 18e4), - oi24hChange: Number((change24h * 1.2).toFixed(2)), - oiInterpretation: change24h > 0 ? "Dinheiro Novo Entrando (Confirma Tend\xEAncia)" : "Fechamento de Posi\xE7\xF5es (Exaust\xE3o)", - fundingRate, - fundingSentiment: fundingRate > 0.02 ? "Euforia Excessiva (Perigo de Queda)" : fundingRate < -5e-3 ? "P\xE2nico / Negativo (Oportunidade de Compra)" : "Taxa Neutra e Saud\xE1vel", - longShortRatio: 1.25, - details: `Funding estimado em ${(fundingRate * 100).toFixed(3)}%. Sentimento do mercado de derivativos.` - }, - confluenceAverage - }, - aiThesis: { - summary: passedFilter ? `Setup de ${decision} para ${symbol} (#${marketCapRank}) validado com ${confluenceAverage}% de conflu\xEAncia institucional e R/R 1:${riskReward}.` : `Crit\xE9rios do God Protocol v2026 pendentes (${confluenceAverage}% < 75% ou sem alinhamento R/R). Recomendado AGUARDAR.`, - institutionalContext: `HTF Di\xE1rio em ${htfTrend}. Moeda do Top 100 Market Cap (#${marketCapRank}). Stop ATR em $${stopLoss}.`, - primaryCatalyst: `Estrutura de m\xE9dias e RSI(${rsiValue}) em 15m alinhados \xE0 sustenta\xE7\xE3o de EMA 50 em MTF (1H).`, - riskWarning: `Controle r\xEDgido: limitar exposi\xE7\xE3o a 1% do capital total. Alerta em Hor\xE1rio de Bras\xEDlia (BRT).`, - verdict: passedFilter ? "EXECUTAR" : "AGUARDAR", - source: "Agente Quantitativo Local" - }, - squeezeBreakout: (() => { - const period = Math.min(sparkPrices.length, 20); - const recentSpark = sparkPrices.length >= period ? sparkPrices.slice(-period) : [currentPrice]; - const sma20 = recentSpark.reduce((a, b) => a + b, 0) / (recentSpark.length || 1); - const variance = recentSpark.reduce((a, b) => a + Math.pow(b - sma20, 2), 0) / (recentSpark.length || 1); - const stdDev = Math.sqrt(variance) || currentPrice * 0.015; - const upperBB = sma20 + 2 * stdDev; - const lowerBB = Math.max(1e-4, sma20 - 2 * stdDev); - const bbWidth = Number(((upperBB - lowerBB) / sma20 * 100).toFixed(2)); - const upperKC = sma20 + 1.5 * atrValue; - const lowerKC = Math.max(1e-4, sma20 - 1.5 * atrValue); - const kcWidth = Number(((upperKC - lowerKC) / sma20 * 100).toFixed(2)); - const isSqueezeOn = upperBB < upperKC && lowerBB > lowerKC; - const isSqueezeFired = change24h >= 4.5 && bbWidth < 7 || change24h >= 6.5; - let squeezeState = "NORMAL"; - let stateLabel = "VOLATILIDADE REGULAR"; - let urgency = "BAIXA"; - let explosionScore = 32; - const catalysts = []; - if (isSqueezeFired || change24h >= 7) { - squeezeState = "IGNICAO_DISPARADA"; - stateLabel = "DISPARO DE EXPLOS\xC3O (BREAKOUT 8%+)"; - urgency = "CRITICA"; - explosionScore = Math.min(98, 88 + marketCapRank % 11); - catalysts.push("Expans\xE3o violenta das Bandas de Bollinger com gatilho de breakout"); - catalysts.push(`Rompimento altista com varia\xE7\xE3o 24h de +${change24h}%`); - } else if (isSqueezeOn) { - squeezeState = "SQUEEZE_ATIVO"; - stateLabel = "COMPRESS\xC3O M\xC1XIMA (SQUEEZE ATIVO)"; - urgency = bbWidth < 3.5 ? "ALTA" : "MODERADA"; - explosionScore = Math.min(87, 72 + Math.round((10 - bbWidth) * 2)); - catalysts.push(`Bandas de Bollinger estranguladas dentro do Canal Keltner (BandWidth: ${bbWidth}%)`); - catalysts.push("Ac\xFAmulo intenso de volatilidade: energia prestes a ser liberada"); - } else if (change24h > 2.5) { - squeezeState = "EXPANSAO_ALTA"; - stateLabel = "EXPANS\xC3O DE MOMENTUM"; - urgency = "MODERADA"; - explosionScore = Math.min(74, 58 + Math.round(change24h * 1.5)); - catalysts.push("Fluxo comprador dominante em andamento"); - } - let shortSqueezeRisk = "MODERADO"; - if (fundingRate <= 1e-3) { - shortSqueezeRisk = "EXTREMO"; - catalysts.push(`Taxa de funding negativa/zerada (${(fundingRate * 100).toFixed(3)}%): Vendedores expostos a Short Squeeze`); - } else if (fundingRate <= 6e-3) { - shortSqueezeRisk = "ALTO"; - } - return { - isSqueezeOn, - isSqueezeFired, - squeezeBarsCount: isSqueezeOn ? Math.max(3, Math.min(18, Math.round(14 - bbWidth))) : 1, - state: squeezeState, - stateLabel, - explosionScore, - urgency, - bollingerBandWidth: bbWidth, - keltnerWidth: kcWidth, - compressionPercent: Math.min(100, Math.max(10, Math.round((1 - bbWidth / Math.max(kcWidth, 0.1)) * 100 + 50))), - momentumDirection: currentPrice >= sma20 ? "ALTA" : "BAIXA", - shortSqueezeRisk, - estimatedTarget8Pct: Number((currentPrice * 1.082).toFixed(decimals)), - estimatedTarget15Pct: Number((currentPrice * 1.154).toFixed(decimals)), - recommendedStopLoss: Number(Math.min(lowerBB, currentPrice * 0.978).toFixed(decimals)), - catalysts - }; - })() - }); - } - if (generatedSignals.length > 0) { - cachedTop100Signals = generatedSignals; - lastTop100FetchTime = now; - await syncRealTimePrices().catch(() => { - }); - } - return cachedTop100Signals; - } catch (err) { - console.warn("CoinGecko Top 100 fetch failed, returning cached signals:", err.message); - await syncRealTimePrices().catch(() => { - }); - return cachedTop100Signals; - } -} -async function syncRealTimePrices() { - if (!cachedTop100Signals || cachedTop100Signals.length === 0) { - return cachedTop100Signals; - } - try { - const res = await fetch("https://api.binance.us/api/v3/ticker/price", { - headers: { "User-Agent": "GodProtocol-Ticker/4.0" }, - signal: AbortSignal.timeout(4e3) - }); - if (!res.ok) return cachedTop100Signals; - const list = await res.json(); - const priceMap = /* @__PURE__ */ new Map(); - for (const item of list) { - const p = parseFloat(item.price); - if (!isNaN(p) && p > 0) { - priceMap.set(item.symbol, p); - } - } - const now = Date.now(); - const brasiliaTime = getBrasiliaTimeStr(now, true); - for (const signal of cachedTop100Signals) { - const cleanSym = signal.symbol.replace("/", "").toUpperCase(); - let livePrice = priceMap.get(cleanSym); - if (!livePrice && cleanSym.endsWith("USDT")) { - livePrice = priceMap.get(cleanSym.replace("USDT", "USD")); - } - if (livePrice && livePrice > 0) { - let decimals = 2; - if (livePrice < 1e-3) decimals = 6; - else if (livePrice < 1) decimals = 4; - else if (livePrice < 10) decimals = 3; - signal.currentPrice = Number(livePrice.toFixed(decimals)); - signal.timestamp = now; - signal.timeStr = brasiliaTime; - const entry = signal.entryPrice || signal.currentPrice; - const stop = signal.stopLoss || entry * 0.98; - signal.fibonacciTargets = calculateFibonacciTargets( - entry, - stop, - signal.decision, - signal.currentPrice, - decimals - ); - signal.takeProfit1 = signal.fibonacciTargets[0].price; - signal.takeProfit2 = signal.fibonacciTargets[1].price; - signal.takeProfit3 = signal.fibonacciTargets[2].price; - signal.takeProfit4 = signal.fibonacciTargets[3].price; - signal.takeProfit5 = signal.fibonacciTargets[4].price; - } - } - return cachedTop100Signals; - } catch (err) { - return cachedTop100Signals; - } -} - -// server/market/marketCache.ts -var cache = /* @__PURE__ */ new Map(); -var DEFAULT_TTL_MS = 15e3; -var MAX_ENTRIES = 100; -function normalizeSymbol(symbol) { - return symbol.trim().toUpperCase(); -} -function getCachedMarketSnapshot(symbol) { - const key = normalizeSymbol(symbol); - const entry = cache.get(key); - if (!entry) return null; - if (Date.now() >= entry.expiresAt) { - cache.delete(key); - return null; - } - return entry.snapshot; -} -function setCachedMarketSnapshot(symbol, snapshot, ttlMs = DEFAULT_TTL_MS) { - const key = normalizeSymbol(symbol); - cache.delete(key); - cache.set(key, { - snapshot, - expiresAt: Date.now() + Math.max(ttlMs, 1e3) - }); - while (cache.size > MAX_ENTRIES) { - const oldestKey = cache.keys().next().value; - if (!oldestKey) break; - cache.delete(oldestKey); - } -} - -// server/market/exchangeClient.ts -var DEFAULT_TIMEOUT_MS = 1e4; -function withTimeout(signal, timeoutMs = DEFAULT_TIMEOUT_MS) { - if (signal) return signal; - return AbortSignal.timeout(timeoutMs); -} -function normalizeBinanceSymbol(symbol) { - return symbol.replace("/", "").toUpperCase(); -} -function normalizeOkxInstrument(symbol) { - return symbol.replace("/", "-").toUpperCase(); -} -function normalizeOkxBar(interval) { - const bars = { - "15m": "15m", - "1h": "1H", - "4h": "4H", - "1d": "1D" - }; - return bars[interval] ?? interval; -} -async function fetchJson(url, signal) { - const response = await fetch(url, { - headers: { Accept: "application/json" }, - signal: withTimeout(signal) - }); - if (!response.ok) { - throw new Error(`Exchange HTTP ${response.status}: ${response.statusText}`); - } - return response.json(); -} -async function fetchBinanceCandles(options) { - const symbol = normalizeBinanceSymbol(options.symbol); - const limit = Math.min(Math.max(options.limit ?? 500, 1), 1e3); - const url = new URL("https://api.binance.com/api/v3/klines"); - url.searchParams.set("symbol", symbol); - url.searchParams.set("interval", options.interval); - url.searchParams.set("limit", String(limit)); - const data = await fetchJson(url.toString(), options.signal); - if (!Array.isArray(data)) throw new Error("Binance returned an invalid kline payload"); - return data.map((row) => ({ - timestamp: Number(row[0]), - open: Number(row[1]), - high: Number(row[2]), - low: Number(row[3]), - close: Number(row[4]), - volume: Number(row[5]) - })); -} -async function fetchOkxCandles(options) { - const instId = normalizeOkxInstrument(options.symbol); - const limit = Math.min(Math.max(options.limit ?? 500, 1), 1e3); - const url = new URL("https://www.okx.com/api/v5/market/candles"); - url.searchParams.set("instId", instId); - url.searchParams.set("bar", normalizeOkxBar(options.interval)); - url.searchParams.set("limit", String(limit)); - const payload = await fetchJson(url.toString(), options.signal); - if (payload.code !== "0" || !Array.isArray(payload.data)) { - throw new Error(`OKX returned an invalid candle payload for ${instId}: ${payload.msg ?? "unknown error"}`); - } - return payload.data.map((row) => ({ - timestamp: Number(row[0]), - open: Number(row[1]), - high: Number(row[2]), - low: Number(row[3]), - close: Number(row[4]), - volume: Number(row[5]) - })).sort((a, b) => a.timestamp - b.timestamp); -} -async function fetchCandlesWithFallback(options, preferred = "binance") { - const order = preferred === "binance" ? ["binance", "okx"] : ["okx", "binance"]; - const errors = []; - for (const exchange of order) { - try { - const candles = exchange === "binance" ? await fetchBinanceCandles(options) : await fetchOkxCandles(options); - if (candles.length === 0) throw new Error(`${exchange} returned no candles`); - return { exchange, candles }; - } catch (error) { - errors.push(`${exchange}: ${String(error)}`); - } - } - throw new Error(`All candle providers failed. ${errors.join(" | ")}`); -} - -// server/market/candleService.ts -var INTERVAL_MAP = { - "15m": "15m", - "1h": "1h", - "4h": "4h", - "1d": "1d" -}; -function toCandle(raw) { - return { - timestamp: raw.timestamp, - timeStr: new Date(raw.timestamp).toISOString(), - open: raw.open, - high: raw.high, - low: raw.low, - close: raw.close, - volume: raw.volume - }; -} -function validateCandle(candle) { - return Number.isFinite(candle.timestamp) && Number.isFinite(candle.open) && Number.isFinite(candle.high) && Number.isFinite(candle.low) && Number.isFinite(candle.close) && Number.isFinite(candle.volume) && candle.high >= Math.max(candle.open, candle.close, candle.low) && candle.low <= Math.min(candle.open, candle.close, candle.high) && candle.volume >= 0; -} -async function fetchRealCandles(symbol, timeframe, limit = 500, preferredExchange = "binance") { - const result = await fetchCandlesWithFallback({ - symbol, - interval: INTERVAL_MAP[timeframe], - limit - }, preferredExchange); - const candles = result.candles.map(toCandle).filter(validateCandle).sort((a, b) => a.timestamp - b.timestamp); - const deduplicated = candles.filter((candle, index) => index === 0 || candle.timestamp !== candles[index - 1].timestamp); - return { exchange: result.exchange, candles: deduplicated }; -} -async function fetchMultiTimeframeCandles(symbol, limits = {}) { - const timeframes = ["15m", "1h", "4h", "1d"]; - const results = await Promise.all( - timeframes.map(async (timeframe) => [ - timeframe, - await fetchRealCandles(symbol, timeframe, limits[timeframe] ?? 500) - ]) - ); - return Object.fromEntries(results); -} - -// server/market/timeframeService.ts -async function loadMarketSnapshot(symbol, limits = {}) { - const raw = await fetchMultiTimeframeCandles(symbol, limits); - const series = {}; - for (const timeframe of ["15m", "1h", "4h", "1d"]) { - const candles = raw[timeframe].candles; - series[timeframe] = { - timeframe, - exchange: raw[timeframe].exchange, - candles, - latestTimestamp: candles.at(-1)?.timestamp ?? 0 - }; - } - return { - symbol, - fetchedAt: Date.now(), - series - }; -} - -// server/market/marketSnapshotService.ts -async function getMarketSnapshot(symbol, options = {}) { - if (!options.forceRefresh) { - const cached = getCachedMarketSnapshot(symbol); - if (cached) return cached; - } - const snapshot = await loadMarketSnapshot(symbol, options.limits); - setCachedMarketSnapshot(symbol, snapshot); - return snapshot; -} - -// server/indicators/technicalIndicators.ts -function closes(candles) { - return candles.map((c) => c.close).filter(Number.isFinite); -} -function ema(values, period) { - if (values.length === 0) return 0; - const seed = values.slice(0, Math.min(period, values.length)).reduce((a, b) => a + b, 0) / Math.min(period, values.length); - if (values.length <= period) return seed; - const multiplier = 2 / (period + 1); - let result = seed; - for (let i = period; i < values.length; i++) result = (values[i] - result) * multiplier + result; - return result; -} -function rsi(values, period = 14) { - if (values.length <= period) return 50; - let gains = 0; - let losses = 0; - for (let i = 1; i <= period; i++) { - const change = values[i] - values[i - 1]; - if (change >= 0) gains += change; - else losses -= change; - } - let avgGain = gains / period; - let avgLoss = losses / period; - for (let i = period + 1; i < values.length; i++) { - const change = values[i] - values[i - 1]; - avgGain = (avgGain * (period - 1) + Math.max(change, 0)) / period; - avgLoss = (avgLoss * (period - 1) + Math.max(-change, 0)) / period; - } - if (avgLoss === 0) return 100; - if (avgGain === 0) return 0; - const rs = avgGain / avgLoss; - return 100 - 100 / (1 + rs); -} -function atr(candles, period = 14) { - if (candles.length < 2) return 0; - const ranges = []; - for (let i = 1; i < candles.length; i++) { - const c = candles[i]; - const prev = candles[i - 1].close; - ranges.push(Math.max(c.high - c.low, Math.abs(c.high - prev), Math.abs(c.low - prev))); - } - const window = ranges.slice(-period); - return window.length ? window.reduce((a, b) => a + b, 0) / window.length : 0; -} -function bollinger(values, period = 20, deviations = 2) { - const window = values.slice(-period); - if (!window.length) return { middle: 0, upper: 0, lower: 0, widthPercent: 0 }; - const middle = window.reduce((a, b) => a + b, 0) / window.length; - const variance = window.reduce((sum, value) => sum + (value - middle) ** 2, 0) / window.length; - const std2 = Math.sqrt(variance); - const upper = middle + deviations * std2; - const lower = middle - deviations * std2; - return { middle, upper, lower, widthPercent: middle ? (upper - lower) / middle * 100 : 0 }; -} -function volumeSma(candles, period = 20) { - const values = candles.slice(-period).map((c) => c.volume).filter(Number.isFinite); - return values.length ? values.reduce((a, b) => a + b, 0) / values.length : 0; -} -function calculateIndicators(candles) { - const values = closes(candles); - const latest = values.at(-1) ?? 0; - const previous = values.at(-2) ?? latest; - const volumeAverage = volumeSma(candles, 20); - const latestVolume = candles.at(-1)?.volume ?? 0; - return { - ema9: ema(values, 9), - ema21: ema(values, 21), - ema50: ema(values, 50), - ema200: ema(values, 200), - rsi14: rsi(values, 14), - atr14: atr(candles, 14), - bollinger: bollinger(values, 20, 2), - volumeSma20: volumeAverage, - relativeVolume20: volumeAverage > 0 ? latestVolume / volumeAverage : 0, - momentumPercent: previous !== 0 ? (latest - previous) / previous * 100 : 0 - }; -} - -// server/structure/swingDetection.ts -function detectSwings(candles, options = {}) { - const leftBars = Math.max(1, options.leftBars ?? 3); - const rightBars = Math.max(1, options.rightBars ?? 3); - const minStrength = Math.max(0, options.minStrength ?? 0); - const swings = []; - if (candles.length < leftBars + rightBars + 1) return swings; - for (let i = leftBars; i < candles.length - rightBars; i += 1) { - const candle = candles[i]; - let isHigh = true; - let isLow = true; - let highStrength = 0; - let lowStrength = 0; - for (let j = i - leftBars; j <= i + rightBars; j += 1) { - if (j === i) continue; - if (candles[j].high > candle.high) isHigh = false; - if (candles[j].low < candle.low) isLow = false; - } - if (isHigh) { - for (let j = i - leftBars; j <= i + rightBars; j += 1) { - if (j !== i) highStrength += Math.max(0, candle.high - candles[j].high); - } - if (highStrength >= minStrength) { - swings.push({ index: i, timestamp: candle.timestamp, price: candle.high, type: "high", strength: highStrength }); - } - } - if (isLow) { - for (let j = i - leftBars; j <= i + rightBars; j += 1) { - if (j !== i) lowStrength += Math.max(0, candles[j].low - candle.low); - } - if (lowStrength >= minStrength) { - swings.push({ index: i, timestamp: candle.timestamp, price: candle.low, type: "low", strength: lowStrength }); - } - } - } - return swings; -} - -// server/structure/marketStructure.ts -function inferTrend(events) { - return events.at(-1)?.direction ?? "neutral"; -} -function detectStructureEvents(candles, swings) { - const events = []; - let trend = "neutral"; - const broken = /* @__PURE__ */ new Set(); - let highCursor = 0; - let lowCursor = 0; - for (let i = 0; i < candles.length; i += 1) { - while (highCursor < swings.length && swings[highCursor].index < i) highCursor += 1; - while (lowCursor < swings.length && swings[lowCursor].index < i) lowCursor += 1; - const priorHighs = swings.slice(0, highCursor).filter((s) => s.type === "high" && !broken.has(s.index)); - const priorLows = swings.slice(0, lowCursor).filter((s) => s.type === "low" && !broken.has(s.index)); - const high = priorHighs.at(-1); - const low = priorLows.at(-1); - const candle = candles[i]; - if (high && candle.close > high.price) { - const direction = "bullish"; - events.push({ type: trend === "bearish" ? "CHoCH" : "BOS", direction, index: i, timestamp: candle.timestamp, level: high.price, swingIndex: high.index }); - trend = direction; - broken.add(high.index); - } else if (low && candle.close < low.price) { - const direction = "bearish"; - events.push({ type: trend === "bullish" ? "CHoCH" : "BOS", direction, index: i, timestamp: candle.timestamp, level: low.price, swingIndex: low.index }); - trend = direction; - broken.add(low.index); - } - } - return events; -} -function detectSweeps(candles, swings, lookback = 80) { - const sweeps = []; - for (let i = 0; i < candles.length; i += 1) { - const candle = candles[i]; - const high = [...swings].reverse().find((s) => s.type === "high" && s.index < i && i - s.index <= lookback); - const low = [...swings].reverse().find((s) => s.type === "low" && s.index < i && i - s.index <= lookback); - if (high && candle.high > high.price && candle.close < high.price) { - sweeps.push({ type: "high", index: i, timestamp: candle.timestamp, level: high.price, wickExtreme: candle.high, close: candle.close, swingIndex: high.index }); - } - if (low && candle.low < low.price && candle.close > low.price) { - sweeps.push({ type: "low", index: i, timestamp: candle.timestamp, level: low.price, wickExtreme: candle.low, close: candle.close, swingIndex: low.index }); - } - } - return sweeps; -} -function detectEqualLevels(swings, tolerancePercent = 15e-4) { - const result = []; - for (let i = 0; i < swings.length; i += 1) { - const base = swings[i]; - const matches = swings.slice(i + 1).filter((s) => s.type === base.type && Math.abs(s.price - base.price) / base.price <= tolerancePercent); - if (!matches.length) continue; - const all = [base, ...matches]; - const price = all.reduce((sum, s) => sum + s.price, 0) / all.length; - const swingIndices = all.map((s) => s.index); - if (!result.some((level) => level.type === base.type && level.swingIndices.some((idx) => swingIndices.includes(idx)))) { - result.push({ type: base.type, price, swingIndices, tolerance: price * tolerancePercent }); - } - } - return result; -} -function analyzeMarketStructure(candles) { - const swings = detectSwings(candles, { leftBars: 3, rightBars: 3 }); - const events = detectStructureEvents(candles, swings); - const sweeps = detectSweeps(candles, swings); - const equalLevels = detectEqualLevels(swings); - return { - trend: inferTrend(events), - swings, - events, - sweeps, - equalLevels, - latestEvent: events.at(-1) ?? null, - latestSweep: sweeps.at(-1) ?? null - }; -} - -// server/structure/smc.ts -function rangePercent(low, high) { - return low > 0 ? (high - low) / low * 100 : 0; -} -function displacementPercent(candle) { - const base = Math.max(Math.abs(candle.open), 1e-9); - return Math.abs(candle.close - candle.open) / base * 100; -} -function detectFVGs(candles, maxAge = 180) { - const result = []; - const start = Math.max(2, candles.length - maxAge); - for (let i = start; i < candles.length; i += 1) { - const left = candles[i - 2]; - const middle = candles[i - 1]; - const right = candles[i]; - if (!left || !middle || !right) continue; - if (right.low > left.high) { - const low = left.high; - const high = right.low; - const gapSize = rangePercent(low, high); - const filled = candles.slice(i + 1).some((c) => c.low <= low); - if (!filled && gapSize > 0) { - result.push({ - type: "bullish", - index: i, - timestamp: middle.timestamp, - low, - high, - midpoint: (low + high) / 2, - sizePercent: gapSize, - filled, - quality: Math.min(100, 35 + gapSize * 20) - }); - } - } - if (right.high < left.low) { - const low = right.high; - const high = left.low; - const gapSize = rangePercent(low, high); - const filled = candles.slice(i + 1).some((c) => c.high >= high); - if (!filled && gapSize > 0) { - result.push({ - type: "bearish", - index: i, - timestamp: middle.timestamp, - low, - high, - midpoint: (low + high) / 2, - sizePercent: gapSize, - filled, - quality: Math.min(100, 35 + gapSize * 20) - }); - } - } - } - return result.slice(-20); -} -function detectOrderBlocks(candles, structure, indicators) { - const result = []; - const atr2 = indicators.atr14; - if (!Number.isFinite(atr2) || atr2 <= 0) return result; - for (const event of structure.events.slice(-30)) { - if (event.type !== "BOS" && event.type !== "CHoCH") continue; - const displacement = candles[event.index]; - if (!displacement) continue; - const body2 = Math.abs(displacement.close - displacement.open); - if (body2 < atr2 * 0.8) continue; - let sourceIndex = -1; - for (let i = event.index - 1; i >= Math.max(0, event.index - 8); i -= 1) { - const c = candles[i]; - const opposite = event.direction === "bullish" ? c.close < c.open : c.close > c.open; - if (opposite) { - sourceIndex = i; - break; - } - } - if (sourceIndex < 0) continue; - const source = candles[sourceIndex]; - const low = source.low; - const high = source.high; - const mitigated = candles.slice(event.index + 1).some((c) => c.low <= high && c.high >= low); - if (mitigated) continue; - const displacementAtr = body2 / atr2; - const quality = Math.min(100, Math.round(35 + displacementAtr * 18 + (event.type === "CHoCH" ? 10 : 0))); - result.push({ - type: event.direction, - index: sourceIndex, - timestamp: source.timestamp, - low, - high, - midpoint: (low + high) / 2, - displacementPercent: displacementPercent(displacement), - mitigated: false, - quality, - sourceEvent: event.type - }); - } - return result.filter((block, index, all) => all.findIndex((x) => x.index === block.index && x.type === block.type) === index).slice(-15); -} -function detectBreakers(candles, structure) { - return structure.events.filter((e) => e.type === "CHoCH").slice(-10).map((event) => { - const source = candles[event.swingIndex]; - return source ? { - type: event.direction, - sourceIndex: event.swingIndex, - timestamp: source.timestamp, - low: source.low, - high: source.high, - midpoint: (source.low + source.high) / 2 - } : null; - }).filter((x) => x !== null); -} -function detectPremiumDiscount(candles, swings) { - const recentHigh = [...swings].reverse().find((s) => s.type === "high"); - const recentLow = [...swings].reverse().find((s) => s.type === "low"); - const currentPrice = candles.at(-1)?.close ?? 0; - if (!recentHigh || !recentLow || recentHigh.price <= recentLow.price || !currentPrice) return null; - const equilibrium = (recentHigh.price + recentLow.price) / 2; - const positionPercent = (currentPrice - recentLow.price) / (recentHigh.price - recentLow.price) * 100; - return { - swingHigh: recentHigh.price, - swingLow: recentLow.price, - equilibrium, - currentPrice, - zone: positionPercent > 55 ? "premium" : positionPercent < 45 ? "discount" : "equilibrium", - positionPercent - }; -} -function detectLiquidityPools(structure) { - const pools = []; - for (const level of structure.equalLevels) { - pools.push({ - type: level.type === "high" ? "buy-side" : "sell-side", - price: level.price, - source: level.type === "high" ? "equal-highs" : "equal-lows", - strength: level.swingIndices.length - }); - } - for (const swing of structure.swings.slice(-20)) { - pools.push({ - type: swing.type === "high" ? "buy-side" : "sell-side", - price: swing.price, - source: swing.type === "high" ? "swing-high" : "swing-low", - strength: Math.max(1, swing.strength) - }); - } - return pools.slice(-30); -} -function inferBias(structure, pd, fvg, blocks, latestSweep) { - let score2 = 0; - if (structure.trend === "bullish") score2 += 2; - if (structure.trend === "bearish") score2 -= 2; - if (pd?.zone === "discount") score2 += 1; - if (pd?.zone === "premium") score2 -= 1; - if (fvg.at(-1)?.type === "bullish") score2 += 1; - if (fvg.at(-1)?.type === "bearish") score2 -= 1; - if (blocks.at(-1)?.type === "bullish") score2 += 2; - if (blocks.at(-1)?.type === "bearish") score2 -= 2; - if (latestSweep?.type === "low") score2 += 1; - if (latestSweep?.type === "high") score2 -= 1; - return score2 >= 2 ? "bullish" : score2 <= -2 ? "bearish" : "neutral"; -} -function analyzeSMC(candles, structure, indicators) { - const fairValueGaps = detectFVGs(candles); - const orderBlocks = detectOrderBlocks(candles, structure, indicators); - const breakerBlocks = detectBreakers(candles, structure); - const premiumDiscount = detectPremiumDiscount(candles, structure.swings); - const liquidityPools = detectLiquidityPools(structure); - const latestSweep = structure.latestSweep; - return { - fairValueGaps, - orderBlocks, - breakerBlocks, - premiumDiscount, - liquidityPools, - latestSweep, - bias: inferBias(structure, premiumDiscount, fairValueGaps, orderBlocks, latestSweep) - }; -} - -// server/derivatives/derivativesClient.ts -var TIMEOUT_MS = 1e4; -function normalizeSymbol2(symbol) { - return symbol.replace("/", "").toUpperCase(); -} -function normalizeOkxSwapInstrument(symbol) { - const normalized = symbol.replace("/", "-").toUpperCase(); - return normalized.endsWith("-SWAP") ? normalized : `${normalized}-SWAP`; -} -async function getJson(url) { - const response = await fetch(url, { - headers: { Accept: "application/json" }, - signal: AbortSignal.timeout(TIMEOUT_MS) - }); - if (!response.ok) throw new Error(`HTTP ${response.status}: ${response.statusText}`); - return response.json(); -} -async function fetchBinanceDerivativesSnapshot(symbol) { - const normalized = normalizeSymbol2(symbol); - const base = "https://fapi.binance.com"; - const [funding, oi, ratio] = await Promise.all([ - getJson(`${base}/fapi/v1/premiumIndex?symbol=${normalized}`), - getJson(`${base}/fapi/v1/openInterest?symbol=${normalized}`), - getJson(`${base}/futures/data/globalLongShortAccountRatio?symbol=${normalized}&period=5m&limit=1`) - ]); - const ratioRow = Array.isArray(ratio) ? ratio[0] : null; - const longShortRatio = ratioRow && Number.isFinite(Number(ratioRow.longShortRatio)) ? Number(ratioRow.longShortRatio) : null; - const longAccountRatio = ratioRow && Number.isFinite(Number(ratioRow.longAccount)) ? Number(ratioRow.longAccount) : null; - const shortAccountRatio = ratioRow && Number.isFinite(Number(ratioRow.shortAccount)) ? Number(ratioRow.shortAccount) : null; - const fundingRate = Number.isFinite(Number(funding?.lastFundingRate)) ? Number(funding.lastFundingRate) : null; - const openInterest = Number.isFinite(Number(oi?.openInterest)) ? Number(oi.openInterest) : null; - const markPrice = Number.isFinite(Number(funding?.markPrice)) ? Number(funding.markPrice) : null; - return { - exchange: "binance-futures", - symbol: normalized, - timestamp: Date.now(), - fundingRate, - fundingTime: Number.isFinite(Number(funding?.nextFundingTime)) ? Number(funding.nextFundingTime) : null, - openInterest, - openInterestValue: openInterest !== null && markPrice !== null ? openInterest * markPrice : null, - longShortRatio, - longAccountRatio, - shortAccountRatio - }; -} -async function fetchOkxDerivativesSnapshot(symbol) { - const instId = normalizeOkxSwapInstrument(symbol); - const base = "https://www.okx.com"; - const [funding, openInterestResponse] = await Promise.all([ - getJson(`${base}/api/v5/public/funding-rate?instId=${encodeURIComponent(instId)}`), - getJson(`${base}/api/v5/public/open-interest?instType=SWAP&instId=${encodeURIComponent(instId)}`) - ]); - if (funding?.code !== "0" || !Array.isArray(funding?.data) || !funding.data[0]) { - throw new Error(`OKX funding-rate returned an invalid payload for ${instId}`); - } - if (openInterestResponse?.code !== "0" || !Array.isArray(openInterestResponse?.data) || !openInterestResponse.data[0]) { - throw new Error(`OKX open-interest returned an invalid payload for ${instId}`); - } - const fundingRow = funding.data[0]; - const oiRow = openInterestResponse.data[0]; - const fundingRate = Number.isFinite(Number(fundingRow.fundingRate)) ? Number(fundingRow.fundingRate) : null; - const openInterest = Number.isFinite(Number(oiRow.oi)) ? Number(oiRow.oi) : null; - const openInterestValue = Number.isFinite(Number(oiRow.oiUsd)) ? Number(oiRow.oiUsd) : null; - return { - exchange: "okx-swap", - symbol: normalizeSymbol2(symbol), - timestamp: Date.now(), - fundingRate, - fundingTime: Number.isFinite(Number(fundingRow.fundingTime)) ? Number(fundingRow.fundingTime) : null, - openInterest, - openInterestValue, - longShortRatio: null, - longAccountRatio: null, - shortAccountRatio: null - }; -} -async function fetchDerivativesSnapshot(symbol) { - const errors = []; - try { - return await fetchBinanceDerivativesSnapshot(symbol); - } catch (error) { - errors.push(`Binance Futures: ${String(error)}`); - } - try { - return await fetchOkxDerivativesSnapshot(symbol); - } catch (error) { - errors.push(`OKX SWAP: ${String(error)}`); - } - throw new Error(`All derivatives providers failed. ${errors.join(" | ")}`); -} - -// server/derivatives/derivativesAnalysis.ts -function analyzeDerivatives(data) { - let score2 = 0; - const reasons = []; - if (data.fundingRate === null) { - reasons.push("Funding indispon\xEDvel"); - } else if (data.fundingRate > 3e-4) { - score2 -= 1; - reasons.push("Funding positivo elevado, favorece risco de longs congestionados"); - } else if (data.fundingRate < -3e-4) { - score2 += 1; - reasons.push("Funding negativo elevado, favorece risco de shorts congestionados"); - } else { - reasons.push("Funding pr\xF3ximo do neutro"); - } - if (data.longShortRatio === null) { - reasons.push("Long/Short indispon\xEDvel"); - } else if (data.longShortRatio > 1.2) { - score2 -= 1; - reasons.push("Contas posicionadas majoritariamente em long"); - } else if (data.longShortRatio < 0.83) { - score2 += 1; - reasons.push("Contas posicionadas majoritariamente em short"); - } else { - reasons.push("Posicionamento Long/Short equilibrado"); - } - const bias = score2 > 0 ? "bullish" : score2 < 0 ? "bearish" : "neutral"; - const fundingState = data.fundingRate === null ? "unavailable" : data.fundingRate > 1e-4 ? "positive" : data.fundingRate < -1e-4 ? "negative" : "neutral"; - const positioningState = data.longShortRatio === null ? "unavailable" : data.longShortRatio > 1.2 ? "long-heavy" : data.longShortRatio < 0.83 ? "short-heavy" : "balanced"; - return { bias, score: score2, fundingState, positioningState, reasons }; -} - -// server/divergence/divergenceEngine.ts -function localLow(values, i, radius = 3) { - if (i < radius || i >= values.length - radius) return false; - for (let j = 1; j <= radius; j++) if (values[i] >= values[i - j] || values[i] > values[i + j]) return false; - return true; -} -function localHigh(values, i, radius = 3) { - if (i < radius || i >= values.length - radius) return false; - for (let j = 1; j <= radius; j++) if (values[i] <= values[i - j] || values[i] < values[i + j]) return false; - return true; -} -function oscillatorSeries(candles, fallback) { - const closes2 = candles.map((c) => c.close); - const result = closes2.map(() => fallback); - let gains = 0; - let losses = 0; - for (let i = 1; i < closes2.length; i++) { - const change = closes2[i] - closes2[i - 1]; - gains = (gains * 13 + Math.max(change, 0)) / 14; - losses = (losses * 13 + Math.max(-change, 0)) / 14; - result[i] = losses === 0 ? 100 : 100 - 100 / (1 + gains / losses); - } - return result; -} -function buildPivots(candles, rsi2, kind) { - const values = candles.map((c) => kind === "low" ? c.low : c.high); - const pivots = []; - for (let i = 3; i < candles.length - 3; i++) { - const pivot = kind === "low" ? localLow(values, i) : localHigh(values, i); - if (pivot) pivots.push({ index: i, price: values[i], oscillator: rsi2[i], timestamp: candles[i].timestamp }); - } - return pivots; -} -function detectDivergences(candles, indicators) { - if (candles.length < 30) return { bullish: [], bearish: [], latest: null }; - const fallbackRsi = indicators?.rsi14 ?? 50; - const rsi2 = oscillatorSeries(candles, fallbackRsi); - const lows = buildPivots(candles, rsi2, "low"); - const highs = buildPivots(candles, rsi2, "high"); - const bullish = []; - const bearish = []; - for (let i = 1; i < lows.length; i++) { - const a = lows[i - 1], b = lows[i]; - const priceDelta = (b.price - a.price) / a.price; - const rsiDelta = b.oscillator - a.oscillator; - const regular = priceDelta < -1e-3 && rsiDelta > 2; - const hidden = priceDelta > 1e-3 && rsiDelta < -2; - if (regular || hidden) { - const strength = Math.min(100, Math.round(Math.abs(priceDelta) * 2500 + Math.abs(rsiDelta) * 4)); - bullish.push({ type: regular ? "bullish-regular" : "bullish-hidden", firstIndex: a.index, secondIndex: b.index, firstTimestamp: a.timestamp, secondTimestamp: b.timestamp, priceFirst: a.price, priceSecond: b.price, rsiFirst: a.oscillator, rsiSecond: b.oscillator, strength, invalidation: b.price }); - } - } - for (let i = 1; i < highs.length; i++) { - const a = highs[i - 1], b = highs[i]; - const priceDelta = (b.price - a.price) / a.price; - const rsiDelta = b.oscillator - a.oscillator; - const regular = priceDelta > 1e-3 && rsiDelta < -2; - const hidden = priceDelta < -1e-3 && rsiDelta > 2; - if (regular || hidden) { - const strength = Math.min(100, Math.round(Math.abs(priceDelta) * 2500 + Math.abs(rsiDelta) * 4)); - bearish.push({ type: regular ? "bearish-regular" : "bearish-hidden", firstIndex: a.index, secondIndex: b.index, firstTimestamp: a.timestamp, secondTimestamp: b.timestamp, priceFirst: a.price, priceSecond: b.price, rsiFirst: a.oscillator, rsiSecond: b.oscillator, strength, invalidation: b.price }); - } - } - const all = [...bullish, ...bearish].sort((a, b) => b.secondTimestamp - a.secondTimestamp); - return { bullish, bearish, latest: all[0] ?? null }; -} - -// server/gann/gannEngine.ts -var RATIOS = [0.25, 0.382, 0.5, 0.618, 0.75, 1, 1.272, 1.618]; -var TIMING_BARS = [9, 18, 27, 36, 45]; -function analyzeGann(candles, structure) { - const swings = structure.swings; - if (candles.length < 20 || swings.length < 2) { - return { anchorLow: null, anchorHigh: null, range: null, levels: [], timingWindows: [], bias: "neutral", score: 0, reasons: ["Dados insuficientes para an\xE1lise Gann"] }; - } - const highs = swings.filter((s) => s.type === "high"); - const lows = swings.filter((s) => s.type === "low"); - const anchorHigh = highs.length ? highs[highs.length - 1].price : Math.max(...candles.map((c) => c.high)); - const anchorLow = lows.length ? lows[lows.length - 1].price : Math.min(...candles.map((c) => c.low)); - const low = Math.min(anchorLow, anchorHigh); - const high = Math.max(anchorLow, anchorHigh); - const range2 = high - low; - const price = candles[candles.length - 1].close; - const levels = RATIOS.map((ratio) => { - const level = low + range2 * ratio; - return { ratio, price: level, relation: level <= price ? "support" : "resistance" }; - }); - const interval = candles.length > 1 ? candles[candles.length - 1].timestamp - candles[candles.length - 2].timestamp : 0; - const timingWindows = interval > 0 ? TIMING_BARS.map((bars) => ({ bars, targetTimestamp: candles[candles.length - 1].timestamp + interval * bars })) : []; - const midpoint = low + range2 * 0.5; - let score2 = 0; - const reasons = []; - if (price > midpoint) { - score2 += 1; - reasons.push("Pre\xE7o acima de 50% do range \xE2ncora"); - } else if (price < midpoint) { - score2 -= 1; - reasons.push("Pre\xE7o abaixo de 50% do range \xE2ncora"); - } - if (structure.trend === "bullish") { - score2 += 1; - reasons.push("Estrutura confirma vi\xE9s bullish"); - } - if (structure.trend === "bearish") { - score2 -= 1; - reasons.push("Estrutura confirma vi\xE9s bearish"); - } - return { anchorLow: low, anchorHigh: high, range: range2, levels, timingWindows, bias: score2 > 0 ? "bullish" : score2 < 0 ? "bearish" : "neutral", score: score2, reasons }; -} - -// server/wyckoff/wyckoffEngine.ts -function average(values) { - return values.length ? values.reduce((sum, value) => sum + value, 0) / values.length : 0; -} -function body(candle) { - return Math.abs(candle.close - candle.open); -} -function range(candle) { - return Math.max(candle.high - candle.low, 0); -} -function detectEvents(candles, rangeLow, rangeHigh, volumeAverage) { - const events = []; - const start = Math.max(2, candles.length - 80); - for (let i = start; i < candles.length; i += 1) { - const c = candles[i]; - const previous = candles[i - 1]; - const previousPrevious = candles[i - 2]; - const volumeRatio = volumeAverage > 0 ? c.volume / volumeAverage : 1; - const cRange = range(c); - if (!cRange) continue; - const spring = c.low < rangeLow && c.close > rangeLow && c.close > c.open && volumeRatio >= 1.15; - const test = previous && previous.low < rangeLow && c.low >= previous.low && c.close > c.open && volumeRatio <= 1.1; - const sos = c.close > rangeHigh && body(c) / cRange >= 0.55 && volumeRatio >= 1.2; - const utad = c.high > rangeHigh && c.close < rangeHigh && c.close < c.open && volumeRatio >= 1.15; - const sow = c.close < rangeLow && body(c) / cRange >= 0.55 && volumeRatio >= 1.2; - if (spring) events.push("SPRING"); - if (test) events.push("TEST"); - if (sos) events.push("SOS"); - if (utad) events.push("UTAD"); - if (sow) events.push("SOW"); - void previousPrevious; - } - return events.slice(-10); -} -function analyzeWyckoff(candles, structure, indicators) { - if (candles.length < 40) { - return { - phase: "neutral", - bias: "neutral", - score: 0, - volumeRatio: indicators.relativeVolume20, - effortVsResult: "balanced", - events: [], - latestEvent: null, - rangeHigh: null, - rangeLow: null, - reasons: ["Dados insuficientes para an\xE1lise Wyckoff"] - }; - } - const window = candles.slice(-60); - const rangeHigh = Math.max(...window.map((c) => c.high)); - const rangeLow = Math.min(...window.map((c) => c.low)); - const latest = candles.at(-1); - const averageVolume = average(window.map((c) => c.volume)); - const volumeRatio = averageVolume > 0 ? latest.volume / averageVolume : 1; - const latestRange = range(latest); - const latestBody = body(latest); - const effortResult = latestRange > 0 ? latestBody / latestRange : 0; - const events = detectEvents(candles, rangeLow, rangeHigh, averageVolume); - const latestEvent = events.at(-1) ?? null; - let score2 = 0; - const reasons = []; - if (structure.trend === "bullish") score2 += 1; - if (structure.trend === "bearish") score2 -= 1; - if (latestEvent === "SPRING" || latestEvent === "TEST" || latestEvent === "SOS") { - score2 += latestEvent === "SOS" ? 2 : 1; - reasons.push(`Evento Wyckoff ${latestEvent}`); - } - if (latestEvent === "UTAD" || latestEvent === "SOW") { - score2 -= latestEvent === "SOW" ? 2 : 1; - reasons.push(`Evento Wyckoff ${latestEvent}`); - } - const position = rangeHigh > rangeLow ? (latest.close - rangeLow) / (rangeHigh - rangeLow) : 0.5; - let phase = "neutral"; - if (structure.trend === "bullish" && position > 0.55) phase = "markup"; - else if (structure.trend === "bearish" && position < 0.45) phase = "markdown"; - else if (latestEvent === "SPRING" || latestEvent === "TEST") phase = "accumulation"; - else if (latestEvent === "UTAD") phase = "distribution"; - else if (position >= 0.4 && position <= 0.6) phase = score2 >= 0 ? "accumulation" : "distribution"; - if (phase === "markup") reasons.push("Estrutura e posi\xE7\xE3o no range sugerem markup"); - if (phase === "markdown") reasons.push("Estrutura e posi\xE7\xE3o no range sugerem markdown"); - if (phase === "accumulation") reasons.push("Pre\xE7o trabalhando a regi\xE3o inferior do range"); - if (phase === "distribution") reasons.push("Pre\xE7o trabalhando a regi\xE3o superior do range"); - let effortVsResult = "balanced"; - if (volumeRatio >= 1.8) effortVsResult = "climax"; - else if (volumeRatio >= 1.3 && effortResult < 0.45) effortVsResult = "absorption"; - else if (volumeRatio <= 0.7 && effortResult >= 0.55) effortVsResult = "low-effort"; - if (effortVsResult === "absorption") reasons.push("Volume elevado com deslocamento relativamente pequeno, poss\xEDvel absor\xE7\xE3o"); - if (effortVsResult === "climax") reasons.push("Volume em n\xEDvel de cl\xEDmax, exige confirma\xE7\xE3o posterior"); - if (effortVsResult === "low-effort") reasons.push("Deslocamento com volume reduzido, falta de oposi\xE7\xE3o aparente"); - const bias = score2 >= 2 ? "bullish" : score2 <= -2 ? "bearish" : "neutral"; - return { phase, bias, score: Math.max(-3, Math.min(3, score2)), volumeRatio, effortVsResult, events, latestEvent, rangeHigh, rangeLow, reasons }; -} - -// server/confluence/confluenceEngine.ts -function signBias(bias) { - return bias === "bullish" ? 1 : bias === "bearish" ? -1 : 0; -} -function indicatorBias(ind) { - const reasons = []; - let score2 = 0; - if (ind.ema9 > ind.ema21) { - score2 += 1; - reasons.push("EMA9 acima da EMA21"); - } else if (ind.ema9 < ind.ema21) { - score2 -= 1; - reasons.push("EMA9 abaixo da EMA21"); - } - if (ind.ema21 > ind.ema50) { - score2 += 1; - reasons.push("EMA21 acima da EMA50"); - } else if (ind.ema21 < ind.ema50) { - score2 -= 1; - reasons.push("EMA21 abaixo da EMA50"); - } - if (ind.ema50 > ind.ema200) { - score2 += 1; - reasons.push("EMA50 acima da EMA200"); - } else if (ind.ema50 < ind.ema200) { - score2 -= 1; - reasons.push("EMA50 abaixo da EMA200"); - } - if (ind.rsi14 >= 55 && ind.rsi14 <= 70) { - score2 += 1; - reasons.push("RSI em regime comprador"); - } else if (ind.rsi14 <= 45 && ind.rsi14 >= 30) { - score2 -= 1; - reasons.push("RSI em regime vendedor"); - } - if (ind.relativeVolume20 >= 1.2) reasons.push("Volume relativo acima da m\xE9dia"); - return { bias: score2 > 0 ? "bullish" : score2 < 0 ? "bearish" : "neutral", reasons }; -} -function calculateConfluence(inputs, derivatives = null) { - const weights = { "15m": 1, "1h": 2, "4h": 3, "1d": 4 }; - const timeframes = []; - let weightedScore = 0; - let totalWeight = 0; - const confirmations = []; - const conflicts = []; - let divergenceScore = 0; - let gannScore = 0; - let wyckoffScore = 0; - for (const input of inputs) { - const ib = indicatorBias(input.indicators); - let score3 = signBias(input.structure.trend) * 2 + signBias(input.smc.bias) * 2 + signBias(ib.bias); - const reasons = [...ib.reasons]; - if (input.structure.latestEvent?.type === "BOS") { - score3 += signBias(input.structure.latestEvent.direction); - reasons.push(`BOS ${input.structure.latestEvent.direction}`); - } - if (input.structure.latestEvent?.type === "CHoCH") { - score3 += signBias(input.structure.latestEvent.direction); - reasons.push(`CHoCH ${input.structure.latestEvent.direction}`); - } - if (input.structure.latestSweep) { - const sweepBias = input.structure.latestSweep.type === "low" ? 1 : -1; - score3 += sweepBias; - reasons.push(input.structure.latestSweep.type === "low" ? "Sweep de sell-side liquidity" : "Sweep de buy-side liquidity"); - } - if (input.smc.premiumDiscount?.zone === "discount") score3 += 1; - if (input.smc.premiumDiscount?.zone === "premium") score3 -= 1; - const latestDivergence = input.divergences?.latest; - if (latestDivergence && latestDivergence.strength >= 25) { - const dScore = latestDivergence.type.startsWith("bullish") ? 2 : -2; - score3 += dScore; - divergenceScore += dScore * weights[input.timeframe]; - reasons.push(`Diverg\xEAncia ${latestDivergence.type} (${latestDivergence.strength}/100)`); - } - if (input.gann) { - const gScore = Math.max(-1, Math.min(1, input.gann.score)); - score3 += gScore; - gannScore += gScore * weights[input.timeframe]; - if (gScore !== 0) reasons.push(`Gann ${input.gann.bias}`); - } - if (input.wyckoff) { - const wScore = Math.max(-2, Math.min(2, input.wyckoff.score)); - score3 += wScore; - wyckoffScore += wScore * weights[input.timeframe]; - if (input.wyckoff.latestEvent) reasons.push(`Wyckoff ${input.wyckoff.latestEvent}`); - reasons.push(`Fase Wyckoff ${input.wyckoff.phase}`); - } - const bias2 = score3 >= 2 ? "bullish" : score3 <= -2 ? "bearish" : "neutral"; - const weight = weights[input.timeframe]; - weightedScore += score3 * weight; - totalWeight += weight * 13; - timeframes.push({ timeframe: input.timeframe, bias: bias2, score: score3, reasons }); - if (score3 >= 5) confirmations.push(`${input.timeframe}: conflu\xEAncia bullish forte`); - if (score3 <= -5) confirmations.push(`${input.timeframe}: conflu\xEAncia bearish forte`); - } - if (derivatives) { - weightedScore += signBias(derivatives.bias) * 2; - totalWeight += 2 * 13; - if (derivatives.score > 0) confirmations.push("Derivativos favorecem cen\xE1rio bullish"); - if (derivatives.score < 0) confirmations.push("Derivativos mostram excesso de posicionamento comprador"); - if (derivatives.score === 0) conflicts.push("Derivativos sem confirma\xE7\xE3o direcional"); - } - const normalized = totalWeight ? weightedScore / totalWeight : 0; - const score2 = Math.round(Math.max(-100, Math.min(100, normalized * 100))); - const bias = score2 >= 20 ? "bullish" : score2 <= -20 ? "bearish" : "neutral"; - const confidence = Math.round(Math.min(99, Math.abs(score2) + confirmations.length * 3)); - const bullishCount = timeframes.filter((t) => t.bias === "bullish").length; - const bearishCount = timeframes.filter((t) => t.bias === "bearish").length; - if (bullishCount > 0 && bearishCount > 0) conflicts.push("Timeframes apresentam conflito de dire\xE7\xE3o"); - if (timeframes.some((t) => t.timeframe === "1d" && t.bias !== bias)) conflicts.push("Daily n\xE3o confirma o vi\xE9s dominante"); - const entryQuality = confidence >= 80 && conflicts.length === 0 ? "A+" : confidence >= 70 ? "A" : confidence >= 55 ? "B" : confidence >= 35 ? "C" : "avoid"; - return { bias, score: score2, confidence, timeframes, confirmations, conflicts, entryQuality, derivatives, divergenceScore, gannScore, wyckoffScore }; -} - -// server/confluence/marketAnalysisService.ts -async function analyzeMarket(symbol, forceRefresh = false) { - const snapshot = await getMarketSnapshot(symbol, { forceRefresh }); - const timeframes = ["15m", "1h", "4h", "1d"].map((timeframe) => { - const series = snapshot.series[timeframe]; - const indicators = calculateIndicators(series.candles); - const structure = analyzeMarketStructure(series.candles); - const smc = analyzeSMC(series.candles, structure, indicators); - const divergences = detectDivergences(series.candles, indicators); - const gann = analyzeGann(series.candles, structure); - const wyckoff = analyzeWyckoff(series.candles, structure, indicators); - return { timeframe, exchange: series.exchange, latestTimestamp: series.latestTimestamp, indicators, structure, smc, divergences, gann, wyckoff }; - }); - let derivatives = null; - try { - derivatives = analyzeDerivatives(await fetchDerivativesSnapshot(symbol)); - } catch (error) { - console.warn(`Derivatives unavailable for ${symbol}:`, error); - } - return { - symbol: snapshot.symbol, - fetchedAt: snapshot.fetchedAt, - timeframes, - derivatives, - confluence: calculateConfluence(timeframes, derivatives) - }; -} - -// server/backtest/historicalDataService.ts -var INTERVAL_MS = { - "15m": 15 * 6e4, - "1h": 60 * 6e4, - "4h": 4 * 60 * 6e4, - "1d": 24 * 60 * 6e4 -}; -var OKX_BAR = { - "15m": "15m", - "1h": "1H", - "4h": "4H", - "1d": "1Dutc" -}; -var TIMEOUT_MS2 = 1e4; -function normalizeSymbol3(symbol) { - return symbol.replace("/", "").toUpperCase(); -} -function normalizeOkxSpotInstrument(symbol) { - return symbol.replace("/", "-").toUpperCase(); -} -function parseCandle(row) { - if (!Array.isArray(row) || row.length < 6) return null; - const timestamp = Number(row[0]); - const open = Number(row[1]); - const high = Number(row[2]); - const low = Number(row[3]); - const close = Number(row[4]); - const volume = Number(row[5]); - if (!Number.isFinite(timestamp) || !Number.isFinite(open) || !Number.isFinite(high) || !Number.isFinite(low) || !Number.isFinite(close) || !Number.isFinite(volume) || high < Math.max(open, close, low) || low > Math.min(open, close, high) || volume < 0) return null; - return { - timestamp, - timeStr: new Date(timestamp).toISOString(), - open, - high, - low, - close, - volume - }; -} -async function fetchBinanceHistoricalCandles(symbol, interval, startTime, endTime) { - const step = INTERVAL_MS[interval]; - if (!step) throw new Error(`Unsupported historical interval: ${interval}`); - const result = []; - let cursor = startTime; - const maxPages = Math.ceil((endTime - startTime) / step / 1e3) + 2; - for (let page = 0; page < maxPages && cursor < endTime; page += 1) { - const url = new URL("https://api.binance.com/api/v3/klines"); - url.searchParams.set("symbol", normalizeSymbol3(symbol)); - url.searchParams.set("interval", interval); - url.searchParams.set("limit", "1000"); - url.searchParams.set("startTime", String(cursor)); - url.searchParams.set("endTime", String(endTime)); - const response = await fetch(url, { - headers: { Accept: "application/json" }, - signal: AbortSignal.timeout(TIMEOUT_MS2) - }); - if (!response.ok) throw new Error(`Binance historical HTTP ${response.status}`); - const data = await response.json(); - if (!Array.isArray(data)) throw new Error("Binance historical payload is invalid"); - if (data.length === 0) break; - for (const row of data) { - const candle = parseCandle(row); - if (candle && candle.timestamp >= startTime && candle.timestamp < endTime) result.push(candle); - } - const lastTimestamp = Number(data[data.length - 1][0]); - if (!Number.isFinite(lastTimestamp) || lastTimestamp < cursor) break; - cursor = lastTimestamp + step; - if (data.length < 1e3) break; - } - return result; -} -async function fetchOkxHistoricalCandles(symbol, interval, startTime, endTime) { - const step = INTERVAL_MS[interval]; - const bar = OKX_BAR[interval]; - if (!step || !bar) throw new Error(`Unsupported OKX historical interval: ${interval}`); - const instId = normalizeOkxSpotInstrument(symbol); - const result = []; - let after = null; - const maxPages = Math.ceil((endTime - startTime) / step / 300) + 4; - for (let page = 0; page < maxPages; page += 1) { - const url = new URL("https://www.okx.com/api/v5/market/history-candles"); - url.searchParams.set("instId", instId); - url.searchParams.set("bar", bar); - url.searchParams.set("limit", "300"); - if (after !== null) url.searchParams.set("after", String(after)); - const response = await fetch(url, { - headers: { Accept: "application/json" }, - signal: AbortSignal.timeout(TIMEOUT_MS2) - }); - if (!response.ok) throw new Error(`OKX historical HTTP ${response.status}`); - const payload = await response.json(); - if (!payload || typeof payload !== "object") throw new Error("OKX historical payload is invalid"); - const body2 = payload; - if (body2.code !== "0" || !Array.isArray(body2.data)) { - throw new Error(`OKX historical returned code ${body2.code ?? "unknown"}: ${body2.msg ?? "invalid payload"}`); - } - if (body2.data.length === 0) break; - let oldestTimestamp = Number.POSITIVE_INFINITY; - let added = 0; - for (const row of body2.data) { - const candle = parseCandle(row); - if (!candle) continue; - oldestTimestamp = Math.min(oldestTimestamp, candle.timestamp); - if (candle.timestamp >= startTime && candle.timestamp < endTime) { - result.push(candle); - added += 1; - } - } - if (!Number.isFinite(oldestTimestamp) || oldestTimestamp <= startTime) break; - if (added === 0 && oldestTimestamp < startTime) break; - const nextAfter = oldestTimestamp; - if (after !== null && nextAfter >= after) break; - after = nextAfter; - } - return result; -} -function dedupeAndSort(candles) { - const unique = /* @__PURE__ */ new Map(); - for (const candle of candles) unique.set(candle.timestamp, candle); - return [...unique.values()].sort((a, b) => a.timestamp - b.timestamp); -} -async function fetchHistoricalBinanceCandles(symbol, interval = "15m", startTime, endTime) { - const step = INTERVAL_MS[interval]; - if (!step) throw new Error(`Unsupported historical interval: ${interval}`); - if (endTime <= startTime) throw new Error("Historical endTime must be greater than startTime"); - try { - const binanceCandles = await fetchBinanceHistoricalCandles(symbol, interval, startTime, endTime); - if (binanceCandles.length >= 300) return dedupeAndSort(binanceCandles); - } catch (error) { - console.warn(`Binance historical unavailable for ${symbol}:`, error); - } - const okxCandles = await fetchOkxHistoricalCandles(symbol, interval, startTime, endTime); - const candles = dedupeAndSort(okxCandles); - if (candles.length === 0) { - throw new Error(`All historical providers failed for ${symbol}. Binance and OKX returned no usable candles.`); - } - return candles; -} - -// server/derivatives/historicalFundingService.ts -var TIMEOUT_MS3 = 1e4; -var PAGE_LIMIT_BINANCE = 1e3; -var PAGE_LIMIT_OKX = 400; -var OKX_MAX_HISTORY_MS = 90 * 864e5; -var MAX_RATE = 0.01; -function normalizeSymbol4(symbol) { - return symbol.replace("/", "").toUpperCase(); -} -function normalizeOkxSwap(symbol) { - const [base, quote] = symbol.toUpperCase().split("/"); - return `${base}-${quote}-SWAP`; -} -async function getJson2(url) { - const response = await fetch(url, { - headers: { Accept: "application/json" }, - signal: AbortSignal.timeout(TIMEOUT_MS3) - }); - if (!response.ok) throw new Error(`HTTP ${response.status}: ${response.statusText}`); - return response.json(); -} -function uniqueSorted(rows) { - const unique = /* @__PURE__ */ new Map(); - for (const row of rows) unique.set(`${row.source}:${row.timestamp}`, row); - return [...unique.values()].sort((a, b) => a.timestamp - b.timestamp); -} -async function fetchBinanceHistoricalFunding(symbol, startTime, endTime) { - const normalized = normalizeSymbol4(symbol); - if (!Number.isFinite(startTime) || !Number.isFinite(endTime) || endTime <= startTime) return []; - const result = []; - let cursor = Math.max(0, Math.floor(startTime)); - const finalTime = Math.floor(endTime); - while (cursor <= finalTime) { - const url = `https://fapi.binance.com/fapi/v1/fundingRate?symbol=${normalized}&startTime=${cursor}&endTime=${finalTime}&limit=${PAGE_LIMIT_BINANCE}`; - const rows = await getJson2(url); - if (!Array.isArray(rows) || rows.length === 0) break; - let newest = cursor; - for (const row of rows) { - const timestamp = Number(row?.fundingTime); - const fundingRate = Number(row?.fundingRate); - if (!Number.isFinite(timestamp) || !Number.isFinite(fundingRate)) continue; - if (timestamp < startTime || timestamp > finalTime) continue; - result.push({ - timestamp, - fundingRate: Math.max(-MAX_RATE, Math.min(MAX_RATE, fundingRate)), - symbol: normalized, - source: "binance-futures" - }); - newest = Math.max(newest, timestamp); - } - if (rows.length < PAGE_LIMIT_BINANCE || newest <= cursor) break; - cursor = newest + 1; - } - return uniqueSorted(result); -} -async function fetchHistoricalOkxFunding(symbol, startTime, endTime) { - if (!Number.isFinite(startTime) || !Number.isFinite(endTime) || endTime <= startTime) return []; - const instId = normalizeOkxSwap(symbol); - const effectiveStart = Math.max(Math.floor(startTime), Math.floor(endTime) - OKX_MAX_HISTORY_MS); - const finalTime = Math.floor(endTime); - const result = []; - let after = null; - for (let page = 0; page < 100; page += 1) { - const params = new URLSearchParams({ instId, limit: String(PAGE_LIMIT_OKX) }); - if (after !== null) params.set("after", String(after)); - const payload = await getJson2(`https://www.okx.com/api/v5/public/funding-rate-history?${params.toString()}`); - if (String(payload?.code ?? "0") !== "0") throw new Error(`OKX funding history error: ${payload?.msg || payload?.code || "unknown error"}`); - const rows = Array.isArray(payload?.data) ? payload.data : []; - if (!rows.length) break; - let oldest = Number.POSITIVE_INFINITY; - for (const row of rows) { - const timestamp = Number(row?.fundingTime); - const fundingRate = Number(row?.realizedRate ?? row?.fundingRate); - if (!Number.isFinite(timestamp) || !Number.isFinite(fundingRate)) continue; - oldest = Math.min(oldest, timestamp); - if (timestamp < effectiveStart || timestamp > finalTime) continue; - result.push({ - timestamp, - fundingRate: Math.max(-MAX_RATE, Math.min(MAX_RATE, fundingRate)), - symbol: instId, - source: "okx-swap" - }); - } - if (oldest === Number.POSITIVE_INFINITY || oldest <= effectiveStart || rows.length < PAGE_LIMIT_OKX) break; - after = oldest; - } - return uniqueSorted(result); -} -async function fetchHistoricalBinanceFunding(symbol, startTime, endTime) { - try { - const binance = await fetchBinanceHistoricalFunding(symbol, startTime, endTime); - if (binance.length) return binance; - } catch (error) { - console.warn(`Binance historical funding unavailable for ${symbol}:`, error); - } - try { - const okx = await fetchHistoricalOkxFunding(symbol, startTime, endTime); - if (okx.length) return okx; - } catch (error) { - console.warn(`OKX historical funding unavailable for ${symbol}:`, error); - } - return []; -} - -// server/backtest/historicalBacktest.ts -function mean(values) { - return values.length ? values.reduce((a, b) => a + b, 0) / values.length : 0; -} -function std(values) { - if (values.length < 2) return 0; - const m = mean(values); - return Math.sqrt(mean(values.map((v) => (v - m) ** 2))); -} -function calculateRatio(values, downsideOnly = false) { - if (values.length < 2) return 0; - const filtered = downsideOnly ? values.filter((v) => v < 0) : values; - const denominator = std(filtered); - return denominator === 0 ? 0 : mean(values) / denominator * Math.sqrt(values.length); -} -function clampIndex(index, length) { - return Math.max(0, Math.min(index, length - 1)); -} -function historicalFundingSum(funding, entryTimestamp, exitTimestamp) { - if (!funding.length) return 0; - let total = 0; - for (const row of funding) { - if (row.timestamp >= entryTimestamp && row.timestamp <= exitTimestamp) total += row.fundingRate; - } - return total; -} -function runHistoricalBacktest(options) { - const candles = [...options.candles].sort((a, b) => a.timestamp - b.timestamp); - const initialCapital = options.initialCapital ?? 1e4; - const riskPerTradePercent = options.riskPerTradePercent ?? 1; - const minScore = options.minScore ?? 35; - const minConfidence = options.minConfidence ?? 50; - const atrStopMultiple = options.atrStopMultiple ?? 1.5; - const rewardRisk = options.rewardRisk ?? 2; - const maxHoldingBars = options.maxHoldingBars ?? 32; - const warmupBars = Math.max(options.warmupBars ?? 220, 220); - const feeBpsPerSide = Math.max(0, options.feeBpsPerSide ?? 5); - const baseSlippageBpsPerSide = Math.max(0, options.slippageBpsPerSide ?? 2); - const latencySlippageBpsPerSide = Math.max(0, options.latencySlippageBpsPerSide ?? 1); - const effectiveSlippageBpsPerSide = baseSlippageBpsPerSide + latencySlippageBpsPerSide; - const proxyFundingRatePer8h = options.fundingRatePer8h ?? 1e-4; - const feeRate = feeBpsPerSide / 1e4; - const slippageRate = effectiveSlippageBpsPerSide / 1e4; - const fundingRate = Number.isFinite(proxyFundingRatePer8h) ? Math.max(-0.01, Math.min(0.01, proxyFundingRatePer8h)) : 0; - const historicalFunding = [...options.historicalFunding ?? []].filter((r) => Number.isFinite(r.timestamp) && Number.isFinite(r.fundingRate)).sort((a, b) => a.timestamp - b.timestamp); - const hasHistoricalFunding = historicalFunding.length > 0; - const trades = []; - const equityCurve = []; - let equity = initialCapital; - let peak = equity; - let maxDrawdownPercent = 0; - let totalFees = 0; - let totalSlippage = 0; - let totalFunding = 0; - let grossProfit = 0; - let nextFreeIndex = warmupBars; - for (let i = warmupBars; i < candles.length - 2; i += 1) { - if (i < nextFreeIndex) continue; - const history = candles.slice(0, i + 1); - const indicators = calculateIndicators(history); - const structure = analyzeMarketStructure(history); - const smc = analyzeSMC(history, structure, indicators); - const divergences = detectDivergences(history, indicators); - const gann = analyzeGann(history, structure); - const wyckoff = analyzeWyckoff(history, structure, indicators); - const confluence = calculateConfluence([{ timeframe: "15m", indicators, structure, smc, divergences, gann, wyckoff }], null); - if (Math.abs(confluence.score) < minScore || confluence.confidence < minConfidence || confluence.entryQuality === "avoid") continue; - const direction = confluence.bias === "bullish" ? "LONG" : "SHORT"; - if (confluence.bias === "neutral") continue; - const entryIndex = i + 1; - const entry = candles[entryIndex]; - const atr2 = indicators.atr14; - if (!Number.isFinite(atr2) || atr2 <= 0 || !Number.isFinite(entry.open)) continue; - const stopDistance = atr2 * atrStopMultiple; - const rawEntryPrice = entry.open; - const entryPrice = direction === "LONG" ? rawEntryPrice * (1 + slippageRate) : rawEntryPrice * (1 - slippageRate); - const stopLoss = direction === "LONG" ? entryPrice - stopDistance : entryPrice + stopDistance; - const takeProfit = direction === "LONG" ? entryPrice + stopDistance * rewardRisk : entryPrice - stopDistance * rewardRisk; - const lastIndex = clampIndex(entryIndex + maxHoldingBars, candles.length - 1); - let rawExitPrice = candles[lastIndex].close; - let status = "TIMEOUT"; - let exitIndex = lastIndex; - for (let j = entryIndex; j <= lastIndex; j += 1) { - const candle = candles[j]; - const hitStop = direction === "LONG" ? candle.low <= stopLoss : candle.high >= stopLoss; - const hitTarget = direction === "LONG" ? candle.high >= takeProfit : candle.low <= takeProfit; - if (hitStop) { - rawExitPrice = stopLoss; - status = "SL ATINGIDO"; - exitIndex = j; - break; - } - if (hitTarget) { - rawExitPrice = takeProfit; - status = "TP ATINGIDO"; - exitIndex = j; - break; - } - } - const exitPrice = direction === "LONG" ? rawExitPrice * (1 - slippageRate) : rawExitPrice * (1 + slippageRate); - const units = equity * (riskPerTradePercent / 100) / stopDistance; - const notionalEntry = units * entryPrice; - const notionalExit = units * exitPrice; - const grossPnl = direction === "LONG" ? units * (exitPrice - entryPrice) : units * (entryPrice - exitPrice); - const fees = (notionalEntry + notionalExit) * feeRate; - const slippageCost = units * Math.abs(entryPrice - rawEntryPrice) + units * Math.abs(exitPrice - rawExitPrice); - const holdingBars = exitIndex - entryIndex + 1; - const fundingRateApplied = hasHistoricalFunding ? historicalFundingSum(historicalFunding, entry.timestamp, candles[exitIndex].timestamp) : fundingRate * (holdingBars / 32); - const fundingSignedCost = (direction === "LONG" ? 1 : -1) * notionalEntry * fundingRateApplied; - const netPnl = grossPnl - fees - fundingSignedCost; - const grossPnlPercent = notionalEntry > 0 ? grossPnl / notionalEntry * 100 : 0; - const feePercent = notionalEntry > 0 ? fees / notionalEntry * 100 : 0; - const slippagePercent = notionalEntry > 0 ? slippageCost / notionalEntry * 100 : 0; - const fundingPercent = notionalEntry > 0 ? fundingSignedCost / notionalEntry * 100 : 0; - const pnlPercent = notionalEntry > 0 ? netPnl / notionalEntry * 100 : 0; - const riskCapital = equity * (riskPerTradePercent / 100); - const grossPnlR = riskCapital > 0 ? grossPnl / riskCapital : 0; - const feesR = riskCapital > 0 ? fees / riskCapital : 0; - const fundingR = riskCapital > 0 ? fundingSignedCost / riskCapital : 0; - const pnlR = riskCapital > 0 ? netPnl / riskCapital : 0; - let worstIntratradeEquity = equity; - const entryFee = notionalEntry * feeRate; - for (let j = entryIndex; j <= exitIndex; j += 1) { - const candle = candles[j]; - const adversePrice = direction === "LONG" ? candle.low : candle.high; - const unrealizedPnl = direction === "LONG" ? units * (adversePrice - entryPrice) : units * (entryPrice - adversePrice); - worstIntratradeEquity = Math.min(worstIntratradeEquity, equity + unrealizedPnl - entryFee); - } - if (peak > 0) maxDrawdownPercent = Math.max(maxDrawdownPercent, Math.max(0, (peak - worstIntratradeEquity) / peak * 100)); - equity += netPnl; - totalFees += fees; - totalSlippage += slippageCost; - totalFunding += fundingSignedCost; - if (grossPnl > 0) grossProfit += grossPnl; - peak = Math.max(peak, equity); - const drawdown = peak > 0 ? (peak - equity) / peak * 100 : 0; - maxDrawdownPercent = Math.max(maxDrawdownPercent, drawdown); - const date = new Date(entry.timestamp).toISOString().slice(0, 10); - trades.push({ id: `${options.symbol.replace("/", "")}-${entry.timestamp}`, timestamp: entry.timestamp, date, symbol: options.symbol, direction, entryPrice, exitPrice, stopLoss, takeProfit, rrRatio: rewardRisk, confidence: confluence.confidence, score: confluence.score, grossPnlPercent, feePercent, slippagePercent, pnlPercent, pnlR, grossPnlR, feesR, fundingPercent, fundingR, status, holdingBars }); - equityCurve.push({ date, equity, tradePnl: netPnl, drawdown }); - nextFreeIndex = exitIndex + 1; - } - const rValues = trades.map((t) => t.pnlR); - const grossRValues = trades.map((t) => t.grossPnlR); - const wins = trades.filter((t) => t.pnlR > 0); - const losses = trades.filter((t) => t.pnlR < 0); - const netRProfit = wins.reduce((sum, t) => sum + t.pnlR, 0); - const netRLoss = Math.abs(losses.reduce((sum, t) => sum + t.pnlR, 0)); - const start = candles[0]?.timestamp ?? Date.now(); - const end = candles[candles.length - 1]?.timestamp ?? start; - const periodDays = Math.max(0, (end - start) / 864e5); - return { symbol: options.symbol, timeframe: "15m", startDate: new Date(start).toISOString(), endDate: new Date(end).toISOString(), periodDays, initialCapital, finalCapital: equity, totalTrades: trades.length, winningTrades: wins.length, losingTrades: losses.length, winRate: trades.length ? wins.length / trades.length * 100 : 0, profitFactor: netRLoss > 0 ? netRProfit / netRLoss : netRProfit > 0 ? Infinity : 0, netProfitPercent: (equity - initialCapital) / initialCapital * 100, grossProfitPercent: initialCapital > 0 ? grossProfit / initialCapital * 100 : 0, totalFeesPercent: initialCapital > 0 ? totalFees / initialCapital * 100 : 0, totalSlippagePercent: initialCapital > 0 ? totalSlippage / initialCapital * 100 : 0, totalFundingPercent: initialCapital > 0 ? totalFunding / initialCapital * 100 : 0, maxDrawdownPercent, sharpeRatio: calculateRatio(rValues), sortinoRatio: calculateRatio(rValues, true), averageRR: mean(rValues), expectancyR: mean(rValues), grossExpectancyR: mean(grossRValues), trades, equityCurve, costModel: { feeBpsPerSide, slippageBpsPerSide: baseSlippageBpsPerSide, latencySlippageBpsPerSide, fundingRatePer8h: hasHistoricalFunding ? 0 : fundingRate, fundingIncluded: hasHistoricalFunding || fundingRate !== 0, fundingSource: hasHistoricalFunding ? "historical-binance" : fundingRate !== 0 ? "proxy" : "none" } }; -} - -// server/signal/signalEngine.ts -function round(value, decimals = 2) { - const factor = 10 ** decimals; - return Math.round(value * factor) / factor; -} -function finite(value) { - return value !== null && value !== void 0 && Number.isFinite(value); -} -function noTrade(symbol, timestamp, reasons, confidence = 0, score2 = 0, warnings = []) { - return { - symbol, - timestamp, - direction: "NO TRADE", - strength: "NONE", - score: round(score2), - confidence, - entryZone: null, - stopLoss: null, - invalidation: null, - takeProfits: null, - riskReward: null, - riskPercent: 0, - positionRiskDistance: null, - reasons, - warnings - }; -} -function generateTradeSignal(analysis, candles, riskPercent = 1) { - const latest = candles.at(-1); - const primary = analysis.timeframes.find((tf) => tf.timeframe === "15m") ?? analysis.timeframes.at(-1); - if (!latest || !primary) { - return noTrade(analysis.symbol, Date.now(), ["Dados insuficientes para gerar o plano."]); - } - const rawScore = analysis.confluence.score; - const confidence = analysis.confluence.confidence; - const absScore = Math.abs(rawScore); - const direction = rawScore >= 20 ? "LONG" : rawScore <= -20 ? "SHORT" : "NO TRADE"; - const higher = analysis.timeframes.filter((tf) => tf.timeframe !== primary.timeframe); - const alignedHigher = countAligned(higher, direction); - const conflicts = countConflicts(higher, direction); - const warnings = []; - if (conflicts > 0) warnings.push(`${conflicts} timeframe(s) superior(es) em conflito.`); - if (analysis.confluence.conflicts.length > 0) warnings.push(...analysis.confluence.conflicts.slice(0, 3)); - if (confidence < 60) warnings.push("Confian\xE7a abaixo do n\xEDvel operacional preferencial."); - if (analysis.confluence.entryQuality === "avoid") warnings.push("Conflu\xEAncia classificou a entrada como evit\xE1vel."); - if (direction === "NO TRADE" || absScore < 20 || confidence < 50 || analysis.confluence.entryQuality === "avoid") { - return noTrade( - analysis.symbol, - latest.timestamp, - ["Conflu\xEAncia insuficiente para um setup operacional de qualidade."], - confidence, - rawScore, - warnings - ); - } - const atr2 = primary.indicators.atr14; - if (!finite(atr2) || atr2 <= 0 || latest.close <= 0) { - return noTrade(analysis.symbol, latest.timestamp, ["ATR ou pre\xE7o inv\xE1lido."], confidence, rawScore, warnings); - } - const reference = latest.close; - const swings = primary.structure.swings; - const swingLow = latestSwingPrice(swings, "low"); - const swingHigh = latestSwingPrice(swings, "high"); - const volatilityStop = direction === "LONG" ? reference - atr2 * 1.5 : reference + atr2 * 1.5; - const structureStop = direction === "LONG" ? finite(swingLow) && swingLow < reference ? swingLow - atr2 * 0.15 : volatilityStop : finite(swingHigh) && swingHigh > reference ? swingHigh + atr2 * 0.15 : volatilityStop; - const stopLoss = direction === "LONG" ? Math.min(volatilityStop, structureStop) : Math.max(volatilityStop, structureStop); - const distance = Math.abs(reference - stopLoss); - if (!finite(distance) || distance <= 0 || distance > reference * 0.08) { - return noTrade(analysis.symbol, latest.timestamp, ["Dist\xE2ncia de stop fora do limite operacional."], confidence, rawScore, warnings); - } - const entryBuffer = Math.min(atr2 * 0.25, reference * 25e-4); - const entryZone = { low: reference - entryBuffer, high: reference + entryBuffer, reference }; - const tp1 = direction === "LONG" ? reference + distance * 1.5 : reference - distance * 1.5; - const tp2 = direction === "LONG" ? reference + distance * 2.5 : reference - distance * 2.5; - const tp3 = direction === "LONG" ? reference + distance * 4 : reference - distance * 4; - const strength = absScore >= 70 && confidence >= 80 && alignedHigher >= 2 && conflicts === 0 ? "A+" : absScore >= 55 && confidence >= 70 ? "A" : absScore >= 40 && confidence >= 60 ? "B" : "C"; - return { - symbol: analysis.symbol, - timestamp: latest.timestamp, - direction, - strength, - score: round(rawScore), - confidence, - entryZone: { low: round(entryZone.low), high: round(entryZone.high), reference: round(reference) }, - stopLoss: round(stopLoss), - invalidation: round(stopLoss), - takeProfits: { tp1: round(tp1), tp2: round(tp2), tp3: round(tp3) }, - riskReward: { tp1: 1.5, tp2: 2.5, tp3: 4 }, - riskPercent: Math.max(0.1, Math.min(2, riskPercent)), - positionRiskDistance: round(distance), - reasons: buildReasons(direction, analysis, primary, alignedHigher), - warnings - }; -} -function latestSwingPrice(swings, type) { - for (let i = swings.length - 1; i >= 0; i -= 1) { - if (swings[i].type === type && finite(swings[i].price)) return swings[i].price; - } - return null; -} -function countAligned(timeframes, direction) { - if (direction === "LONG") return timeframes.filter((tf) => tf.structure.trend === "bullish").length; - if (direction === "SHORT") return timeframes.filter((tf) => tf.structure.trend === "bearish").length; - return 0; -} -function countConflicts(timeframes, direction) { - if (direction === "LONG") return timeframes.filter((tf) => tf.structure.trend === "bearish").length; - if (direction === "SHORT") return timeframes.filter((tf) => tf.structure.trend === "bullish").length; - return 0; -} -function buildReasons(direction, analysis, primary, alignedHigher) { - const reasons = [ - `Conflu\xEAncia ${direction} com score ${round(Math.abs(analysis.confluence.score), 1)}.`, - `Confian\xE7a estrutural em ${analysis.confluence.confidence}%.` - ]; - if (primary.structure.trend === (direction === "LONG" ? "bullish" : "bearish")) { - reasons.push(`Estrutura ${direction === "LONG" ? "bullish" : "bearish"} no 15m.`); - } - const latestFvg = primary.smc.fairValueGaps.at(-1); - const latestOrderBlock = primary.smc.orderBlocks.at(-1); - if (latestFvg) reasons.push(`FVG ${latestFvg.type} no contexto.`); - if (latestOrderBlock) reasons.push(`Order Block ${latestOrderBlock.type} identificado.`); - if (primary.wyckoff.latestEvent) reasons.push(`Evento Wyckoff ${primary.wyckoff.latestEvent} detectado.`); - if (primary.divergences.latest) reasons.push(`Diverg\xEAncia ${primary.divergences.latest.type} detectada.`); - if (alignedHigher > 0) reasons.push(`${alignedHigher} timeframe(s) superior(es) alinhado(s).`); - return reasons; -} - -// server/ai/quantAnalyst.ts -function compactAnalysis(analysis, signal) { - const timeframes = analysis.timeframes.map((tf) => ({ - timeframe: tf.timeframe, - trend: tf.structure.trend, - rsi: tf.indicators.rsi14, - atr: tf.indicators.atr14, - structure: tf.structure.events.slice(-4), - smcBias: tf.smc.bias, - latestFvg: tf.smc.fairValueGaps.at(-1) ?? null, - latestOrderBlock: tf.smc.orderBlocks.at(-1) ?? null, - divergence: tf.divergences.latest, - gann: tf.gann, - wyckoff: tf.wyckoff - })); - return JSON.stringify({ symbol: analysis.symbol, fetchedAt: analysis.fetchedAt, confluence: analysis.confluence, derivatives: analysis.derivatives, signal, timeframes }); -} -function buildQuantAnalystPrompt(input) { - return `Voc\xEA \xE9 o Quant Analyst de um sistema profissional de trading de criptomoedas. - -Sua fun\xE7\xE3o \xE9 auditar um sinal quantitativo j\xE1 calculado. N\xC3O invente pre\xE7os, indicadores, n\xEDveis, eventos ou dados ausentes. N\xC3O substitua os c\xE1lculos determin\xEDsticos. - -Regras: -1. Use somente os dados JSON fornecidos. -2. Avalie alinhamento entre 15m, 1H, 4H e 1D. -3. D\xEA peso especial \xE0 estrutura, SMC, Wyckoff, diverg\xEAncias e derivativos quando dispon\xEDveis. -4. Procure conflitos, baixa qualidade, diverg\xEAncias contra a dire\xE7\xE3o, aus\xEAncia de confirma\xE7\xE3o e risco de contexto. -5. A decis\xE3o deve ser exatamente uma de: CONFIRM, WEAKEN ou REJECT. -6. CONFIRM apoia o sinal. WEAKEN indica conflitos relevantes. REJECT indica conflito estrutural ou dados insuficientes. -7. Seja objetivo e n\xE3o forne\xE7a promessa de lucro. - -Responda SOMENTE em JSON v\xE1lido: {"decision":"CONFIRM|WEAKEN|REJECT","rationale":"...","riskFlags":["..."]} - -DADOS: -${compactAnalysis(input.analysis, input.signal)}`; -} -function parseQuantAnalystResponse(text, model) { - const fallback = { decision: "WEAKEN", rationale: "A resposta do modelo n\xE3o p\xF4de ser validada como JSON estruturado.", riskFlags: ["ai_response_invalid"], model }; - try { - const parsed = JSON.parse(text); - if (!["CONFIRM", "WEAKEN", "REJECT"].includes(String(parsed.decision))) return fallback; - return { - decision: parsed.decision, - rationale: typeof parsed.rationale === "string" ? parsed.rationale : "Sem justificativa estruturada.", - riskFlags: Array.isArray(parsed.riskFlags) ? parsed.riskFlags.filter((flag) => typeof flag === "string").slice(0, 10) : [], - model - }; - } catch { - return fallback; - } -} - -// server/paper/paperTradingEngine.ts -var clamp = (v, a, b) => Math.max(a, Math.min(b, v)); -var positive = (v) => Number.isFinite(v) && v > 0; -var round2 = (v, d = 8) => Math.round(v * 10 ** d) / 10 ** d; -var PaperTradingEngine = class { - constructor(config = {}) { - this.config = { initialCapital: positive(config.initialCapital ?? 1e4) ? config.initialCapital ?? 1e4 : 1e4, feeBpsPerSide: clamp(config.feeBpsPerSide ?? 5, 0, 100), slippageBpsPerSide: clamp(config.slippageBpsPerSide ?? 2, 0, 100), maxRiskPercent: clamp(config.maxRiskPercent ?? 1, 0.1, 2), maxOpenPositions: Math.max(1, Math.floor(config.maxOpenPositions ?? 1)), maxDrawdownPercent: clamp(config.maxDrawdownPercent ?? 10, 1, 100), partialTpPercent: clamp(config.partialTpPercent ?? 33.333333, 10, 90) }; - this.state = this.emptyState(); - } - emptyStats() { - return { totalTrades: 0, winningTrades: 0, losingTrades: 0, breakevenTrades: 0, winRatePercent: 0, profitFactor: 0, netPnl: 0, netPnlPercent: 0, averageTradePnl: 0, averageR: 0, bestTradePnl: 0, worstTradePnl: 0, tp1HitRatePercent: 0, tp2HitRatePercent: 0, halted: false }; - } - emptyState() { - const capital = this.config.initialCapital; - return { account: { initialCapital: capital, equity: capital, realizedPnl: 0, unrealizedPnl: 0, feesPaid: 0, slippagePaid: 0, peakEquity: capital, maxDrawdownPercent: 0, halted: false, lastMarkPrice: null }, positions: [], history: [], stats: this.emptyStats() }; - } - getState() { - return structuredClone(this.state); - } - reset() { - this.state = this.emptyState(); - return this.getState(); - } - openFromSignal(signal, timestamp = Date.now()) { - if (signal.direction === "NO TRADE" || !signal.entryZone || !signal.stopLoss || !signal.takeProfits || this.state.account.halted || this.state.positions.length >= this.config.maxOpenPositions) return null; - const riskPct = Math.min(signal.riskPercent, this.config.maxRiskPercent), riskCash = this.state.account.equity * riskPct / 100, entry = signal.entryZone.reference, distance = Math.abs(entry - signal.stopLoss); - if (!positive(entry) || !positive(distance) || riskCash <= 0) return null; - const quantity = riskCash / distance, notional = quantity * entry, entrySlip = entry * this.config.slippageBpsPerSide / 1e4, executedEntry = signal.direction === "LONG" ? entry + entrySlip : entry - entrySlip, fee = notional * this.config.feeBpsPerSide / 1e4, slippageCost = quantity * Math.abs(executedEntry - entry); - const p = { id: `paper-${signal.symbol.replace("/", "-")}-${timestamp}-${Math.random().toString(36).slice(2, 8)}`, symbol: signal.symbol, direction: signal.direction, status: "OPEN", openedAt: timestamp, closedAt: null, entryPrice: round2(executedEntry), quantity: round2(quantity), notional: round2(notional, 4), stopLoss: signal.stopLoss, initialStopLoss: signal.stopLoss, takeProfits: signal.takeProfits, remainingQuantity: round2(quantity), realizedPnl: round2(-fee - slippageCost), unrealizedPnl: 0, feesPaid: round2(fee), slippagePaid: round2(slippageCost), closeReason: null, tp1Hit: false, tp2Hit: false, breakevenActivated: false, lastMarkPrice: round2(executedEntry) }; - this.state.positions.push(p); - this.state.account.realizedPnl -= fee + slippageCost; - this.state.account.feesPaid += fee; - this.state.account.slippagePaid += slippageCost; - this.markToMarket(executedEntry); - return structuredClone(p); - } - processCandle(candle, timestamp = candle.timestamp) { - for (const p of [...this.state.positions]) { - const hit = this.firstHit(p, candle); - if (!hit) { - if (timestamp - p.openedAt >= 32 * 15 * 6e4) this.executeClose(p, candle.close, "TIMEOUT", timestamp); - continue; - } - if (hit.reason === "STOP") this.executeClose(p, hit.price, "STOP", timestamp); - else if (hit.reason === "TP1" && !p.tp1Hit) { - this.executePartialClose(p, hit.price, "TP1", timestamp); - p.tp1Hit = true; - p.breakevenActivated = true; - p.stopLoss = p.entryPrice; - } else if (hit.reason === "TP2" && p.tp1Hit && !p.tp2Hit) { - this.executePartialClose(p, hit.price, "TP2", timestamp); - p.tp2Hit = true; - p.stopLoss = p.entryPrice; - } else if (hit.reason === "TP3" && p.tp2Hit) this.executeClose(p, hit.price, "TP3", timestamp); - } - this.markToMarket(candle.close, timestamp); - } - markToMarket(price, _timestamp = Date.now()) { - if (!positive(price)) return; - let unrealized = 0; - for (const p of this.state.positions) { - p.lastMarkPrice = round2(price); - p.unrealizedPnl = round2(p.direction === "LONG" ? (price - p.entryPrice) * p.remainingQuantity : (p.entryPrice - price) * p.remainingQuantity); - unrealized += p.unrealizedPnl; - } - this.state.account.unrealizedPnl = round2(unrealized); - this.state.account.equity = round2(this.state.account.initialCapital + this.state.account.realizedPnl + unrealized, 4); - this.state.account.lastMarkPrice = round2(price); - this.state.account.peakEquity = Math.max(this.state.account.peakEquity, this.state.account.equity); - const dd = this.state.account.peakEquity > 0 ? (this.state.account.peakEquity - this.state.account.equity) / this.state.account.peakEquity * 100 : 0; - this.state.account.maxDrawdownPercent = Math.max(this.state.account.maxDrawdownPercent, dd); - if (this.state.account.maxDrawdownPercent >= this.config.maxDrawdownPercent) this.state.account.halted = true; - this.recalculateStats(); - } - close(positionId, price, reason = "MANUAL", timestamp = Date.now()) { - const p = this.state.positions.find((x) => x.id === positionId); - if (!p || !positive(price)) return null; - this.executeClose(p, price, reason, timestamp); - this.markToMarket(price, timestamp); - return structuredClone(p); - } - firstHit(p, c) { - if (p.direction === "LONG") { - if (c.low <= p.stopLoss) return { price: p.stopLoss, reason: "STOP" }; - if (!p.tp1Hit && c.high >= p.takeProfits.tp1) return { price: p.takeProfits.tp1, reason: "TP1" }; - if (p.tp1Hit && !p.tp2Hit && c.high >= p.takeProfits.tp2) return { price: p.takeProfits.tp2, reason: "TP2" }; - if (p.tp2Hit && c.high >= p.takeProfits.tp3) return { price: p.takeProfits.tp3, reason: "TP3" }; - } else { - if (c.high >= p.stopLoss) return { price: p.stopLoss, reason: "STOP" }; - if (!p.tp1Hit && c.low <= p.takeProfits.tp1) return { price: p.takeProfits.tp1, reason: "TP1" }; - if (p.tp1Hit && !p.tp2Hit && c.low <= p.takeProfits.tp2) return { price: p.takeProfits.tp2, reason: "TP2" }; - if (p.tp2Hit && c.low <= p.takeProfits.tp3) return { price: p.takeProfits.tp3, reason: "TP3" }; - } - return null; - } - executePartialClose(p, price, _reason, _timestamp) { - if (p.status === "CLOSED") return; - const quantity = Math.min(p.remainingQuantity, p.quantity * this.config.partialTpPercent / 100); - this.executeQuantityClose(p, quantity, price); - } - executeQuantityClose(p, quantity, price) { - if (p.status === "CLOSED" || quantity <= 0) return; - const slip = price * this.config.slippageBpsPerSide / 1e4, executed = p.direction === "LONG" ? price - slip : price + slip, gross = p.direction === "LONG" ? (executed - p.entryPrice) * quantity : (p.entryPrice - executed) * quantity, fee = Math.abs(executed * quantity) * this.config.feeBpsPerSide / 1e4, slippageCost = quantity * Math.abs(executed - price), net = gross - fee - slippageCost; - p.realizedPnl = round2(p.realizedPnl + net); - p.feesPaid = round2(p.feesPaid + fee); - p.slippagePaid = round2(p.slippagePaid + slippageCost); - p.remainingQuantity = round2(Math.max(0, p.remainingQuantity - quantity)); - p.unrealizedPnl = 0; - this.state.account.realizedPnl += net; - this.state.account.feesPaid += fee; - this.state.account.slippagePaid += slippageCost; - } - executeClose(p, price, reason, timestamp) { - if (p.status === "CLOSED") return; - if (p.remainingQuantity > 0) this.executeQuantityClose(p, p.remainingQuantity, price); - p.remainingQuantity = 0; - p.unrealizedPnl = 0; - p.status = "CLOSED"; - p.closedAt = timestamp; - p.closeReason = reason; - this.state.positions = this.state.positions.filter((x) => x.id !== p.id); - this.state.history.push(structuredClone(p)); - this.recalculateStats(); - } - recalculateStats() { - const h = this.state.history, total = h.length, w = h.filter((p) => p.realizedPnl > 0).length, l = h.filter((p) => p.realizedPnl < 0).length, be = total - w - l, gp = h.reduce((s, p) => s + Math.max(0, p.realizedPnl), 0), gl = Math.abs(h.reduce((s, p) => s + Math.min(0, p.realizedPnl), 0)), net = this.state.account.realizedPnl; - const avgR = total ? h.reduce((s, p) => { - const risk = Math.abs(p.entryPrice - p.initialStopLoss) * p.quantity; - return s + (risk > 0 ? p.realizedPnl / risk : 0); - }, 0) / total : 0; - this.state.stats = { totalTrades: total, winningTrades: w, losingTrades: l, breakevenTrades: be, winRatePercent: total ? w / total * 100 : 0, profitFactor: gl > 0 ? gp / gl : gp > 0 ? Infinity : 0, netPnl: round2(net, 4), netPnlPercent: this.state.account.initialCapital > 0 ? round2(net / this.state.account.initialCapital * 100, 4) : 0, averageTradePnl: total ? round2(net / total, 4) : 0, averageR: round2(avgR, 4), bestTradePnl: total ? round2(Math.max(...h.map((p) => p.realizedPnl)), 4) : 0, worstTradePnl: total ? round2(Math.min(...h.map((p) => p.realizedPnl)), 4) : 0, tp1HitRatePercent: total ? h.filter((p) => p.tp1Hit).length / total * 100 : 0, tp2HitRatePercent: total ? h.filter((p) => p.tp2Hit).length / total * 100 : 0, halted: this.state.account.halted }; - } -}; - -// server/paper/paperTradingLoop.ts -var PaperTradingLoop = class { - constructor(engine = new PaperTradingEngine(), symbol = "BTC/USDT", intervalMs = 15e3) { - this.timer = null; - this.ticking = false; - this.lastCandleTimestamp = null; - this.lastTickAt = null; - this.lastError = null; - this.engine = engine; - this.symbol = symbol; - this.intervalMs = intervalMs; - } - getEngine() { - return this.engine; - } - getStatus() { - return { running: this.timer !== null, symbol: this.symbol, intervalMs: this.intervalMs, lastTickAt: this.lastTickAt, lastCandleTimestamp: this.lastCandleTimestamp, lastError: this.lastError }; - } - async tick() { - if (this.ticking) return; - this.ticking = true; - try { - const series = await fetchRealCandles(this.symbol, "15m", 500); - const candles = series.candles; - if (candles.length < 50) throw new Error("insufficient_closed_candles"); - const closed = candles.slice(0, -1); - const candle = closed.at(-1); - if (!candle) throw new Error("no_closed_candle"); - this.lastTickAt = Date.now(); - this.lastError = null; - if (this.lastCandleTimestamp === candle.timestamp) return; - this.engine.processCandle(candle); - this.lastCandleTimestamp = candle.timestamp; - if (this.engine.getState().positions.length === 0 && !this.engine.getState().account.halted) { - const analysis = await analyzeMarket(this.symbol); - const signal = generateTradeSignal(analysis, candles.slice(0, -1)); - if (signal.direction !== "NO TRADE") this.engine.openFromSignal(signal, Date.now()); - } - } catch (error) { - this.lastError = error instanceof Error ? error.message : "paper_loop_error"; - } finally { - this.ticking = false; - } - } - start() { - if (this.timer) return; - void this.tick(); - this.timer = setInterval(() => void this.tick(), this.intervalMs); - } - stop() { - if (this.timer) { - clearInterval(this.timer); - this.timer = null; - } - } -}; -var paperTradingLoop = new PaperTradingLoop(); - -// server/quant/regimeAnalytics.ts -var mean2 = (values) => values.length ? values.reduce((a, b) => a + b, 0) / values.length : 0; -var round3 = (value, digits = 4) => Number((Number.isFinite(value) ? value : 0).toFixed(digits)); -function classifyRegime(candles, index) { - const start = Math.max(0, index - 49); - const window = candles.slice(start, index + 1); - if (window.length < 20) return "NEUTRAL"; - const closes2 = window.map((c) => c.close); - const returns = closes2.slice(1).map((v, i) => v / closes2[i] - 1); - const meanReturn = mean2(returns); - const volatility = Math.sqrt(mean2(returns.map((r) => (r - meanReturn) ** 2))); - const move = closes2.at(-1) / closes2[0] - 1; - const threshold = Math.max(volatility * 2, 15e-4); - if (volatility > 8e-3) return "HIGH_VOLATILITY"; - if (volatility < 25e-4) return "LOW_VOLATILITY"; - if (move > threshold * 2) return "TREND_BULL"; - if (move < -threshold * 2) return "TREND_BEAR"; - return "RANGE"; -} -function drawdownR(values) { - let equity = 0; - let peak = 0; - let max = 0; - for (const value of values) { - equity += value; - peak = Math.max(peak, equity); - max = Math.max(max, peak - equity); - } - return max; -} -function analyzeRegimes(candles, trades) { - const buckets = /* @__PURE__ */ new Map(); - for (const trade of trades) { - let entryIndex = candles.findIndex((c) => c.timestamp === trade.timestamp); - if (entryIndex < 0) entryIndex = candles.findIndex((c) => c.timestamp >= trade.timestamp); - if (entryIndex <= 0) continue; - const regime = classifyRegime(candles, entryIndex - 1); - buckets.set(regime, [...buckets.get(regime) ?? [], trade.pnlR]); - } - const allRegimes = ["TREND_BULL", "TREND_BEAR", "RANGE", "HIGH_VOLATILITY", "LOW_VOLATILITY", "NEUTRAL"]; - const result = allRegimes.map((regime) => { - const values = buckets.get(regime) ?? []; - const wins = values.filter((v) => v > 0).length; - const losses = values.filter((v) => v < 0).length; - const grossWin = values.filter((v) => v > 0).reduce((a, b) => a + b, 0); - const grossLoss = Math.abs(values.filter((v) => v < 0).reduce((a, b) => a + b, 0)); - return { regime, trades: values.length, wins, losses, winRatePercent: round3(values.length ? wins / values.length * 100 : 0, 2), netR: round3(values.reduce((a, b) => a + b, 0)), expectancyR: round3(mean2(values)), profitFactor: round3(grossLoss > 0 ? grossWin / grossLoss : grossWin > 0 ? Infinity : 0), maxDrawdownR: round3(drawdownR(values)) }; - }); - const active = result.filter((x) => x.trades > 0); - const strongest = active.filter((x) => x.trades >= 5).sort((a, b) => b.expectancyR - a.expectancyR)[0]?.regime ?? null; - const weakest = active.filter((x) => x.trades >= 5).sort((a, b) => a.expectancyR - b.expectancyR)[0]?.regime ?? null; - const dominant = active.sort((a, b) => b.trades - a.trades)[0]?.regime ?? "NEUTRAL"; - const warnings = []; - for (const bucket of active) { - if (bucket.trades < 5) warnings.push(`${bucket.regime}: amostra pequena (${bucket.trades} trades).`); - if (bucket.expectancyR < 0) warnings.push(`${bucket.regime}: expectancy negativa.`); - } - return { buckets: result, dominantRegime: dominant, strongestRegime: strongest, weakestRegime: weakest, warnings }; -} - -// server/quant/statisticalAnalysis.ts -var round4 = (v, d = 4) => Number((Number.isFinite(v) ? v : 0).toFixed(d)); -function median(values) { - if (!values.length) return 0; - const sorted = [...values].sort((a, b) => a - b); - const mid = Math.floor(sorted.length / 2); - return sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2; -} -function stats(values) { - const mean5 = values.length ? values.reduce((a, b) => a + b, 0) / values.length : 0; - const variance = values.length > 1 ? values.reduce((sum, value) => sum + (value - mean5) ** 2, 0) / (values.length - 1) : 0; - const wins = values.filter((v) => v > 0); - const losses = values.filter((v) => v < 0); - const grossWin = wins.reduce((a, b) => a + b, 0); - const grossLoss = Math.abs(losses.reduce((a, b) => a + b, 0)); - return { - mean: mean5, - median: median(values), - stdDev: Math.sqrt(variance), - wins: wins.length, - losses: losses.length, - profitFactor: grossLoss > 0 ? grossWin / grossLoss : grossWin > 0 ? Infinity : 0, - payoffRatio: wins.length && losses.length ? grossWin / wins.length / (grossLoss / losses.length) : 0 - }; -} -function streak(values) { - let wins = 0, losses = 0, maxWins = 0, maxLosses = 0; - for (const value of values) { - if (value > 0) { - wins++; - losses = 0; - maxWins = Math.max(maxWins, wins); - } else if (value < 0) { - losses++; - wins = 0; - maxLosses = Math.max(maxLosses, losses); - } else { - wins = 0; - losses = 0; - } - } - return { maxWins, maxLosses }; -} -function directionOf(trade) { - const candidate2 = trade; - const value = String(candidate2.direction ?? candidate2.side ?? candidate2.signal?.direction ?? "").toUpperCase(); - if (value.includes("LONG") || value === "BUY") return "LONG"; - if (value.includes("SHORT") || value === "SELL") return "SHORT"; - return "UNKNOWN"; -} -function directionBucket(direction, trades) { - const values = trades.map((t) => t.pnlR); - const s = stats(values); - const streaks = streak(values); - return { - direction, - trades: values.length, - wins: s.wins, - losses: s.losses, - winRatePercent: round4(values.length ? s.wins / values.length * 100 : 0, 2), - netR: round4(values.reduce((a, b) => a + b, 0)), - expectancyR: round4(s.mean), - medianR: round4(s.median), - stdDevR: round4(s.stdDev), - profitFactor: round4(s.profitFactor), - payoffRatio: round4(s.payoffRatio), - maxConsecutiveLosses: streaks.maxLosses, - maxConsecutiveWins: streaks.maxWins - }; -} -function analyzeStatistics(trades) { - const values = trades.map((t) => t.pnlR).filter(Number.isFinite); - const s = stats(values); - const streaks = streak(values); - const standardError = values.length > 1 ? s.stdDev / Math.sqrt(values.length) : 0; - const ci = values.length >= 30 ? { low: s.mean - 1.96 * standardError, high: s.mean + 1.96 * standardError } : null; - const groups = /* @__PURE__ */ new Map(); - for (const trade of trades) { - const direction = directionOf(trade); - groups.set(direction, [...groups.get(direction) ?? [], trade]); - } - const directions = ["LONG", "SHORT", "UNKNOWN"].map((direction) => directionBucket(direction, groups.get(direction) ?? [])); - const warnings = []; - if (values.length < 30) warnings.push(`Amostra estat\xEDstica pequena: ${values.length} trades.`); - if (values.length >= 30 && s.mean <= 0) warnings.push("Intervalo de confian\xE7a da expectancy n\xE3o parte de uma m\xE9dia positiva."); - if (s.stdDev > Math.abs(s.mean) * 3 && values.length >= 30) warnings.push("Alta dispers\xE3o dos resultados em rela\xE7\xE3o \xE0 expectancy."); - if (streaks.maxLosses >= 6) warnings.push(`Sequ\xEAncia m\xE1xima de ${streaks.maxLosses} perdas consecutivas.`); - for (const bucket of directions.filter((d) => d.trades > 0)) { - if (bucket.trades < 20) warnings.push(`${bucket.direction}: amostra abaixo de 20 trades.`); - if (bucket.expectancyR < 0) warnings.push(`${bucket.direction}: expectancy negativa.`); - } - return { - sampleSize: values.length, - meanR: round4(s.mean), - medianR: round4(s.median), - stdDevR: round4(s.stdDev), - standardErrorR: round4(standardError), - expectancyCi95R: ci ? { low: round4(ci.low), high: round4(ci.high) } : null, - positiveTradeRatePercent: round4(values.length ? s.wins / values.length * 100 : 0, 2), - payoffRatio: round4(s.payoffRatio), - maxConsecutiveLosses: streaks.maxLosses, - maxConsecutiveWins: streaks.maxWins, - directions, - warnings - }; -} - -// server/quant/robustnessAnalysis.ts -var round5 = (v, d = 4) => Number((Number.isFinite(v) ? v : 0).toFixed(d)); -function mean3(values) { - return values.length ? values.reduce((a, b) => a + b, 0) / values.length : 0; -} -function seededRandom(seed) { - let state = seed >>> 0; - return () => { - state = 1664525 * state + 1013904223 >>> 0; - return state / 4294967296; - }; -} -function bootstrap(values, iterations = 2e3, seed = 20260908) { - if (values.length < 30) return null; - const random = seededRandom(seed + values.length); - const samples = new Array(iterations); - for (let i = 0; i < iterations; i++) { - let total = 0; - for (let j = 0; j < values.length; j++) total += values[Math.floor(random() * values.length)]; - samples[i] = total / values.length; - } - samples.sort((a, b) => a - b); - return { low: samples[Math.floor(iterations * 0.025)], high: samples[Math.floor(iterations * 0.975)] }; -} -function analyzeRobustness(trades) { - const values = trades.map((t) => t.pnlR).filter(Number.isFinite); - if (values.length < 30) { - return { - sampleSize: values.length, - positiveExpectancy: mean3(values) > 0, - profitFactorAboveOne: false, - bootstrapExpectancy95: null, - stabilityScore: 0, - grade: "INSUFFICIENT_DATA", - checks: [], - warnings: ["S\xE3o necess\xE1rios pelo menos 30 trades para a an\xE1lise de robustez."] - }; - } - const wins = values.filter((v) => v > 0).reduce((a, b) => a + b, 0); - const losses = Math.abs(values.filter((v) => v < 0).reduce((a, b) => a + b, 0)); - const pf2 = losses > 0 ? wins / losses : Infinity; - const expectancy = mean3(values); - const bootstrapCi = bootstrap(values); - const checks = []; - const warnings = []; - let score2 = 0; - if (expectancy > 0) { - score2 += 25; - checks.push("Expectancy hist\xF3rica positiva."); - } else warnings.push("Expectancy hist\xF3rica negativa."); - if (pf2 > 1.2) { - score2 += 25; - checks.push("Profit Factor acima de 1.20."); - } else warnings.push("Profit Factor n\xE3o supera 1.20."); - if (bootstrapCi && bootstrapCi.low > 0) { - score2 += 30; - checks.push("Bootstrap 95% da expectancy permanece acima de zero."); - } else warnings.push("Bootstrap 95% ainda inclui expectancy n\xE3o positiva."); - const median2 = [...values].sort((a, b) => a - b)[Math.floor(values.length / 2)]; - if (Math.sign(expectancy) === Math.sign(median2) && median2 > 0) { - score2 += 10; - checks.push("Mediana e m\xE9dia possuem sinal positivo."); - } else warnings.push("Distribui\xE7\xE3o n\xE3o confirma claramente a vantagem pela mediana."); - const firstHalf = mean3(values.slice(0, Math.floor(values.length / 2))); - const secondHalf = mean3(values.slice(Math.floor(values.length / 2))); - if (firstHalf > 0 && secondHalf > 0) { - score2 += 10; - checks.push("Expectancy positiva nas duas metades da amostra."); - } else warnings.push("Uma das metades da amostra apresenta expectancy n\xE3o positiva."); - const grade = score2 >= 80 ? "ROBUST" : score2 >= 55 ? "MODERATE" : "FRAGILE"; - return { - sampleSize: values.length, - positiveExpectancy: expectancy > 0, - profitFactorAboveOne: pf2 > 1, - bootstrapExpectancy95: bootstrapCi ? { low: round5(bootstrapCi.low), high: round5(bootstrapCi.high) } : null, - stabilityScore: score2, - grade, - checks, - warnings - }; -} - -// server/quant/timeSeriesAnalysis.ts -var round6 = (v, d = 4) => Number((Number.isFinite(v) ? v : 0).toFixed(d)); -function periodKey(timestamp) { - const date = new Date(timestamp); - return `${date.getUTCFullYear()}-${String(date.getUTCMonth() + 1).padStart(2, "0")}`; -} -function analyzeTimeSeries(trades) { - const groups = /* @__PURE__ */ new Map(); - for (const trade of trades) { - const key = periodKey(trade.timestamp); - groups.set(key, [...groups.get(key) ?? [], trade]); - } - const periods = [...groups.entries()].sort((a, b) => a[0].localeCompare(b[0])).map(([period, items]) => { - const values = items.map((t) => t.pnlR).filter(Number.isFinite); - const wins = values.filter((v) => v > 0).reduce((a, b) => a + b, 0); - const losses = Math.abs(values.filter((v) => v < 0).reduce((a, b) => a + b, 0)); - return { - period, - trades: values.length, - netR: round6(values.reduce((a, b) => a + b, 0)), - expectancyR: round6(values.length ? values.reduce((a, b) => a + b, 0) / values.length : 0), - winRatePercent: round6(values.length ? values.filter((v) => v > 0).length / values.length * 100 : 0, 2), - profitFactor: round6(losses > 0 ? wins / losses : wins > 0 ? Infinity : 0) - }; - }); - const active = periods.filter((p) => p.trades > 0); - const positive2 = active.filter((p) => p.netR > 0).length; - const negative = active.filter((p) => p.netR < 0).length; - const warnings = []; - if (active.length < 3) warnings.push("Menos de tr\xEAs per\xEDodos dispon\xEDveis para avaliar estabilidade temporal."); - for (const period of active) if (period.trades < 10) warnings.push(`${period.period}: amostra pequena (${period.trades} trades).`); - if (active.length && positive2 / active.length < 0.5) warnings.push("Menos da metade dos per\xEDodos apresentou resultado l\xEDquido positivo."); - return { - periods, - positivePeriods: positive2, - negativePeriods: negative, - consistencyPercent: round6(active.length ? positive2 / active.length * 100 : 0, 2), - bestPeriod: active.length ? [...active].sort((a, b) => b.expectancyR - a.expectancyR)[0].period : null, - worstPeriod: active.length ? [...active].sort((a, b) => a.expectancyR - b.expectancyR)[0].period : null, - warnings - }; -} - -// server/quant/robustParameterSelection.ts -var round7 = (v, d = 4) => Number((Number.isFinite(v) ? v : 0).toFixed(d)); -var pf = (r) => Number.isFinite(r.profitFactor) ? r.profitFactor : r.profitFactor > 0 ? 99 : 0; -function score(r) { - if (r.totalTrades < 20) return 0; - const expectancy = Math.max(-1, Math.min(2, r.expectancyR)); - const profitFactor2 = Math.max(0, Math.min(3, pf(r))); - const ddPenalty = Math.max(0, Math.min(1, r.maxDrawdownPercent / 20)); - const sampleBonus = Math.min(1, r.totalTrades / 100); - return round7(Math.max(0, expectancy / 2) * 35 + Math.min(1, profitFactor2 / 2) * 30 + (1 - ddPenalty) * 20 + sampleBonus * 15, 2); -} -function candidate(id, options, r) { - const rankScore = score(r); - return { id, minScore: options.minScore ?? 35, minConfidence: options.minConfidence ?? 50, atrStopMultiple: options.atrStopMultiple ?? 1.5, rewardRisk: options.rewardRisk ?? 2, maxHoldingBars: options.maxHoldingBars ?? 32, trades: r.totalTrades, expectancyR: round7(r.expectancyR), profitFactor: round7(pf(r)), netProfitPercent: round7(r.netProfitPercent, 2), maxDrawdownPercent: round7(r.maxDrawdownPercent, 2), stabilityScore: rankScore, rankScore }; -} -function configKey(config) { - return config.join("-"); -} -function selectRobustParameters(symbol, candles, baseOptions2 = {}) { - const base = [baseOptions2.minScore ?? 35, baseOptions2.minConfidence ?? 50, baseOptions2.atrStopMultiple ?? 1.5, baseOptions2.rewardRisk ?? 2, baseOptions2.maxHoldingBars ?? 32]; - const configs = [base]; - const add = (config) => { - if (!configs.some((existing) => configKey(existing) === configKey(config))) configs.push(config); - }; - add([30, base[1], base[2], base[3], base[4]]); - add([40, base[1], base[2], base[3], base[4]]); - add([base[0], 45, base[2], base[3], base[4]]); - add([base[0], 55, base[2], base[3], base[4]]); - add([base[0], base[1], 1.25, base[3], base[4]]); - add([base[0], base[1], 1.75, base[3], base[4]]); - add([base[0], base[1], base[2], 1.75, base[4]]); - add([base[0], base[1], base[2], 2.25, base[4]]); - add([base[0], base[1], base[2], base[3], 24]); - add([base[0], base[1], base[2], base[3], 40]); - add([30, 45, 1.25, 1.75, 24]); - add([40, 55, 1.75, 2.25, 40]); - add([30, 55, 1.75, 2.25, 32]); - add([40, 45, 1.25, 2, 40]); - const candidates = []; - for (const [s, c, a, r, h] of configs) { - const options = { ...baseOptions2, minScore: s, minConfidence: c, atrStopMultiple: a, rewardRisk: r, maxHoldingBars: h }; - candidates.push(candidate(`${s}-${c}-${a}-${r}-${h}`, options, runHistoricalBacktest({ ...options, symbol, candles }))); - } - const baseline = candidates.find((c) => c.minScore === base[0] && c.minConfidence === base[1] && c.atrStopMultiple === base[2] && c.rewardRisk === base[3] && c.maxHoldingBars === base[4]) ?? null; - for (const c of candidates) { - const nearby = candidates.filter((n) => { - const distance = Math.abs(n.minScore - c.minScore) / 10 + Math.abs(n.minConfidence - c.minConfidence) / 10 + Math.abs(n.atrStopMultiple - c.atrStopMultiple) / 0.25 + Math.abs(n.rewardRisk - c.rewardRisk) / 0.25 + Math.abs(n.maxHoldingBars - c.maxHoldingBars) / 8; - return distance <= 2.01 && n.trades >= 30; - }); - const viable = nearby.filter((n) => n.expectancyR > 0 && n.profitFactor > 1).length; - c.stabilityScore = round7(c.rankScore * 0.7 + (nearby.length ? viable / nearby.length * 100 : 0) * 0.3, 2); - } - const eligible = candidates.filter((c) => c.trades >= 30 && c.expectancyR > 0 && c.profitFactor > 1); - eligible.sort((a, b) => b.stabilityScore - a.stabilityScore || b.rankScore - a.rankScore || b.trades - a.trades); - const selected = eligible[0] ?? null; - const stable = candidates.filter((c) => selected && Math.abs(c.stabilityScore - selected.stabilityScore) <= 8 && c.trades >= 30 && c.expectancyR > 0 && c.profitFactor > 1); - const warnings = []; - if (selected && baseline && selected.id !== baseline.id) warnings.push("A configura\xE7\xE3o selecionada difere do baseline. Validar em OOS antes de qualquer uso operacional."); - if (candles.length < 1500) warnings.push("Hist\xF3rico curto para otimiza\xE7\xE3o robusta; aumentar a janela antes de concluir sobre par\xE2metros."); - if (!selected) warnings.push("Nenhuma configura\xE7\xE3o atingiu os crit\xE9rios m\xEDnimos de robustez."); - warnings.push("Busca compacta de par\xE2metros: valida\xE7\xE3o OOS e estabilidade regional continuam obrigat\xF3rias."); - const grade = selected === null ? "INSUFFICIENT_DATA" : stable.length >= 7 ? "ROBUST" : stable.length >= 3 ? "PROMISING" : "FRAGILE"; - return { candidates: candidates.sort((a, b) => b.stabilityScore - a.stabilityScore).slice(0, 30), selected, baseline, stableRegion: { minScore: [...new Set(stable.map((c) => c.minScore))].sort((a, b) => a - b), minConfidence: [...new Set(stable.map((c) => c.minConfidence))].sort((a, b) => a - b), atrStopMultiple: [...new Set(stable.map((c) => c.atrStopMultiple))].sort((a, b) => a - b), rewardRisk: [...new Set(stable.map((c) => c.rewardRisk))].sort((a, b) => a - b), maxHoldingBars: [...new Set(stable.map((c) => c.maxHoldingBars))].sort((a, b) => a - b) }, grade, warnings }; -} - -// server/quant/walkForwardAnalysis.ts -var round8 = (v, d = 4) => Number((Number.isFinite(v) ? v : 0).toFixed(d)); -function runWindow(symbol, candles, base) { - return runHistoricalBacktest({ ...base, symbol, candles }); -} -function calculateOosMetrics(trades, riskPerTradePercent) { - let equity = 100; - let peak = equity; - let maxDrawdownPercent = 0; - const riskFraction = Math.max(1e-4, riskPerTradePercent) / 100; - for (const trade of trades) { - equity *= Math.max(0, 1 + trade.pnlR * riskFraction); - peak = Math.max(peak, equity); - if (peak > 0) maxDrawdownPercent = Math.max(maxDrawdownPercent, (peak - equity) / peak * 100); - } - const wins = trades.filter((t) => t.pnlR > 0).reduce((a, t) => a + t.pnlR, 0); - const losses = Math.abs(trades.filter((t) => t.pnlR < 0).reduce((a, t) => a + t.pnlR, 0)); - return { netProfitPercent: equity - 100, maxDrawdownPercent, profitFactor: losses > 0 ? wins / losses : wins > 0 ? Infinity : 0 }; -} -function runWalkForwardAnalysis(symbol, candles, baseOptions2 = {}, trainBars = 2e3, testBars = 500, stepBars = 500) { - const sorted = [...candles].sort((a, b) => a.timestamp - b.timestamp); - const windows = []; - const allTestTrades = []; - let windowIndex = 0; - for (let trainStart = 0; trainStart + trainBars + testBars <= sorted.length; trainStart += stepBars) { - const train = sorted.slice(trainStart, trainStart + trainBars); - const test = sorted.slice(trainStart + trainBars, trainStart + trainBars + testBars); - const selection = selectRobustParameters(symbol, train, baseOptions2); - const selected = selection.selected; - const trainResult = selected ? runWindow(symbol, train, { ...baseOptions2, minScore: selected.minScore, minConfidence: selected.minConfidence, atrStopMultiple: selected.atrStopMultiple, rewardRisk: selected.rewardRisk, maxHoldingBars: selected.maxHoldingBars }) : runWindow(symbol, train, baseOptions2); - const testResult = selected ? runWindow(symbol, test, { ...baseOptions2, minScore: selected.minScore, minConfidence: selected.minConfidence, atrStopMultiple: selected.atrStopMultiple, rewardRisk: selected.rewardRisk, maxHoldingBars: selected.maxHoldingBars }) : runWindow(symbol, test, baseOptions2); - const enough = !!selected && trainResult.totalTrades >= 20 && testResult.totalTrades >= 10; - const pass = enough && testResult.expectancyR > 0 && testResult.profitFactor > 1; - windows.push({ - index: windowIndex++, - trainStart: trainResult.startDate, - trainEnd: trainResult.endDate, - testStart: testResult.startDate, - testEnd: testResult.endDate, - trainTrades: trainResult.totalTrades, - testTrades: testResult.totalTrades, - trainExpectancyR: round8(trainResult.expectancyR), - testExpectancyR: round8(testResult.expectancyR), - trainNetProfitPercent: round8(trainResult.netProfitPercent, 2), - testNetProfitPercent: round8(testResult.netProfitPercent, 2), - testProfitFactor: round8(testResult.profitFactor), - testMaxDrawdownPercent: round8(testResult.maxDrawdownPercent, 2), - selectedParameters: selected ? { minScore: selected.minScore, minConfidence: selected.minConfidence, atrStopMultiple: selected.atrStopMultiple, rewardRisk: selected.rewardRisk, maxHoldingBars: selected.maxHoldingBars } : null, - status: !enough ? "INSUFFICIENT_DATA" : pass ? "PASS" : "FAIL" - }); - allTestTrades.push(...testResult.trades); - } - const wins = allTestTrades.filter((t) => t.pnlR > 0); - const losses = allTestTrades.filter((t) => t.pnlR < 0); - const grossWin = wins.reduce((a, t) => a + t.pnlR, 0); - const grossLoss = Math.abs(losses.reduce((a, t) => a + t.pnlR, 0)); - const oosExpectancy = allTestTrades.length ? allTestTrades.reduce((a, t) => a + t.pnlR, 0) / allTestTrades.length : 0; - const passed = windows.filter((w) => w.status === "PASS").length; - const evaluated = windows.filter((w) => w.status !== "INSUFFICIENT_DATA").length; - const oosMetrics = calculateOosMetrics(allTestTrades, baseOptions2.riskPerTradePercent ?? 1); - const consistency = evaluated ? passed / evaluated * 100 : 0; - const warnings = []; - if (windows.length < 3) warnings.push("Menos de tr\xEAs janelas walk-forward dispon\xEDveis."); - if (allTestTrades.length < 30) warnings.push(`Amostra OOS pequena: ${allTestTrades.length} trades.`); - if (evaluated && consistency < 60) warnings.push("Menos de 60% das janelas adaptativas foram positivas."); - if (oosExpectancy <= 0) warnings.push("Expectancy agregada OOS n\xE3o \xE9 positiva."); - if (windows.some((w) => w.selectedParameters === null)) warnings.push("Uma ou mais janelas n\xE3o encontraram par\xE2metros robustos no treino."); - const grade = allTestTrades.length < 30 || evaluated < 3 ? "INSUFFICIENT_DATA" : consistency >= 75 && oosExpectancy > 0 ? "ROBUST" : consistency >= 60 && oosExpectancy > 0 ? "PROMISING" : "FRAGILE"; - return { windows, trainBars, testBars, stepBars, adaptive: true, outOfSampleTrades: allTestTrades.length, outOfSampleExpectancyR: round8(oosExpectancy), outOfSampleNetProfitPercent: round8(oosMetrics.netProfitPercent, 2), outOfSampleWinRatePercent: round8(allTestTrades.length ? wins.length / allTestTrades.length * 100 : 0, 2), outOfSampleProfitFactor: round8(grossLoss > 0 ? grossWin / grossLoss : grossWin > 0 ? Infinity : 0), outOfSampleMaxDrawdownPercent: round8(oosMetrics.maxDrawdownPercent, 2), passedWindows: passed, consistencyPercent: round8(consistency, 2), grade, warnings }; -} - -// server/quant/stressTest.ts -var round9 = (v, d = 4) => Number((Number.isFinite(v) ? v : 0).toFixed(d)); -var displayProfitFactor = (v) => Number.isFinite(v) ? round9(v) : v > 0 ? 99 : 0; -function summarize(result, fee, slippage, latency, funding, base) { - const netDelta = base ? result.netProfitPercent - base.netProfitPercent : 0; - const expDelta = base ? result.expectancyR - base.expectancyR : 0; - const ddDelta = base ? result.maxDrawdownPercent - base.maxDrawdownPercent : 0; - const enough = result.totalTrades >= 30; - const positive2 = result.expectancyR > 0 && result.profitFactor > 1; - const baseExpectancy = base?.expectancyR ?? result.expectancyR; - const expectancyThreshold = baseExpectancy > 0 ? baseExpectancy * 0.7 : 0; - const status = !enough ? "INSUFFICIENT_DATA" : !positive2 ? "FAIL" : result.expectancyR >= expectancyThreshold ? "PASS" : "DEGRADED"; - return { name: `${fee}bps fee / ${slippage}bps slip / ${latency}bps latency / ${(funding * 100).toFixed(3)}% funding/8h`, feeBpsPerSide: fee, slippageBpsPerSide: slippage, latencySlippageBpsPerSide: latency, fundingRatePer8h: funding, result: { trades: result.totalTrades, netProfitPercent: round9(result.netProfitPercent, 2), expectancyR: round9(result.expectancyR), profitFactor: displayProfitFactor(result.profitFactor), maxDrawdownPercent: round9(result.maxDrawdownPercent, 2), winRatePercent: round9(result.winRate, 2) }, deltaFromBase: { netProfitPercent: round9(netDelta, 2), expectancyR: round9(expDelta), maxDrawdownPercent: round9(ddDelta, 2) }, status }; -} -function runStressTest(symbol, candles, baseOptions2 = {}) { - const baseResult = runHistoricalBacktest({ ...baseOptions2, symbol, candles, feeBpsPerSide: 5, slippageBpsPerSide: 2, latencySlippageBpsPerSide: 1, fundingRatePer8h: 1e-4 }); - const scenarios = [ - [7, 3, 1, 1e-4], - [10, 5, 2, 2e-4], - [15, 8, 3, 3e-4], - [20, 10, 5, 5e-4] - ].map(([fee, slippage, latency, funding]) => summarize( - runHistoricalBacktest({ ...baseOptions2, symbol, candles, feeBpsPerSide: fee, slippageBpsPerSide: slippage, latencySlippageBpsPerSide: latency, fundingRatePer8h: funding }), - fee, - slippage, - latency, - funding, - baseResult - )); - const base = summarize(baseResult, 5, 2, 1, 1e-4); - const passed = scenarios.filter((s) => s.status === "PASS").length; - const evaluated = scenarios.filter((s) => s.status !== "INSUFFICIENT_DATA").length; - const warnings = []; - if (base.result.trades < 30) warnings.push("Amostra base inferior a 30 trades."); - if (evaluated < scenarios.length) warnings.push("Alguns cen\xE1rios n\xE3o possuem amostra suficiente."); - if (scenarios.some((s) => s.status === "FAIL")) warnings.push("A estrat\xE9gia perde expectancy positiva em pelo menos um cen\xE1rio de custos."); - if (scenarios.some((s) => s.result.maxDrawdownPercent > 15)) warnings.push("Drawdown acima de 15% em cen\xE1rio de stress."); - const grade = base.result.trades < 30 ? "INSUFFICIENT_DATA" : passed === scenarios.length ? "RESILIENT" : passed >= 2 ? "MODERATE" : "FRAGILE"; - return { base, scenarios, passedScenarios: passed, grade, warnings }; -} - -// server/quant/monteCarloAnalysis.ts -var round10 = (v, d = 4) => Number((Number.isFinite(v) ? v : 0).toFixed(d)); -function rng(seed) { - let s = seed >>> 0; - return () => { - s = 1664525 * s + 1013904223 >>> 0; - return s / 4294967296; - }; -} -function percentile(values, p) { - if (!values.length) return 0; - const index = (values.length - 1) * p; - const lower = Math.floor(index); - const upper = Math.ceil(index); - if (lower === upper) return values[lower]; - return values[lower] + (values[upper] - values[lower]) * (index - lower); -} -function analyzeMonteCarlo(trades, simulations = 3e3, riskPerTradePercent = 1) { - const values = trades.map((t) => t.pnlR).filter(Number.isFinite); - const safeRiskPercent = Number.isFinite(riskPerTradePercent) && riskPerTradePercent > 0 ? riskPerTradePercent : 1; - const riskFraction = safeRiskPercent / 100; - if (values.length < 30) return { - simulations, - sampleSize: values.length, - expectancyR: round10(values.length ? values.reduce((a, b) => a + b, 0) / values.length : 0), - medianFinalR: 0, - p05FinalR: 0, - p95FinalR: 0, - medianMaxDrawdownR: 0, - p95MaxDrawdownR: 0, - probabilityOfLossPercent: 0, - probabilityOfDrawdownOver10RPercent: 0, - grade: "INSUFFICIENT_DATA", - warnings: ["S\xE3o necess\xE1rios pelo menos 30 trades para Monte Carlo."] - }; - const random = rng(20260908 + values.length + Math.round(safeRiskPercent * 100)); - const finals = []; - const drawdowns = []; - let losses = 0; - let largeDrawdowns = 0; - for (let i = 0; i < simulations; i++) { - let equity = 1; - let peak = 1; - let maxDdFraction = 0; - for (let j = 0; j < values.length; j++) { - const tradeR = values[Math.floor(random() * values.length)]; - equity *= Math.max(0, 1 + tradeR * riskFraction); - peak = Math.max(peak, equity); - maxDdFraction = Math.max(maxDdFraction, peak > 0 ? (peak - equity) / peak : 0); - } - finals.push((equity - 1) / riskFraction); - drawdowns.push(maxDdFraction / riskFraction); - if (equity < 1) losses++; - if (maxDdFraction / riskFraction > 10) largeDrawdowns++; - } - finals.sort((a, b) => a - b); - drawdowns.sort((a, b) => a - b); - const probabilityLoss = losses / simulations * 100; - const probabilityLargeDd = largeDrawdowns / simulations * 100; - const warnings = []; - if (probabilityLoss > 25) warnings.push(`Monte Carlo: ${round10(probabilityLoss, 2)}% das simula\xE7\xF5es terminaram abaixo do capital inicial.`); - if (probabilityLargeDd > 25) warnings.push(`Monte Carlo: ${round10(probabilityLargeDd, 2)}% das simula\xE7\xF5es excederam 10R de drawdown.`); - const grade = probabilityLoss < 5 && probabilityLargeDd < 10 ? "ROBUST" : probabilityLoss < 15 && probabilityLargeDd < 25 ? "MODERATE" : "FRAGILE"; - return { - simulations, - sampleSize: values.length, - expectancyR: round10(values.reduce((a, b) => a + b, 0) / values.length), - medianFinalR: round10(percentile(finals, 0.5)), - p05FinalR: round10(percentile(finals, 0.05)), - p95FinalR: round10(percentile(finals, 0.95)), - medianMaxDrawdownR: round10(percentile(drawdowns, 0.5)), - p95MaxDrawdownR: round10(percentile(drawdowns, 0.95)), - probabilityOfLossPercent: round10(probabilityLoss, 2), - probabilityOfDrawdownOver10RPercent: round10(probabilityLargeDd, 2), - grade, - warnings - }; -} - -// server/quant/quantitativeLab.ts -var finite2 = (v, fallback = 0) => Number.isFinite(v) ? v : fallback; -var round11 = (v, d = 4) => Number(finite2(v).toFixed(d)); -var delta = (a, b) => round11(a - b); -function backtestMetrics(result) { - const wins = result.trades.filter((t) => t.pnlR > 0).length; - const losses = result.trades.filter((t) => t.pnlR < 0).length; - return { trades: result.totalTrades, winRatePercent: round11(result.winRate), profitFactor: finite2(result.profitFactor), netPnl: round11(result.finalCapital - result.initialCapital, 2), netPnlPercent: round11(result.netProfitPercent), expectancyR: round11(result.expectancyR), averageR: round11(result.averageRR), maxDrawdownPercent: round11(result.maxDrawdownPercent), bestTradePnl: result.trades.length ? round11(Math.max(...result.trades.map((t) => t.pnlPercent)), 4) : 0, worstTradePnl: result.trades.length ? round11(Math.min(...result.trades.map((t) => t.pnlPercent)), 4) : 0, tp1HitRatePercent: round11(result.trades.filter((t) => t.status === "TP ATINGIDO").length / Math.max(1, result.totalTrades) * 100), tp2HitRatePercent: 0, fees: round11(result.totalFeesPercent, 4), slippage: round11(result.totalSlippagePercent, 4), initialCapital: result.initialCapital, finalCapital: result.finalCapital, periodDays: round11(result.periodDays, 2), sharpeRatio: round11(result.sharpeRatio), sortinoRatio: round11(result.sortinoRatio), grossExpectancyR: round11(result.grossExpectancyR), tradeDistribution: { wins, losses, breakevens: result.totalTrades - wins - losses }, equityCurve: result.equityCurve }; -} -function paperMetrics(state) { - return { trades: state.stats.totalTrades, winRatePercent: round11(state.stats.winRatePercent), profitFactor: finite2(state.stats.profitFactor), netPnl: round11(state.account.realizedPnl, 2), netPnlPercent: round11(state.stats.netPnlPercent), expectancyR: round11(state.stats.averageR), averageR: round11(state.stats.averageR), maxDrawdownPercent: round11(state.account.maxDrawdownPercent), bestTradePnl: round11(state.stats.bestTradePnl, 4), worstTradePnl: round11(state.stats.worstTradePnl, 4), tp1HitRatePercent: round11(state.stats.tp1HitRatePercent), tp2HitRatePercent: round11(state.stats.tp2HitRatePercent), fees: round11(state.account.feesPaid, 4), slippage: round11(state.account.slippagePaid, 4), initialCapital: state.account.initialCapital, equity: state.account.equity, openPositions: state.positions.length, halted: state.account.halted, history: state.history }; -} -function buildComparison(backtest, paper) { - const common = { netPnlDeltaPercent: delta(paper.netPnlPercent, backtest.netPnlPercent), winRateDeltaPercent: delta(paper.winRatePercent, backtest.winRatePercent), expectancyDeltaR: delta(paper.expectancyR, backtest.expectancyR), drawdownDeltaPercent: delta(paper.maxDrawdownPercent, backtest.maxDrawdownPercent), profitFactorDelta: delta(paper.profitFactor, backtest.profitFactor) }; - if (backtest.trades < 30 || paper.trades < 10) return { ...common, status: "INSUFFICIENT_DATA", reasons: ["S\xE3o necess\xE1rios pelo menos 30 trades de backtest e 10 de Paper Trading para compara\xE7\xE3o robusta."] }; - const reasons = []; - if (Math.abs(common.winRateDeltaPercent) > 15) reasons.push(`Win rate diverge ${round11(Math.abs(common.winRateDeltaPercent), 2)} pontos percentuais.`); - if (Math.abs(common.expectancyDeltaR) > 0.25) reasons.push(`Expectancy diverge ${round11(Math.abs(common.expectancyDeltaR), 3)}R.`); - if (Math.abs(common.drawdownDeltaPercent) > 5) reasons.push(`Drawdown m\xE1ximo diverge ${round11(Math.abs(common.drawdownDeltaPercent), 2)} pontos percentuais.`); - return { ...common, status: reasons.length ? "DIVERGENT" : "ALIGNED", reasons: reasons.length ? reasons : ["M\xE9tricas principais est\xE3o dentro das bandas de alinhamento definidas."] }; -} -function assessQuality(result) { - if (result.totalTrades < 30) return { score: 0, grade: "INSUFFICIENT_DATA", checks: [`Amostra: ${result.totalTrades} trades.`], warnings: ["Menos de 30 trades."] }; - let score2 = 0; - const checks = []; - const warnings = []; - if (result.expectancyR > 0) { - score2 += 25; - checks.push("Expectancy positiva."); - } else warnings.push("Expectancy n\xE3o \xE9 positiva."); - if (result.profitFactor > 1.2) { - score2 += 20; - checks.push("Profit Factor acima de 1.20."); - } else warnings.push("Profit Factor baixo."); - if (result.maxDrawdownPercent < 10) { - score2 += 20; - checks.push("Drawdown abaixo de 10%."); - } else warnings.push("Drawdown elevado."); - if (result.winRate >= 45) { - score2 += 15; - checks.push("Win rate >= 45%."); - } else warnings.push("Win rate abaixo de 45%."); - if (result.sharpeRatio > 1) { - score2 += 10; - checks.push("Sharpe acima de 1."); - } else warnings.push("Sharpe n\xE3o supera 1."); - if (result.totalTrades >= 100) { - score2 += 10; - checks.push("Amostra >= 100 trades."); - } else warnings.push("Amostra ainda abaixo de 100 trades."); - const grade = score2 >= 85 ? "A" : score2 >= 70 ? "B" : score2 >= 50 ? "C" : "D"; - return { score: score2, grade, checks, warnings }; -} -var baseOptions = { initialCapital: 1e4, riskPerTradePercent: 1, minScore: 35, minConfidence: 50, atrStopMultiple: 1.5, rewardRisk: 2, maxHoldingBars: 32, warmupBars: 220 }; -var emptySelection = () => ({ candidates: [], selected: null, baseline: null, stableRegion: { minScore: [], minConfidence: [], atrStopMultiple: [], rewardRisk: [], maxHoldingBars: [] }, grade: "INSUFFICIENT_DATA", warnings: ["Otimiza\xE7\xE3o de par\xE2metros \xE9 executada apenas pela valida\xE7\xE3o OOS."] }); -function buildQuantitativeLab(result, paperState, candles) { - const backtest = backtestMetrics(result); - const paper = paperMetrics(paperState); - return { generatedAt: Date.now(), symbol: result.symbol, backtest, paper, comparison: buildComparison(backtest, paper), quality: assessQuality(result), regimes: analyzeRegimes(candles, result.trades), statistics: analyzeStatistics(result.trades), robustness: analyzeRobustness(result.trades), timeSeries: analyzeTimeSeries(result.trades), walkForward: runWalkForwardAnalysis(result.symbol, candles, baseOptions), stressTest: runStressTest(result.symbol, candles, baseOptions), monteCarlo: analyzeMonteCarlo(result.trades, 3e3, baseOptions.riskPerTradePercent), parameterSelection: emptySelection() }; -} - -// server/quant/multiRegimeAnalytics.ts -var round12 = (value, digits = 4) => Number((Number.isFinite(value) ? value : 0).toFixed(digits)); -var mean4 = (values) => values.length ? values.reduce((a, b) => a + b, 0) / values.length : 0; -function profitFactor(values) { - const wins = values.filter((v) => v > 0).reduce((a, b) => a + b, 0); - const losses = Math.abs(values.filter((v) => v < 0).reduce((a, b) => a + b, 0)); - return losses > 0 ? wins / losses : wins > 0 ? Infinity : 0; -} -function drawdownR2(values) { - let equity = 0; - let peak = 0; - let max = 0; - for (const value of values) { - equity += value; - peak = Math.max(peak, equity); - max = Math.max(max, peak - equity); - } - return max; -} -function percentile2(values, p) { - if (!values.length) return 0; - const sorted = [...values].sort((a, b) => a - b); - return sorted[Math.min(sorted.length - 1, Math.max(0, Math.floor((sorted.length - 1) * p)))]; -} -function latestFundingAtOrBefore(funding, timestamp) { - let latest = null; - for (const row of funding) { - if (row.timestamp > timestamp) break; - latest = row; - } - return latest; -} -function classifyTrend(candles, index) { - const start = Math.max(0, index - 63); - const window = candles.slice(start, index + 1); - if (window.length < 32) return "RANGE"; - const closes2 = window.map((c) => c.close); - const move = closes2.at(-1) / closes2[0] - 1; - const volatility = Math.sqrt(mean4(closes2.slice(1).map((v, i) => { - const r = v / closes2[i] - 1; - return r * r; - }))); - const threshold = Math.max(volatility * 2.5, 3e-3); - if (move >= threshold) return "BULL"; - if (move <= -threshold) return "BEAR"; - return "RANGE"; -} -function classifyVolatility(candles, index) { - const start = Math.max(0, index - 95); - const window = candles.slice(start, index + 1); - if (window.length < 32) return "NORMAL"; - const returns = window.slice(1).map((c, i) => c.close / window[i].close - 1); - const currentWindow = returns.slice(-31); - const current = Math.sqrt(mean4(currentWindow.map((r) => r * r))); - const history = []; - for (let i = 31; i < returns.length; i += 1) { - const sample = returns.slice(i - 31, i + 1); - history.push(Math.sqrt(mean4(sample.map((r) => r * r)))); - } - const low = percentile2(history, 0.33); - const high = percentile2(history, 0.67); - if (current <= low) return "LOW"; - if (current >= high) return "HIGH"; - return "NORMAL"; -} -function classifyFunding(funding, timestamp, direction) { - const latest = latestFundingAtOrBefore(funding, timestamp); - if (!latest) return "UNAVAILABLE"; - const abs = Math.abs(latest.fundingRate); - const neutralThreshold = 5e-5; - if (abs <= neutralThreshold) return "NEUTRAL"; - const favorable = direction === "LONG" ? latest.fundingRate < 0 : latest.fundingRate > 0; - return favorable ? "FAVORABLE" : "ADVERSE"; -} -function validateMultiRegime(candles, trades, historicalFunding = [], minimumTradesPerCell = 10) { - const safeCandles = [...candles].sort((a, b) => a.timestamp - b.timestamp); - const safeFunding = [...historicalFunding].filter((x) => Number.isFinite(x.timestamp) && Number.isFinite(x.fundingRate)).sort((a, b) => a.timestamp - b.timestamp); - const trendValues = ["BULL", "BEAR", "RANGE"]; - const volatilityValues = ["LOW", "NORMAL", "HIGH"]; - const fundingValues = safeFunding.length ? ["FAVORABLE", "NEUTRAL", "ADVERSE"] : ["FAVORABLE", "NEUTRAL", "ADVERSE", "UNAVAILABLE"]; - const buckets = /* @__PURE__ */ new Map(); - for (const trade of trades) { - let entryIndex = safeCandles.findIndex((c) => c.timestamp === trade.timestamp); - if (entryIndex < 0) entryIndex = safeCandles.findIndex((c) => c.timestamp >= trade.timestamp); - if (entryIndex <= 0) continue; - const signalIndex = entryIndex - 1; - const trend = classifyTrend(safeCandles, signalIndex); - const volatility = classifyVolatility(safeCandles, signalIndex); - const funding = classifyFunding(safeFunding, safeCandles[signalIndex].timestamp, trade.direction); - const key = `${trend}|${volatility}|${funding}`; - buckets.set(key, [...buckets.get(key) ?? [], trade.pnlR]); - } - const cells = []; - for (const trend of trendValues) { - for (const volatility of volatilityValues) { - for (const funding of fundingValues) { - const key = `${trend}|${volatility}|${funding}`; - const values = buckets.get(key) ?? []; - const wins = values.filter((v) => v > 0).length; - cells.push({ - key, - trend, - volatility, - funding, - trades: values.length, - wins, - winRatePercent: round12(values.length ? wins / values.length * 100 : 0, 2), - netR: round12(values.reduce((a, b) => a + b, 0)), - expectancyR: round12(mean4(values)), - profitFactor: round12(profitFactor(values)), - maxDrawdownR: round12(drawdownR2(values)) - }); - } - } - } - const covered = cells.filter((c) => c.trades > 0); - const evaluated = cells.filter((c) => c.trades >= minimumTradesPerCell); - const positive2 = evaluated.filter((c) => c.expectancyR > 0); - const coveragePercent = cells.length ? covered.length / cells.length * 100 : 0; - const consistencyPercent = evaluated.length ? positive2.length / evaluated.length * 100 : 0; - const worstExpectancyR = evaluated.length ? Math.min(...evaluated.map((c) => c.expectancyR)) : 0; - const sourceCounts = { - binance: safeFunding.filter((x) => x.source === "binance-futures").length, - okx: safeFunding.filter((x) => x.source === "okx-swap").length - }; - const entryTimestamps = trades.map((t) => t.timestamp).filter(Number.isFinite); - const firstEntry = entryTimestamps.length ? Math.min(...entryTimestamps) : null; - const lastEntry = entryTimestamps.length ? Math.max(...entryTimestamps) : null; - const fundingAtEntries = trades.filter((t) => latestFundingAtOrBefore(safeFunding, t.timestamp)).length; - const fundingCoveragePercent = trades.length ? fundingAtEntries / trades.length * 100 : 0; - const warnings = []; - if (!safeFunding.length) warnings.push("Nenhum evento hist\xF3rico de funding dispon\xEDvel para classificar o eixo de funding."); - if (fundingCoveragePercent < 95 && trades.length) warnings.push(`Cobertura de funding na entrada em ${round12(fundingCoveragePercent, 1)}% dos trades.`); - if (covered.length < Math.min(9, cells.length)) warnings.push("A matriz cobre poucas combina\xE7\xF5es de regimes; aumentar a janela pode melhorar a representatividade."); - if (evaluated.some((c) => c.expectancyR <= 0)) warnings.push("Existe pelo menos uma combina\xE7\xE3o de regime relevante com expectancy n\xE3o positiva."); - if (evaluated.length < 5) warnings.push(`Menos de cinco c\xE9lulas atingiram o m\xEDnimo de ${minimumTradesPerCell} trades.`); - const status = evaluated.length < 5 ? "INSUFFICIENT_DATA" : consistencyPercent >= 75 && worstExpectancyR > 0 ? "ROBUST" : consistencyPercent >= 50 ? "MIXED" : "FRAGILE"; - return { - totalCells: cells.length, - coveredCells: covered.length, - evaluatedCells: evaluated.length, - positiveCells: positive2.length, - coveragePercent: round12(coveragePercent, 1), - consistencyPercent: round12(consistencyPercent, 1), - worstExpectancyR: round12(worstExpectancyR), - status, - minimumTradesPerCell, - fundingEvents: safeFunding.length, - fundingCoveragePercent: round12(fundingCoveragePercent, 1), - fundingSources: sourceCounts, - firstFundingTimestamp: safeFunding[0]?.timestamp ?? null, - lastFundingTimestamp: safeFunding.at(-1)?.timestamp ?? null, - warnings, - cells - }; -} - -// server/quant/parameterValidation.ts -var round13 = (v, d = 4) => Number((Number.isFinite(v) ? v : 0).toFixed(d)); -var displayPf = (v) => Number.isFinite(v) ? v : v > 0 ? 99 : 0; -function assessOverfitting(trainResult, holdoutResult, selected) { - if (!holdoutResult || !selected || trainResult.totalTrades < 30 || holdoutResult.totalTrades < 30) return { level: "INSUFFICIENT_DATA", score: 0, trainExpectancyR: round13(trainResult.expectancyR), holdoutExpectancyR: round13(holdoutResult?.expectancyR ?? 0), expectancyRetentionPercent: 0, trainProfitFactor: round13(displayPf(trainResult.profitFactor)), holdoutProfitFactor: round13(displayPf(holdoutResult?.profitFactor ?? 0)), profitFactorRetentionPercent: 0, tradeCount: holdoutResult?.totalTrades ?? 0, warnings: ["Amostra insuficiente para medir overfitting com confian\xE7a."] }; - const trainExpectancy = trainResult.expectancyR; - const holdoutExpectancy = holdoutResult.expectancyR; - const trainPf = displayPf(trainResult.profitFactor); - const holdoutPf = displayPf(holdoutResult.profitFactor); - const expectancyRetention = trainExpectancy > 0 ? holdoutExpectancy / trainExpectancy * 100 : holdoutExpectancy > 0 ? 100 : 0; - const pfRetention = trainPf > 0 ? holdoutPf / trainPf * 100 : 0; - const stableBonus = selected.stabilityScore >= selected.rankScore * 0.85 ? 10 : selected.stabilityScore >= selected.rankScore * 0.7 ? 5 : 0; - const samplePenalty = holdoutResult.totalTrades < 50 ? 10 : 0; - const expectancyPenalty = expectancyRetention < 30 ? 45 : expectancyRetention < 50 ? 30 : expectancyRetention < 70 ? 15 : 0; - const pfPenalty = pfRetention < 40 ? 25 : pfRetention < 60 ? 15 : pfRetention < 80 ? 5 : 0; - const score2 = Math.max(0, Math.min(100, 100 + stableBonus - samplePenalty - expectancyPenalty - pfPenalty)); - const level = score2 >= 75 ? "LOW" : score2 >= 50 ? "MODERATE" : "HIGH"; - const warnings = []; - if (expectancyRetention < 70) warnings.push(`Reten\xE7\xE3o de expectancy treino\u2192OOS em ${round13(expectancyRetention, 1)}%.`); - if (pfRetention < 80) warnings.push(`Reten\xE7\xE3o de Profit Factor treino\u2192OOS em ${round13(pfRetention, 1)}%.`); - if (holdoutResult.totalTrades < 50) warnings.push("Holdout tem menos de 50 trades; risco estat\xEDstico maior."); - if (level === "HIGH") warnings.push("Risco alto de overfitting: n\xE3o liberar para uso operacional."); - return { level, score: round13(score2, 1), trainExpectancyR: round13(trainExpectancy), holdoutExpectancyR: round13(holdoutExpectancy), expectancyRetentionPercent: round13(expectancyRetention, 1), trainProfitFactor: round13(trainPf), holdoutProfitFactor: round13(holdoutPf), profitFactorRetentionPercent: round13(pfRetention, 1), tradeCount: holdoutResult.totalTrades, warnings }; -} -function bootstrapSignificance(values, baselineValues, simulations = 3e3) { - if (values.length < 30) return { sampleSize: values.length, meanR: round13(values.length ? values.reduce((a, b) => a + b, 0) / values.length : 0), bootstrapCi95R: null, probabilityPositiveExpectancyPercent: 0, probabilityPositiveDeltaPercent: 0, baselineDeltaCi95R: null, status: "INSUFFICIENT_DATA", warnings: ["S\xE3o necess\xE1rios pelo menos 30 trades OOS."] }; - let seed = 20260908 + values.length; - const random = () => { - seed = 1664525 * seed + 1013904223 >>> 0; - return seed / 4294967296; - }; - const means = []; - const deltas = []; - let positive2 = 0; - let positiveDelta = 0; - const observed = values.reduce((a, b) => a + b, 0) / values.length; - for (let s = 0; s < simulations; s++) { - let sum = 0; - let deltaSum = 0; - for (let i = 0; i < values.length; i++) { - const idx = Math.floor(random() * values.length); - const v = values[idx]; - sum += v; - if (baselineValues?.length) deltaSum += v - baselineValues[Math.floor(random() * baselineValues.length)]; - } - const m = sum / values.length; - means.push(m); - if (m > 0) positive2++; - if (baselineValues?.length) { - const d = deltaSum / values.length; - deltas.push(d); - if (d > 0) positiveDelta++; - } - } - means.sort((a, b) => a - b); - deltas.sort((a, b) => a - b); - const percentile3 = (arr, p) => arr.length ? arr[Math.min(arr.length - 1, Math.max(0, Math.floor((arr.length - 1) * p)))] : 0; - const ci = { low: percentile3(means, 0.025), high: percentile3(means, 0.975) }; - const deltaCi = deltas.length ? { low: percentile3(deltas, 0.025), high: percentile3(deltas, 0.975) } : null; - const pPositive = positive2 / simulations * 100; - const pDelta = baselineValues?.length ? positiveDelta / simulations * 100 : 0; - const significant = ci.low > 0 && (!deltaCi || deltaCi.low > 0) && pPositive >= 97.5; - const warnings = []; - if (ci.low <= 0) warnings.push("IC bootstrap 95% da expectancy inclui zero."); - if (deltaCi && deltaCi.low <= 0) warnings.push("IC bootstrap da vantagem contra baseline inclui zero."); - return { sampleSize: values.length, meanR: round13(observed), bootstrapCi95R: { low: round13(ci.low), high: round13(ci.high) }, probabilityPositiveExpectancyPercent: round13(pPositive, 2), probabilityPositiveDeltaPercent: round13(pDelta, 2), baselineDeltaCi95R: deltaCi ? { low: round13(deltaCi.low), high: round13(deltaCi.high) } : null, status: significant ? "SIGNIFICANT" : "WEAK", warnings }; -} -function validateRegimes(candles, trades) { - const analysis = analyzeRegimes(candles, trades); - const evaluated = analysis.buckets.filter((b) => b.trades >= 10); - const positive2 = evaluated.filter((b) => b.expectancyR > 0); - const covered = analysis.buckets.filter((b) => b.trades > 0).length; - const coverage = covered / analysis.buckets.length * 100; - const consistency = evaluated.length ? positive2.length / evaluated.length * 100 : 0; - const worst = evaluated.length ? Math.min(...evaluated.map((b) => b.expectancyR)) : 0; - const warnings = [...analysis.warnings]; - if (covered < 3) warnings.push("Menos de tr\xEAs regimes tiveram opera\xE7\xF5es na janela ampliada."); - if (evaluated.some((b) => b.expectancyR <= 0)) warnings.push("Existe pelo menos um regime relevante com expectancy n\xE3o positiva."); - const status = evaluated.length < 3 ? "INSUFFICIENT_DATA" : consistency >= 75 && worst > 0 ? "ROBUST" : consistency >= 50 ? "MIXED" : "FRAGILE"; - return { coveredRegimes: covered, evaluatedRegimes: evaluated.length, positiveRegimes: positive2.length, coveragePercent: round13(coverage, 1), consistencyPercent: round13(consistency, 1), worstExpectancyR: round13(worst), status, warnings, buckets: analysis.buckets }; -} -function validateSelectedParameters(symbol, candles, baseOptions2 = {}, trainPercent = 70) { - const safePercent = Math.max(60, Math.min(80, trainPercent)); - const splitIndex = Math.floor(candles.length * safePercent / 100); - const train = candles.slice(0, splitIndex); - const holdout = candles.slice(splitIndex); - const optimization = selectRobustParameters(symbol, train, baseOptions2); - const baselineHoldout = runHistoricalBacktest({ ...baseOptions2, symbol, candles: holdout }); - const selectedHoldout = optimization.selected ? runHistoricalBacktest({ ...baseOptions2, symbol, candles: holdout, minScore: optimization.selected.minScore, minConfidence: optimization.selected.minConfidence, atrStopMultiple: optimization.selected.atrStopMultiple, rewardRisk: optimization.selected.rewardRisk, maxHoldingBars: optimization.selected.maxHoldingBars }) : null; - const trainSelected = optimization.selected ? runHistoricalBacktest({ ...baseOptions2, symbol, candles: train, minScore: optimization.selected.minScore, minConfidence: optimization.selected.minConfidence, atrStopMultiple: optimization.selected.atrStopMultiple, rewardRisk: optimization.selected.rewardRisk, maxHoldingBars: optimization.selected.maxHoldingBars }) : null; - const regimeWindow = optimization.selected ? runHistoricalBacktest({ ...baseOptions2, symbol, candles, minScore: optimization.selected.minScore, minConfidence: optimization.selected.minConfidence, atrStopMultiple: optimization.selected.atrStopMultiple, rewardRisk: optimization.selected.rewardRisk, maxHoldingBars: optimization.selected.maxHoldingBars }) : null; - const monteCarlo = selectedHoldout && selectedHoldout.totalTrades >= 30 ? analyzeMonteCarlo(selectedHoldout.trades, 3e3, baseOptions2.riskPerTradePercent ?? 1) : null; - const stressTest = selectedHoldout && selectedHoldout.totalTrades >= 30 ? runStressTest(symbol, holdout, { ...baseOptions2, minScore: optimization.selected?.minScore, minConfidence: optimization.selected?.minConfidence, atrStopMultiple: optimization.selected?.atrStopMultiple, rewardRisk: optimization.selected?.rewardRisk, maxHoldingBars: optimization.selected?.maxHoldingBars }) : null; - const expectancyDeltaR = round13((selectedHoldout?.expectancyR ?? 0) - baselineHoldout.expectancyR); - const netProfitDeltaPercent = round13((selectedHoldout?.netProfitPercent ?? 0) - baselineHoldout.netProfitPercent, 2); - const drawdownDeltaPercent = round13((selectedHoldout?.maxDrawdownPercent ?? 0) - baselineHoldout.maxDrawdownPercent, 2); - const selectedBeatsBaseline = !!selectedHoldout && selectedHoldout.totalTrades >= 30 && selectedHoldout.expectancyR > 0 && selectedHoldout.profitFactor > 1 && selectedHoldout.expectancyR >= baselineHoldout.expectancyR; - const overfittingGuard = assessOverfitting(trainSelected ?? runHistoricalBacktest({ ...baseOptions2, symbol, candles: train }), selectedHoldout, optimization.selected); - const significance = bootstrapSignificance(selectedHoldout?.trades.map((t) => t.pnlR).filter(Number.isFinite) ?? [], baselineHoldout.trades.map((t) => t.pnlR).filter(Number.isFinite)); - const regimeValidation = validateRegimes(candles, regimeWindow?.trades ?? []); - const historicalFunding = baseOptions2.historicalFunding ?? []; - const multiRegimeValidation = validateMultiRegime(candles, regimeWindow?.trades ?? [], historicalFunding, 10); - const warnings = [...overfittingGuard.warnings, ...significance.warnings, ...regimeValidation.warnings, ...multiRegimeValidation.warnings]; - if (train.length < 3e3 || holdout.length < 1500) warnings.push("Janela inferior \xE0 recomendada para valida\xE7\xE3o multi-regime estendida."); - if (!optimization.selected) warnings.push("Nenhum par\xE2metro foi selecionado no treino."); - if (selectedHoldout && selectedHoldout.totalTrades < 30) warnings.push("O holdout selecionado tem menos de 30 trades."); - if (monteCarlo?.grade === "FRAGILE") warnings.push("Monte Carlo classificou a distribui\xE7\xE3o como FRAGILE."); - if (stressTest?.grade === "FRAGILE") warnings.push("Stress Test classificou a configura\xE7\xE3o como FRAGILE."); - const enough = train.length >= 3e3 && holdout.length >= 1500 && !!selectedHoldout && selectedHoldout.totalTrades >= 30; - const riskChecksPass = !!monteCarlo && monteCarlo.grade !== "FRAGILE" && !!stressTest && stressTest.grade !== "FRAGILE"; - const verdict = !enough ? "INSUFFICIENT_DATA" : !selectedBeatsBaseline || overfittingGuard.level === "HIGH" || significance.status !== "SIGNIFICANT" || regimeValidation.status === "FRAGILE" || regimeValidation.status === "INSUFFICIENT_DATA" || multiRegimeValidation.status === "FRAGILE" || multiRegimeValidation.status === "INSUFFICIENT_DATA" ? "REJECT" : selectedBeatsBaseline && riskChecksPass && overfittingGuard.level === "LOW" && regimeValidation.status === "ROBUST" && multiRegimeValidation.status === "ROBUST" ? "PASS" : "CAUTION"; - return { split: { trainCandles: train.length, holdoutCandles: holdout.length, trainPercent: safePercent }, optimization, baselineHoldout, selectedHoldout, regimeWindow, monteCarlo, stressTest, comparison: { expectancyDeltaR, netProfitDeltaPercent, drawdownDeltaPercent, selectedBeatsBaseline }, overfittingGuard, significance, regimeValidation, multiRegimeValidation, verdict, warnings }; -} - -// server.ts -var import_meta = {}; -import_dotenv.default.config(); -var __filename = (0, import_url.fileURLToPath)(import_meta.url); -var __dirname = import_path.default.dirname(__filename); -var app = (0, import_express.default)(); -var PORT = 3e3; -app.use(import_express.default.json()); -var genAI = null; -if (process.env.GEMINI_API_KEY) { - try { - genAI = new import_genai.GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY, httpOptions: { headers: { "User-Agent": "aistudio-build" } } }); - } catch (err) { - console.error("Error initializing GoogleGenAI:", err); - } -} -var cachedSignals = []; -var cachedNotifications = []; -var isRefreshing = false; -var lastRefreshTime = 0; -var lastSourceInfo = "Iniciando varredura das 100 maiores criptos por Market Cap..."; -async function refreshMarketData() { - if (isRefreshing) return; - isRefreshing = true; - try { - const signals = await fetchTop100Cryptos(); - if (signals?.length) { - cachedSignals = signals; - lastRefreshTime = Date.now(); - lastSourceInfo = "Top 100 Criptomoedas por Market Cap (Tempo Real \u2022 CoinGecko + Binance)"; - } - const validSignals = cachedSignals.filter((s) => s.passedFilter); - validSignals.forEach((signal) => { - if (!cachedNotifications.some((n) => n.symbol === signal.symbol && Date.now() - n.timestamp < 9e5)) { - const timeBrasilia = getBrasiliaTimeStr(Date.now(), true); - cachedNotifications.unshift({ id: `notif-${Date.now()}-${Math.random().toString(36).substring(2, 7)}`, timestamp: Date.now(), timeStr: timeBrasilia, type: "CRITICAL_SIGNAL", severity: "high", symbol: signal.symbol, title: `Setup Quantitativo Confirmado: ${signal.symbol} (#${signal.marketCapRank || 0})`, message: `Conflu\xEAncia institucional atingiu ${signal.confidence}%. Entrada em $${signal.entryPrice} e SL din\xE2mico em $${signal.stopLoss} (R/R 1:${signal.riskReward}) \u2022 ${timeBrasilia} (Hor\xE1rio de Bras\xEDlia).`, read: false, actionable: true }); - } - }); - cachedSignals.forEach((signal) => { - if (Math.abs(signal.change24h) >= 7 && !cachedNotifications.some((n) => n.symbol === signal.symbol && n.type === "FUNDING_ALERT" && Date.now() - n.timestamp < 12e5)) { - const timeBrasilia = getBrasiliaTimeStr(Date.now(), true); - cachedNotifications.unshift({ id: `notif-vol-${Date.now()}-${Math.random().toString(36).substring(2, 7)}`, timestamp: Date.now(), timeStr: timeBrasilia, type: "FUNDING_ALERT", severity: "medium", symbol: signal.symbol, title: `Alerta de Volatilidade: ${signal.symbol} (#${signal.marketCapRank || 0})`, message: `Varia\xE7\xE3o de 24h atingiu ${signal.change24h > 0 ? "+" : ""}${signal.change24h}% no ativo #${signal.marketCapRank || 0} \u2022 ${timeBrasilia} (Bras\xEDlia).`, read: false }); - } - }); - if (cachedNotifications.length > 50) cachedNotifications = cachedNotifications.slice(0, 50); - } catch (err) { - console.error("Failed to refresh real-time market data:", err.message); - } finally { - isRefreshing = false; - } -} -refreshMarketData(); -setInterval(refreshMarketData, 35e3); -setInterval(async () => { - if (cachedSignals.length > 0) { - await syncRealTimePrices(); - lastRefreshTime = Date.now(); - } -}, 6e3); -app.get("/api/market/candles/:symbol", async (req, res) => { - try { - const symbol = decodeURIComponent(req.params.symbol).toUpperCase(); - if (!/^[A-Z0-9]+\/[A-Z0-9]+$/.test(symbol)) return res.status(400).json({ error: "S\xEDmbolo inv\xE1lido. Use o formato BTC/USDT." }); - const snapshot = await getMarketSnapshot(symbol, { forceRefresh: req.query.refresh === "true" }); - return res.json({ success: true, source: "Binance/OKX real OHLCV", snapshot }); - } catch (error) { - console.error("Real market snapshot error:", error); - return res.status(502).json({ success: false, error: error?.message || "Falha ao carregar dados reais de mercado." }); - } -}); -app.get("/api/market/analysis/:symbol", async (req, res) => { - try { - const symbol = decodeURIComponent(req.params.symbol).toUpperCase(); - if (!/^[A-Z0-9]+\/[A-Z0-9]+$/.test(symbol)) return res.status(400).json({ error: "S\xEDmbolo inv\xE1lido. Use o formato BTC/USDT." }); - const analysis = await analyzeMarket(symbol, req.query.refresh === "true"); - return res.json({ success: true, source: "Binance/OKX real OHLCV + indicadores + estrutura + SMC + diverg\xEAncias + Gann + Wyckoff", analysis }); - } catch (error) { - console.error("Quantitative market analysis error:", error); - return res.status(502).json({ success: false, error: error?.message || "Falha ao executar an\xE1lise quantitativa." }); - } -}); -app.get("/api/signal/:symbol", async (req, res) => { - try { - const symbol = decodeURIComponent(req.params.symbol || "BTC/USDT").toUpperCase().replace("-", "/"); - if (!/^[A-Z0-9]+\/[A-Z0-9]+$/.test(symbol)) return res.status(400).json({ success: false, error: "S\xEDmbolo inv\xE1lido. Use BTC/USDT." }); - const riskPercent = Number(req.query.riskPercent ?? 1); - const analysis = await analyzeMarket(symbol, req.query.refresh === "true"); - const series = await fetchRealCandles(symbol, "15m", 500); - const signal = generateTradeSignal(analysis, series.candles, Number.isFinite(riskPercent) ? riskPercent : 1); - return res.json({ success: true, source: "Binance/OKX real OHLCV + quantitative signal engine", signal, generatedAt: Date.now() }); - } catch (error) { - console.error("Signal engine error:", error); - return res.status(502).json({ success: false, error: error?.message || "Falha ao gerar sinal." }); - } -}); -app.get("/api/paper/status", (_req, res) => res.json({ success: true, ...paperTradingLoop.getStatus(), state: paperTradingLoop.getEngine().getState() })); -app.post("/api/paper/start", (_req, res) => { - paperTradingLoop.start(); - res.json({ success: true, ...paperTradingLoop.getStatus() }); -}); -app.post("/api/paper/stop", (_req, res) => { - paperTradingLoop.stop(); - res.json({ success: true, ...paperTradingLoop.getStatus() }); -}); -app.post("/api/paper/tick", async (_req, res) => { - await paperTradingLoop.tick(); - const status = paperTradingLoop.getStatus(); - const success = status.lastError === null; - return res.status(success ? 200 : 502).json({ success, ...status, state: paperTradingLoop.getEngine().getState() }); -}); -app.post("/api/paper/reset", (_req, res) => { - paperTradingLoop.stop(); - const state = paperTradingLoop.getEngine().reset(); - return res.json({ success: true, ...paperTradingLoop.getStatus(), state }); -}); -app.post("/api/paper/close/:positionId", (req, res) => { - const price = Number(req.body?.price); - const closed = paperTradingLoop.getEngine().close(req.params.positionId, price, "MANUAL"); - if (!closed) return res.status(404).json({ success: false, error: "Posi\xE7\xE3o n\xE3o encontrada ou pre\xE7o inv\xE1lido." }); - return res.json({ success: true, position: closed, state: paperTradingLoop.getEngine().getState() }); -}); -app.get("/api/market/signals", async (_req, res) => { - if (!cachedSignals.length) await refreshMarketData(); - res.json({ timestamp: lastRefreshTime || Date.now(), version: "The God Protocol v2026 (v4.0)", dataSource: lastSourceInfo, isRealTime: true, signals: cachedSignals, activeCount: cachedSignals.filter((s) => s.passedFilter).length, monitoredCount: cachedSignals.length }); -}); -app.post("/api/market/refresh-prices", async (_req, res) => { - const updatedSignals = await syncRealTimePrices(); - lastRefreshTime = Date.now(); - res.json({ success: true, timestamp: lastRefreshTime, signals: updatedSignals, activeCount: updatedSignals.filter((s) => s.passedFilter).length }); -}); -app.post("/api/market/scan", async (_req, res) => { - await refreshMarketData(); - res.json({ success: true, message: "Varredura quantitativa em tempo real via Binance.US + OKX conclu\xEDda com sucesso.", dataSource: lastSourceInfo, signals: cachedSignals }); -}); -app.post("/api/ai-analysis", async (req, res) => { - try { - const requestedSymbol = typeof req.body?.symbol === "string" ? req.body.symbol.toUpperCase().replace("-", "/") : "BTC/USDT"; - if (!/^[A-Z0-9]+\/[A-Z0-9]+$/.test(requestedSymbol)) return res.status(400).json({ error: "S\xEDmbolo inv\xE1lido. Use BTC/USDT." }); - const riskPercent = Number(req.body?.riskPercent ?? 1); - const analysis = await analyzeMarket(requestedSymbol, true); - const series = await fetchRealCandles(requestedSymbol, "15m", 500); - const signal = generateTradeSignal(analysis, series.candles, Number.isFinite(riskPercent) ? riskPercent : 1); - const model = "gemini-3.8-flash"; - if (!genAI) return res.json({ success: true, aiAvailable: false, analysis: { decision: "WEAKEN", rationale: "GEMINI_API_KEY n\xE3o configurada; an\xE1lise determin\xEDstica dispon\xEDvel.", riskFlags: ["gemini_api_unavailable"], model: "local-fallback" }, signal }); - const prompt = buildQuantAnalystPrompt({ analysis, signal }); - const response = await genAI.models.generateContent({ model, contents: prompt }); - const quantAnalyst = parseQuantAnalystResponse(response.text || "", model); - return res.json({ success: true, aiAvailable: true, analysis: quantAnalyst, signal, generatedAt: Date.now() }); - } catch (error) { - console.error("Gemini Quant Analyst error:", error); - return res.status(502).json({ success: false, error: error?.message || "Falha no Quant Analyst." }); - } -}); -app.get("/api/backtest", async (req, res) => { - try { - const symbol = String(req.query.symbol || "BTC/USDT").toUpperCase(); - if (!/^[A-Z0-9]+\/[A-Z0-9]+$/.test(symbol)) return res.status(400).json({ success: false, error: "S\xEDmbolo inv\xE1lido. Use o formato BTC/USDT." }); - const requestedDays = Number.parseInt(String(req.query.days || "60"), 10); - const days = Number.isFinite(requestedDays) ? Math.max(7, Math.min(requestedDays, 90)) : 60; - const endTime = Date.now(); - const startTime = endTime - days * 864e5; - const [candles, historicalFunding] = await Promise.all([fetchHistoricalBinanceCandles(symbol, "15m", startTime, endTime), fetchHistoricalBinanceFunding(symbol, startTime, endTime)]); - if (candles.length < 300) return res.status(422).json({ success: false, error: `Hist\xF3rico insuficiente: ${candles.length} candles.` }); - const result = runHistoricalBacktest({ symbol, candles, initialCapital: 1e4, riskPerTradePercent: 1, minScore: 35, minConfidence: 50, atrStopMultiple: 1.5, rewardRisk: 2, maxHoldingBars: 32, warmupBars: 220, historicalFunding }); - return res.json({ success: true, source: "Binance real OHLCV + Binance Futures historical funding", parameters: { symbol, timeframe: "15m", days, candles: candles.length, fundingEvents: historicalFunding.length, riskPerTradePercent: 1, rewardRisk: 2, maxHoldingBars: 32 }, ...result }); - } catch (error) { - console.error("Historical backtest error:", error); - return res.status(502).json({ success: false, error: error?.message || "Falha ao executar backtest hist\xF3rico real." }); - } -}); -app.get("/api/quant/lab", async (req, res) => { - try { - const symbol = String(req.query.symbol || "BTC/USDT").toUpperCase(); - const requestedDays = Number.parseInt(String(req.query.days || "30"), 10); - const days = Number.isFinite(requestedDays) ? Math.max(7, Math.min(requestedDays, 90)) : 30; - if (!/^[A-Z0-9]+\/[A-Z0-9]+$/.test(symbol)) return res.status(400).json({ success: false, error: "S\xEDmbolo inv\xE1lido. Use o formato BTC/USDT." }); - const endTime = Date.now(); - const startTime = endTime - days * 864e5; - const [candles, historicalFunding] = await Promise.all([fetchHistoricalBinanceCandles(symbol, "15m", startTime, endTime), fetchHistoricalBinanceFunding(symbol, startTime, endTime)]); - if (candles.length < 300) return res.status(422).json({ success: false, error: `Hist\xF3rico insuficiente: ${candles.length} candles.` }); - const options = { symbol, candles, initialCapital: 1e4, riskPerTradePercent: 1, minScore: 35, minConfidence: 50, atrStopMultiple: 1.5, rewardRisk: 2, maxHoldingBars: 32, warmupBars: 220, historicalFunding }; - const backtest = runHistoricalBacktest(options); - const paperState = paperTradingLoop.getEngine().getState(); - const lab = buildQuantitativeLab(backtest, paperState, candles); - return res.json({ success: true, source: "Binance real OHLCV + Binance Futures historical funding \u2022 Quant Lab completo", parameters: { symbol, timeframe: "15m", days, candles: candles.length, fundingEvents: historicalFunding.length, riskPerTradePercent: 1, rewardRisk: 2, maxHoldingBars: 32 }, ...lab }); - } catch (error) { - console.error("Quantitative lab error:", error); - return res.status(502).json({ success: false, error: error?.message || "Falha ao executar Quant Lab." }); - } -}); -app.get("/api/quant/validate", async (req, res) => { - try { - const symbol = String(req.query.symbol || "BTC/USDT").toUpperCase(); - const requestedDays = Number.parseInt(String(req.query.days || "180"), 10); - const days = Number.isFinite(requestedDays) ? Math.max(90, Math.min(requestedDays, 180)) : 180; - const trainPercent = Number(req.query.trainPercent ?? 70); - if (!/^[A-Z0-9]+\/[A-Z0-9]+$/.test(symbol)) return res.status(400).json({ success: false, error: "S\xEDmbolo inv\xE1lido. Use o formato BTC/USDT." }); - const endTime = Date.now(); - const startTime = endTime - days * 864e5; - const [candles, historicalFunding] = await Promise.all([fetchHistoricalBinanceCandles(symbol, "15m", startTime, endTime), fetchHistoricalBinanceFunding(symbol, startTime, endTime)]); - if (candles.length < 5e3) return res.status(422).json({ success: false, error: `Hist\xF3rico insuficiente para OOS multi-regime: ${candles.length} candles. Recomenda-se pelo menos 5.000.` }); - const baseOptions2 = { initialCapital: 1e4, riskPerTradePercent: 1, minScore: 35, minConfidence: 50, atrStopMultiple: 1.5, rewardRisk: 2, maxHoldingBars: 32, warmupBars: 220, historicalFunding }; - const validation = validateSelectedParameters(symbol, candles, baseOptions2, Number.isFinite(trainPercent) ? trainPercent : 70); - return res.json({ success: true, source: "Binance real OHLCV + Binance Futures historical funding \u2022 extended multi-regime OOS validation", parameters: { symbol, timeframe: "15m", days, candles: candles.length, fundingEvents: historicalFunding.length, trainPercent: validation.split.trainPercent, regimeWindowDays: days }, ...validation }); - } catch (error) { - console.error("Parameter OOS validation error:", error); - return res.status(502).json({ success: false, error: error?.message || "Falha na valida\xE7\xE3o OOS multi-regime." }); - } -}); -app.get("/api/notifications", (_req, res) => res.json({ notifications: cachedNotifications, unreadCount: cachedNotifications.filter((n) => !n.read).length })); -app.post("/api/notifications/mark-read", (_req, res) => { - cachedNotifications.forEach((n) => { - n.read = true; - }); - res.json({ success: true, unreadCount: 0 }); -}); -app.post("/api/notifications/clear", (_req, res) => { - cachedNotifications = []; - res.json({ success: true, count: 0 }); -}); -async function startServer() { - if (process.env.NODE_ENV !== "production") { - const vite = await (0, import_vite.createServer)({ server: { middlewareMode: true }, appType: "spa" }); - app.use(vite.middlewares); - } else { - const distPath = import_path.default.join(process.cwd(), "dist"); - app.use(import_express.default.static(distPath)); - app.get("*", (_req, res) => res.sendFile(import_path.default.join(distPath, "index.html"))); - } - app.listen(PORT, "0.0.0.0", () => console.log(`[The God Protocol v2026] Server running on http://0.0.0.0:${PORT}`)); -} -startServer(); -//# sourceMappingURL=server.cjs.map