← Назад
'use strict'; const axios = require('axios'); const logger = require('../utils/logger'); const ASSETS = ['BTC', 'ETH', 'SOL', 'BNB', 'XRP', 'DOGE']; const BINANCE_KLINES = 'https://api.binance.com/api/v3/klines'; // Simple EMA calculation function ema(values, period) { if (!values.length) return 0; const k = 2 / (period + 1); let result = values[0]; for (let i = 1; i < values.length; i++) { result = values[i] * k + result * (1 - k); } return result; } /** * Fetch spot klines and compute trend metrics for all assets. * Returns: { BTC: { bias, strength, change1h, change4h, change24h, emaDirection, momentum }, ... } */ async function fetchSpotTrends() { const trends = {}; const tasks = ASSETS.map(async (asset) => { const symbol = `${asset}USDT`; try { // Fetch 24 × 1h candles (covers 24h lookback) const resp = await axios.get(BINANCE_KLINES, { params: { symbol, interval: '1h', limit: 25 }, timeout: 5000, }); const candles = resp.data; // [[openTime, open, high, low, close, volume, ...], ...] if (!candles || candles.length < 5) return; const closes = candles.map(c => parseFloat(c[4])); const current = closes[closes.length - 1]; // Price changes const change1h = closes.length >= 2 ? ((current - closes[closes.length - 2]) / closes[closes.length - 2]) * 100 : 0; const change4h = closes.length >= 5 ? ((current - closes[closes.length - 5]) / closes[closes.length - 5]) * 100 : 0; const change24h = closes.length >= 25 ? ((current - closes[0]) / closes[0]) * 100 : ((current - closes[0]) / closes[0]) * 100; // EMA 8 vs EMA 21 direction const ema8 = ema(closes, 8); const ema21 = ema(closes, 21); const emaDirection = ema8 > ema21 ? 'UP' : ema8 < ema21 ? 'DOWN' : 'FLAT'; // Momentum: rate of change over last 4 candles (1h each) const momentum = closes.length >= 5 ? ((closes[closes.length - 1] - closes[closes.length - 5]) / closes[closes.length - 5]) * 100 : 0; // Overall bias determination let bias; let strength; // 0-100 const bullishSignals = [ change1h > 0.3, change4h > 0.5, change24h > 1, emaDirection === 'UP', momentum > 0.5, ].filter(Boolean).length; const bearishSignals = [ change1h < -0.3, change4h < -0.5, change24h < -1, emaDirection === 'DOWN', momentum < -0.5, ].filter(Boolean).length; if (bullishSignals >= 4) { bias = 'STRONG_BULLISH'; strength = 80 + bullishSignals * 4; } else if (bullishSignals >= 3) { bias = 'BULLISH'; strength = 60 + bullishSignals * 4; } else if (bearishSignals >= 4) { bias = 'STRONG_BEARISH'; strength = 80 + bearishSignals * 4; } else if (bearishSignals >= 3) { bias = 'BEARISH'; strength = 60 + bearishSignals * 4; } else { bias = 'NEUTRAL'; strength = 30; } strength = Math.min(strength, 100); trends[asset] = { bias, strength, change1h: parseFloat(change1h.toFixed(2)), change4h: parseFloat(change4h.toFixed(2)), change24h: parseFloat(change24h.toFixed(2)), emaDirection, momentum: parseFloat(momentum.toFixed(2)), ema8: parseFloat(ema8.toFixed(2)), ema21: parseFloat(ema21.toFixed(2)), spot: current, }; } catch (err) { logger.error(`Spot trend fetch error (${asset}): ${err.message}`); } }); await Promise.all(tasks); return trends; } /** * Check if trend confirms a directional signal * @param {Object} trend - spotTrend for the asset * @param {string} direction - 'BULLISH' or 'BEARISH' * @returns {{ confirmed: boolean, reason: string }} */ function confirmsTrend(trend, direction) { if (!trend) return { confirmed: true, reason: 'нет данных тренда' }; if (direction === 'BULLISH') { // Buy Call should NOT fire when spot is actively falling if (trend.bias === 'STRONG_BEARISH' || trend.bias === 'BEARISH') { return { confirmed: false, reason: `спот падает (${trend.change4h > 0 ? '+' : ''}${trend.change4h}% за 4ч, EMA ${trend.emaDirection})` }; } // Reject if recent dump even if overall neutral if (trend.change1h < -1.5 || trend.change4h < -3) { return { confirmed: false, reason: `резкий дамп (1ч ${trend.change1h}%, 4ч ${trend.change4h}%)` }; } return { confirmed: true, reason: '' }; } if (direction === 'BEARISH') { // Buy Put should NOT fire when spot is actively pumping if (trend.bias === 'STRONG_BULLISH' || trend.bias === 'BULLISH') { return { confirmed: false, reason: `спот растёт (${trend.change4h > 0 ? '+' : ''}${trend.change4h}% за 4ч, EMA ${trend.emaDirection})` }; } if (trend.change1h > 1.5 || trend.change4h > 3) { return { confirmed: false, reason: `резкий памп (1ч +${trend.change1h}%, 4ч +${trend.change4h}%)` }; } return { confirmed: true, reason: '' }; } return { confirmed: true, reason: '' }; } /** * Build human-readable trend description for rationale */ function trendLabel(trend) { if (!trend) return ''; const arrow = trend.change4h >= 0 ? '📈' : '📉'; const sign4h = trend.change4h >= 0 ? '+' : ''; const sign1h = trend.change1h >= 0 ? '+' : ''; return `${arrow} Спот ${sign4h}${trend.change4h}% (4ч) / ${sign1h}${trend.change1h}% (1ч)`; } module.exports = { fetchSpotTrends, confirmsTrend, trendLabel };