goldenChat base source add
This commit is contained in:
@@ -0,0 +1,310 @@
|
||||
/**
|
||||
* 추세 강도(정배열/역배열) 계산 유틸리티
|
||||
*
|
||||
* 정배열: 단기 이동평균 > 중기 이동평균 > 장기 이동평균 (상승 추세)
|
||||
* 역배열: 단기 이동평균 < 중기 이동평균 < 장기 이동평균 (하락 추세)
|
||||
*
|
||||
* 공식:
|
||||
* 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)
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user