← Назад'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 };