/** * 추세 강도(정배열/역배열) 계산 유틸리티 * * 정배열: 단기 이동평균 > 중기 이동평균 > 장기 이동평균 (상승 추세) * 역배열: 단기 이동평균 < 중기 이동평균 < 장기 이동평균 (하락 추세) * * 공식: * S = ((MA10 - MA60) + (MA20 - MA60)) / ATR(14) * TrendStrength = tanh(S) * 100 * * 결과: -100 ~ +100 * - +100에 가까움: 강한 정배열(상승추세) * - -100에 가까움: 강한 역배열(하락추세) * - 0 근처: 추세 약함 또는 혼조 */ // OHLC 캔들 데이터 인터페이스 export interface OHLCData { open: number; high: number; low: number; close: number; volume?: number; timestamp?: string; } // 추세 강도 결과 인터페이스 export interface TrendStrengthResult { symbol: string; koreanName: string; englishName?: string; trendStrength: number; // -100 ~ +100 ma10: number; ma20: number; ma60: number; atr14: number; isFavorite?: boolean; } /** * 단순 이동평균(SMA) 계산 * @param data 종가 배열 * @param period 이동평균 기간 * @returns 이동평균 값 (데이터가 부족하면 null) */ export function calculateSMA(data: number[], period: number): number | null { if (data.length < period) return null; const recentData = data.slice(-period); const sum = recentData.reduce((acc, val) => acc + val, 0); return sum / period; } /** * ATR(Average True Range) 계산 * @param ohlcData OHLC 데이터 배열 * @param period ATR 기간 (기본: 14) * @returns ATR 값 (데이터가 부족하면 null) */ export function calculateATR(ohlcData: OHLCData[], period: number = 14): number | null { if (ohlcData.length < period + 1) return null; const trueRanges: number[] = []; // True Range 계산 (최근 period+1개 데이터 사용) const recentData = ohlcData.slice(-(period + 1)); for (let i = 1; i < recentData.length; i++) { const current = recentData[i]; const previous = recentData[i - 1]; // True Range = max(High - Low, |High - Previous Close|, |Low - Previous Close|) const tr = Math.max( current.high - current.low, Math.abs(current.high - previous.close), Math.abs(current.low - previous.close) ); trueRanges.push(tr); } // ATR = SMA of True Ranges const atr = trueRanges.reduce((acc, val) => acc + val, 0) / trueRanges.length; return atr; } /** * 추세 강도 계산 * * 공식: * S = ((MA10 - MA60) + (MA20 - MA60)) / ATR(14) * TrendStrength = tanh(S) * 100 * * @param ohlcData OHLC 데이터 배열 (최소 60개 이상 필요) * @returns 추세 강도 (-100 ~ +100) 또는 null */ export function calculateTrendStrength(ohlcData: OHLCData[]): { trendStrength: number; ma10: number; ma20: number; ma60: number; atr14: number; } | null { // 최소 60개 데이터 필요 if (ohlcData.length < 60) { console.warn('추세 강도 계산에 최소 60개 데이터 필요'); return null; } // 종가 배열 추출 const closePrices = ohlcData.map(d => d.close); // 이동평균 계산 const ma10 = calculateSMA(closePrices, 10); const ma20 = calculateSMA(closePrices, 20); const ma60 = calculateSMA(closePrices, 60); // ATR 계산 const atr14 = calculateATR(ohlcData, 14); // 유효성 검사 if (ma10 === null || ma20 === null || ma60 === null || atr14 === null || atr14 === 0) { return null; } // S 계산: ((MA10 - MA60) + (MA20 - MA60)) / ATR(14) const s = ((ma10 - ma60) + (ma20 - ma60)) / atr14; // TrendStrength = tanh(S) * 100 const trendStrength = Math.tanh(s) * 100; return { trendStrength: Math.round(trendStrength * 100) / 100, // 소수점 2자리 ma10: Math.round(ma10 * 100) / 100, ma20: Math.round(ma20 * 100) / 100, ma60: Math.round(ma60 * 100) / 100, atr14: Math.round(atr14 * 100) / 100, }; } /** * 추세 강도에 따른 라벨 반환 * @param strength 추세 강도 값 * @returns 라벨 문자열 */ export function getTrendLabel(strength: number): string { if (strength >= 80) return '매우 강한 정배열'; if (strength >= 50) return '강한 정배열'; if (strength >= 20) return '정배열'; if (strength >= -20) return '중립'; if (strength >= -50) return '역배열'; if (strength >= -80) return '강한 역배열'; return '매우 강한 역배열'; } /** * 정배열 강도 계산 (0 ~ 100) * trendStrength가 양수일 때만 값이 있음 * @param trendStrength 통합 추세 강도 (-100 ~ +100) * @returns 정배열 강도 (0 ~ 100) */ export function getBullishStrength(trendStrength: number): number { return Math.max(0, trendStrength); } /** * 역배열 강도 계산 (0 ~ 100) * trendStrength가 음수일 때만 값이 있음 * @param trendStrength 통합 추세 강도 (-100 ~ +100) * @returns 역배열 강도 (0 ~ 100) */ export function getBearishStrength(trendStrength: number): number { return Math.max(0, -trendStrength); } /** * 정배열/역배열 강도 분리 반환 * @param trendStrength 통합 추세 강도 (-100 ~ +100) * @returns { bullish: 정배열 강도, bearish: 역배열 강도 } */ export function getStrengthValues(trendStrength: number): { bullish: number; bearish: number } { return { bullish: getBullishStrength(trendStrength), bearish: getBearishStrength(trendStrength), }; } /** * 추세 강도에 따른 색상 반환 * @param strength 추세 강도 값 * @returns 색상 코드 */ export function getTrendColor(strength: number): string { if (strength >= 50) return '#c62828'; // 강한 상승 - 빨간색 if (strength >= 20) return '#ef5350'; // 상승 - 연한 빨간색 if (strength >= -20) return '#9e9e9e'; // 중립 - 회색 if (strength >= -50) return '#42a5f5'; // 하락 - 연한 파란색 return '#1565c0'; // 강한 하락 - 파란색 } /** * 추세 강도 기준 정렬 함수 * @param items 정렬할 항목 배열 * @param sortMode 정렬 모드 ('default' | 'bullish' | 'bearish') * @returns 정렬된 배열 */ export function sortByTrendStrength( items: T[], sortMode: 'default' | 'bullish' | 'bearish' ): T[] { const sorted = [...items]; switch (sortMode) { case 'bullish': // 정배열: 추세 강도가 높은 순 (내림차순) return sorted.sort((a, b) => (b.trendStrength ?? 0) - (a.trendStrength ?? 0)); case 'bearish': // 역배열: 추세 강도가 낮은 순 (오름차순) return sorted.sort((a, b) => (a.trendStrength ?? 0) - (b.trendStrength ?? 0)); case 'default': default: // 기본: 이름순 정렬 return sorted.sort((a, b) => a.koreanName.localeCompare(b.koreanName, 'ko')); } } /** * 배치 추세 강도 계산 (여러 종목 동시 처리) * @param symbolsWithData 종목별 OHLC 데이터 맵 * @returns 추세 강도 결과 배열 */ export function calculateBatchTrendStrength( symbolsWithData: Map ): TrendStrengthResult[] { const results: TrendStrengthResult[] = []; symbolsWithData.forEach((data, symbol) => { const strength = calculateTrendStrength(data.ohlcData); if (strength) { results.push({ symbol, koreanName: data.koreanName, englishName: data.englishName, trendStrength: strength.trendStrength, ma10: strength.ma10, ma20: strength.ma20, ma60: strength.ma60, atr14: strength.atr14, isFavorite: data.isFavorite, }); } else { // 데이터 부족 시 기본값 results.push({ symbol, koreanName: data.koreanName, englishName: data.englishName, trendStrength: 0, ma10: 0, ma20: 0, ma60: 0, atr14: 0, isFavorite: data.isFavorite, }); } }); return results; } // 사용 예시 /* import { calculateTrendStrength, sortByTrendStrength, getTrendLabel, getTrendColor } from './trendStrength'; // 단일 종목 추세 강도 계산 const ohlcData = [ { open: 100, high: 105, low: 98, close: 103 }, { open: 103, high: 108, low: 101, close: 106 }, // ... 최소 60개 데이터 ]; const result = calculateTrendStrength(ohlcData); if (result) { console.log(`추세 강도: ${result.trendStrength}`); console.log(`라벨: ${getTrendLabel(result.trendStrength)}`); console.log(`색상: ${getTrendColor(result.trendStrength)}`); } // 여러 종목 정렬 const items = [ { symbol: 'BTC', koreanName: '비트코인', trendStrength: 75 }, { symbol: 'ETH', koreanName: '이더리움', trendStrength: -30 }, { symbol: 'XRP', koreanName: '리플', trendStrength: 45 }, ]; // 정배열 순 정렬 (상승 추세가 강한 순) const bullishSorted = sortByTrendStrength(items, 'bullish'); // 결과: BTC(75) -> XRP(45) -> ETH(-30) // 역배열 순 정렬 (하락 추세가 강한 순) const bearishSorted = sortByTrendStrength(items, 'bearish'); // 결과: ETH(-30) -> XRP(45) -> BTC(75) */