DEV Community

mcw999
mcw999

Posted on

Cryptocurrency Market Overheating and Cooling Detection System Built with Node.js + TypeScript

Cryptocurrency Market Overheating and Cooling Detection System Built with Node.js + TypeScript

In cryptocurrency trading, accurately identifying market overheating and cooling timing is extremely important. This article explains how to build a price, volume, and open interest data analysis system using the KuCoin Futures API to detect market signals in real-time.

System Overview

This system analyzes three key data points of the cryptocurrency market:

  • Price Data: Market trends and price volatility
  • Volume Data: Trading activity levels
  • Open Interest Data: Position accumulation status

From this data, we detect four types of signals and classify market overheating/cooling states into four levels.

Tech Stack

{
  "dependencies": {
    "express": "^4.18.2",
    "typescript": "^5.0.0",
    "node-fetch": "^3.3.0",
    "@types/node": "^18.0.0"
  },
  "devDependencies": {
    "vitest": "^0.34.0",
    "@types/express": "^4.17.17"
  }
}
Enter fullscreen mode Exit fullscreen mode

Project Structure

src/
├── types/
│   ├── market.ts
│   └── signals.ts
├── services/
│   ├── kucoinApi.ts
│   ├── signalDetector.ts
│   └── coolingAnalyzer.ts
├── utils/
│   └── timeframe.ts
├── routes/
│   └── analysis.ts
└── app.ts
Enter fullscreen mode Exit fullscreen mode

Type Definition Design

First, let's define types for market data and signals:

// types/market.ts
export interface MarketData {
  timestamp: number;
  price: number;
  volume: number;
  openInterest: number;
  symbol: string;
}

export type TimeFrame = 'HTF' | 'LTF';
export type HTFInterval = '4h' | '1d';
export type LTFInterval = '15m' | '1h';

// types/signals.ts
export type SignalType = 'HIGH_ZONE' | 'PRICE_WEAK' | 'VOLUME_WEAK' | 'OI_SIGNAL';
export type CoolingLevel = 'confirmed' | 'watch' | 'mild' | 'none';

export interface DetectedSignal {
  type: SignalType;
  strength: number;
  timestamp: number;
  timeframe: TimeFrame;
}

export interface CoolingAnalysis {
  level: CoolingLevel;
  signals: DetectedSignal[];
  confidence: number;
  recommendation: string;
}
Enter fullscreen mode Exit fullscreen mode

KuCoin API Connection Service

Let's implement a service to fetch data from the KuCoin Futures API:

// services/kucoinApi.ts
import fetch from 'node-fetch';

export class KuCoinApiService {
  private readonly baseUrl = 'https://api-futures.kucoin.com';

  async getKlineData(
    symbol: string, 
    interval: string, 
    limit: number = 100
  ): Promise<MarketData[]> {
    try {
      const endpoint = `/api/v1/kline/query`;
      const params = new URLSearchParams({
        symbol,
        granularity: this.convertInterval(interval),
        from: (Date.now() - limit * this.getIntervalMs(interval)).toString(),
        to: Date.now().toString()
      });

      const response = await fetch(`${this.baseUrl}${endpoint}?${params}`);
      const data = await response.json();

      return this.parseKlineData(data.data, symbol);
    } catch (error) {
      console.error('KuCoin API Error:', error);
      throw new Error('Failed to fetch market data');
    }
  }

  async getOpenInterest(symbol: string): Promise<number> {
    const endpoint = `/api/v1/contracts/${symbol}`;
    const response = await fetch(`${this.baseUrl}${endpoint}`);
    const data = await response.json();

    return parseFloat(data.data.openInterest);
  }

  private convertInterval(interval: string): string {
    const mapping: { [key: string]: string } = {
      '15m': '900',
      '1h': '3600',
      '4h': '14400',
      '1d': '86400'
    };
    return mapping[interval] || '3600';
  }

  private getIntervalMs(interval: string): number {
    const mapping: { [key: string]: number } = {
      '15m': 15 * 60 * 1000,
      '1h': 60 * 60 * 1000,
      '4h': 4 * 60 * 60 * 1000,
      '1d': 24 * 60 * 60 * 1000
    };
    return mapping[interval] || 60 * 60 * 1000;
  }

  private parseKlineData(rawData: any[], symbol: string): MarketData[] {
    return rawData.map(item => ({
      timestamp: parseInt(item[0]),
      price: parseFloat(item[2]), // close price
      volume: parseFloat(item[5]),
      openInterest: 0, // fetched separately
      symbol
    }));
  }
}
Enter fullscreen mode Exit fullscreen mode

