311 lines
8.9 KiB
TypeScript
311 lines
8.9 KiB
TypeScript
/**
|
|
* 추세 강도(정배열/역배열) 계산 유틸리티
|
|
*
|
|
* 정배열: 단기 이동평균 > 중기 이동평균 > 장기 이동평균 (상승 추세)
|
|
* 역배열: 단기 이동평균 < 중기 이동평균 < 장기 이동평균 (하락 추세)
|
|
*
|
|
* 공식:
|
|
* 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<T extends { trendStrength?: number; koreanName: string }>(
|
|
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<string, {
|
|
koreanName: string;
|
|
englishName?: string;
|
|
ohlcData: OHLCData[];
|
|
isFavorite?: boolean;
|
|
}>
|
|
): 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)
|
|
*/
|
|
|