← Back
'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 };

📜 Git History

43817b9feat: trading stats + trade journal — Binance sync, stats bar, journal table3 months ago
Show last diff
Loading...