Signal Detection Logic

Let's implement a service that detects four types of signals:

// services/signalDetector.ts
export class SignalDetector {

  detectHighZone(data: MarketData[]): DetectedSignal | null {
    if (data.length < 20) return null;

    const recent = data.slice(-5);
    const historical = data.slice(-20, -5);

    const recentHigh = Math.max(...recent.map(d => d.price));
    const historicalAvg = historical.reduce((sum, d) => sum + d.price, 0) / historical.length;

    const priceRatio = recentHigh / historicalAvg;

    if (priceRatio > 1.15) { // 15% or more increase
      return {
        type: 'HIGH_ZONE',
        strength: Math.min((priceRatio - 1) * 100, 100),
        timestamp: Date.now(),
        timeframe: 'HTF'
      };
    }

    return null;
  }

  detectPriceWeak(data: MarketData[]): DetectedSignal | null {
    if (data.length < 10) return null;

    const recent = data.slice(-3);
    const comparison = data.slice(-10, -3);

    // Detect declining price momentum
    const recentSlope = this.calculateSlope(recent.map(d => d.price));
    const comparisonSlope = this.calculateSlope(comparison.map(d => d.price));

    if (recentSlope < comparisonSlope * 0.5 && comparisonSlope > 0) {
      return {
        type: 'PRICE_WEAK',
        strength: Math.abs(recentSlope - comparisonSlope) * 10,
        timestamp: Date.now(),
        timeframe: 'LTF'
      };
    }

    return null;
  }

  detectVolumeWeak(data: MarketData[]): DetectedSignal | null {
    if (data.length < 10) return null;

    const recentVolume = data.slice(-3).reduce((sum, d) => sum + d.volume, 0) / 3;
    const avgVolume = data.slice(-10).reduce((sum, d) => sum + d.volume, 0) / 10;

    const volumeRatio = recentVolume / avgVolume;

    if (volumeRatio < 0.7) { // 30% or more volume decrease
      return {
        type: 'VOLUME_WEAK',
        strength: (1 - volumeRatio) * 100,
        timestamp: Date.now(),
        timeframe: 'LTF'
      };
    }

    return null;
  }

  detectOISignal(data: MarketData[], currentOI: number): DetectedSignal | null {
    if (data.length < 5) return null;

    const recentPriceChange = (data[data.length - 1].price - data[data.length - 5].price) / data[data.length - 5].price;
    const avgOI = data.reduce((sum, d) => sum + d.openInterest, 0) / data.length;

    // Decreasing OI during price rise is an overheating signal
    if (recentPriceChange > 0.05 && currentOI < avgOI * 0.9) {
      return {
        type: 'OI_SIGNAL',
        strength: Math.abs(recentPriceChange) * 100,
        timestamp: Date.now(),
        timeframe: 'HTF'
      };
    }

    return null;
  }

  private calculateSlope(values: number[]): number {
    const n = values.length;
    const xSum = (n * (n - 1)) / 2;
    const ySum = values.reduce((sum, val) => sum + val, 0);
    const xySum = values.reduce((sum, val, i) => sum + i * val, 0);
    const xSquareSum = (n * (n - 1) * (2 * n - 1)) / 6;

    return (n * xySum - xSum * ySum) / (n * xSquareSum - xSum * xSum);
  }
}
Enter fullscreen mode Exit fullscreen mode

Cooling Level Analysis

Integrate multiple signals to determine the cooling level:

// services/coolingAnalyzer.ts
export class CoolingAnalyzer {

  analyzeCoolingLevel(signals: DetectedSignal[]): CoolingAnalysis {
    const signalWeights = {
      HIGH_ZONE: 0.4,
      PRICE_WEAK: 0.2,
      VOLUME_WEAK: 0.2,
      OI_SIGNAL: 0.2
    };

    let totalScore = 0;
    let weightSum = 0;

    signals.forEach(signal => {
      const weight = signalWeights[signal.type];
      totalScore += signal.strength * weight;
      weightSum += weight;
    });

    const confidence = weightSum > 0 ? totalScore / (weightSum * 100) : 0;
    const level = this.determineCoolingLevel(confidence, signals);

    return {
      level,
      signals,
      confidence,
      recommendation: this.generateRecommendation(level, confidence)
    };
  }

  private determineCoolingLevel(confidence: number, signals: DetectedSignal[]): CoolingLevel {
    const highZoneSignal = signals.find(s => s.type === 'HIGH_ZONE');
    const weaknessSignals = signals.filter(s => 
      s.type === 'PRICE_WEAK' || s.type === 'VOLUME_WEAK' || s.type === 'OI_SIGNAL'
    );

    if (highZoneSignal && weaknessSignals.length >= 2 && confidence > 0.7) {
      return 'confirmed';
    } else if (highZoneSignal && weaknessSignals.length >= 1 && confidence > 0.5) {
      return 'watch';
    } else if (weaknessSignals.length >= 1 && confidence > 0.3) {
      return 'mild';
    }

    return 'none';
  }

  private generateRecommendation(level: CoolingLevel, confidence: number): string {
    const recommendations = {
      confirmed: 'Market overheating confirmed. Consider reducing positions.',
      watch: 'Signs of overheating detected. Monitor closely and prepare for profit-taking.',
      mild: 'Mild bearish signals detected. Watch for trend changes.',
      none: 'No clear overheating signals currently detected.'
    };

    return recommendations[level];
  }
}
Enter fullscreen mode Exit fullscreen mode

Express API Endpoints

Implement API endpoints to provide analysis results:

// routes/analysis.ts
import { Router } from 'express';
import { KuCoinApiService } from '../services/kucoinApi';
import { SignalDetector } from '../services/signalDetector';
import { CoolingAnalyzer } from '../services/coolingAnalyzer';

const router = Router();
const apiService = new KuCoinApiService();
const signalDetector = new SignalDetector();
const coolingAnalyzer = new CoolingAnalyzer();

router.get('/analyze/:symbol', async (req, res) => {
  try {
    const { symbol } = req.params;
    const { timeframe = 'HTF', interval = '4h' } = req.query;

    // Fetch data
    const marketData = await apiService.getKlineData(symbol, interval as string);
    const currentOI = await apiService.getOpenInterest(symbol);

    // Integrate open interest data
    marketData.forEach(data => data.openInterest = currentOI);

    // Detect signals
    const signals = [
      signalDetector.detectHighZone(marketData),
      signalDetector.detectPriceWeak(marketData),
      signalDetector.detectVolumeWeak(marketData),
      signalDetector.detectOISignal(marketData, currentOI)
    ].filter(signal => signal !== null);

    // Analyze cooling level
    const analysis = coolingAnalyzer.analyzeCoolingLevel(signals);

    res.json({
      symbol,
      timeframe,
      analysis,
      timestamp: Date.now()
    });

  } catch (error) {
    console.error('Analysis error:', error);
    res.status(500).json({ error: 'Analysis failed' });
  }
});

export default router;
Enter fullscreen mode Exit fullscreen mode

Test Implementation

Unit testing with Vitest is also important:

// tests/signalDetector.test.ts
import { describe, it, expect } from 'vitest';
import { SignalDetector } from '../src/services/signalDetector';
import type { MarketData } from '../src/types/market';

describe('SignalDetector', () => {
  const detector = new SignalDetector();

  it('should detect high zone signal', () => {
    const mockData: MarketData[] = Array.from({ length: 20 }, (_, i) => ({
      timestamp: Date.now() - (20 - i) * 60000,
      price: i < 15 ? 100 : 120, // Last 5 candles show 20% increase
      volume: 1000,
      openInterest: 5000,
      symbol: 'BTCUSDT'
    }));

    const signal = detector.detectHighZone(mockData);

    expect(signal).toBeTruthy();
    expect(signal?.type).toBe('HIGH_ZONE');
    expect(signal?.strength).toBeGreaterThan(15);
  });

  it('should detect volume weakness', () => {
    const mockData: MarketData[] = Array.from({ length: 10 }, (_, i) => ({
      timestamp: Date.now() - (10 - i) * 60000,
      price: 100,
      volume: i < 7 ? 1000 : 500, // Last 3 candles show 50% volume drop
      openInterest: 5000,
      symbol: 'BTCUSDT'
    }));

    const signal = detector.detectVolumeWeak(mockData);

    expect(signal).toBeTruthy();
    expect(signal?.type).toBe('VOLUME_WEAK');
  });
});
Enter fullscreen mode Exit fullscreen mode

This system provides a comprehensive framework for detecting cryptocurrency market overheating and cooling

Top comments (0)