Files
goldenChart/frontend_golden/src/components/IndicatorSettingsPopup.tsx
T
2026-05-23 15:11:48 +09:00

2165 lines
120 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useState, useEffect, useRef } from 'react';
import {
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Button,
TextField,
Typography,
Box,
Slider,
Divider,
useTheme,
useMediaQuery,
IconButton,
Grid,
Select,
MenuItem,
FormControl,
InputLabel,
Switch,
Paper,
PaperProps,
Autocomplete,
} from '@mui/material';
import Draggable from 'react-draggable';
import {
Close as CloseIcon,
Settings as SettingsIcon,
Add as AddIcon,
Delete as DeleteIcon,
HelpOutline as HelpOutlineIcon,
} from '@mui/icons-material';
import { useChartColors, ChartColorSettings } from '../contexts/ChartColorContext';
import { useIndicatorSettings, IndicatorSettings } from '../contexts/IndicatorSettingsContext';
// 보조지표 타입 정의
export type IndicatorType =
| 'volume'
| 'macd'
| 'stochastic'
| 'rsi'
| 'momentum'
| 'adx'
| 'dmi'
| 'cci'
| 'trix'
| 'disparity'
| 'newPsychological'
| 'investPsychological'
| 'williamsR'
| 'bwi'
| 'obv'
| 'volumeOsc'
| 'vr'
| 'vr2'
| 'dmi'
| 'ma'
| 'ichimoku';
interface IndicatorSettingsPopupProps {
open: boolean;
onClose: () => void;
indicatorType: IndicatorType | null;
}
// 보조지표별 정보
const indicatorInfo: Record<IndicatorType, { name: string; description: string }> = {
volume: { name: '거래량', description: '매수/매도 거래량을 막대 그래프로 표시' },
macd: { name: 'MACD', description: 'Moving Average Convergence Divergence - 이동평균 수렴·발산 지표' },
stochastic: { name: 'Stochastic Slow', description: '현재 가격이 일정 기간의 가격 범위에서 어디에 위치하는지 표시' },
rsi: { name: 'RSI', description: 'Relative Strength Index - 상대강도지수' },
momentum: { name: 'Momentum', description: 'Momentum - 현재 가격과 n기간 전 가격의 차이를 나타내는 모멘텀 지표' },
adx: { name: 'ADX', description: 'Average Directional Index - 추세 강도 지표' },
dmi: { name: 'DMI', description: 'Directional Movement Index - 방향성 지표 (ADX + +DI + -DI)' },
cci: { name: 'CCI', description: 'Commodity Channel Index - 상품 채널 지수' },
williamsR: { name: 'Williams %R', description: '과매수/과매도 상태를 나타내는 모멘텀 지표' },
obv: { name: 'OBV', description: 'On Balance Volume - 거래량 누적 지표' },
trix: { name: 'TRIX', description: 'Triple Exponential Average - 삼중지수이동평균' },
newPsychological: { name: '신심리도', description: '일정 기간 동안 상승일 비율' },
investPsychological: { name: '투자심리도', description: '투자자들의 심리 상태를 나타내는 지표' },
disparity: { name: '이격도', description: '현재 가격과 이동평균선 간의 괴리 정도' },
bwi: { name: 'BWI', description: 'Bollinger Band Width Index - 볼린저밴드 %B' },
volumeOsc: { name: 'VolumeOSC', description: 'Volume Oscillator - 거래량 오실레이터' },
vr: { name: 'VR', description: 'Volume Ratio - 거래량 비율' },
vr2: { name: 'VR2', description: 'Volume Ratio (추가) - 거래량 비율 (별도 설정)' },
ma: { name: '이동평균선', description: 'Moving Average - 일정 기간 가격의 평균을 선으로 표시' },
ichimoku: { name: '일목균형표', description: 'Ichimoku Cloud - 추세, 지지/저항, 모멘텀을 한눈에 표시' },
};
// 보조지표별 상세 설명
const getIndicatorDetailedHelp = (type: IndicatorType): React.ReactNode => {
switch (type) {
case 'macd':
return (
<Box>
<Typography variant="h6" sx={{ fontWeight: 600, mb: 2 }}>1️⃣ MACD란?</Typography>
<Typography variant="body2" paragraph>
MACD는 개의 이동평균선의 차이를 이용해
</Typography>
<Box sx={{ pl: 2, mb: 2 }}>
<Typography variant="body2"> 추세 방향</Typography>
<Typography variant="body2"> 매수·매도 시점</Typography>
<Typography variant="body2"> 힘의 강도(모멘텀)</Typography>
</Box>
<Typography variant="body2" paragraph>
판단하는 지표입니다.
</Typography>
<Typography variant="body2" sx={{ fontStyle: 'italic', color: 'primary.main', mb: 3 }}>
"주가의 흐름이 강해지고 있는지, 약해지고 있는지" 보여줍니다.
</Typography>
<Typography variant="h6" sx={{ fontWeight: 600, mb: 2 }}>2️⃣ MACD의 구성 요소 (3가지)</Typography>
<Box sx={{ pl: 2, mb: 3 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1 }}> MACD </Typography>
<Typography variant="body2" sx={{ pl: 2, mb: 2 }}>
12 지수이동평균(EMA) 26 EMA<br />
단기 추세와 중기 추세의 차이
</Typography>
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1 }}> 시그널 </Typography>
<Typography variant="body2" sx={{ pl: 2, mb: 2 }}>
MACD 선의 9 EMA<br />
매매 신호를 보조하는 기준선
</Typography>
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1 }}> 히스토그램</Typography>
<Typography variant="body2" sx={{ pl: 2, mb: 2 }}>
MACD 시그널 <br />
선의 간격을 막대그래프로 표시<br />
힘이 커지는지 / 약해지는지 시각적으로 보여줌
</Typography>
</Box>
<Typography variant="h6" sx={{ fontWeight: 600, mb: 2 }}>3️⃣ MACD 해석 방법 (핵심)</Typography>
<Box sx={{ mb: 3 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1 }}>🔹 1. 골든크로스 / 데드크로스</Typography>
<Typography variant="body2" sx={{ pl: 2, mb: 1 }}>가장 많이 쓰이는 신호입니다.</Typography>
<Box sx={{ pl: 2, mb: 2 }}>
<Typography variant="body2" sx={{ color: 'success.main', fontWeight: 600 }}> 매수 신호</Typography>
<Typography variant="body2" sx={{ pl: 2, mb: 1 }}>
MACD 선이 시그널 선을 아래에서 위로 돌파<br />
상승 모멘텀 시작 가능성
</Typography>
<Typography variant="body2" sx={{ color: 'error.main', fontWeight: 600 }}> 매도 신호</Typography>
<Typography variant="body2" sx={{ pl: 2 }}>
MACD 선이 시그널 선을 위에서 아래로 이탈<br />
하락 모멘텀 시작 가능성
</Typography>
</Box>
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1 }}>🔹 2. 0 기준 해석</Typography>
<Typography variant="body2" sx={{ pl: 2, mb: 1 }}>MACD가 0 /아래에 있는지도 중요합니다.</Typography>
<Box sx={{ pl: 2, mb: 2 }}>
<Typography variant="body2"> 0 상승 추세 우위</Typography>
<Typography variant="body2"> 0 아래 하락 추세 우위</Typography>
<Typography variant="body2" sx={{ mt: 1, color: 'warning.main' }}>
📌 0 위에서 골든크로스 = 신뢰도 <br />
📌 0 아래 데드크로스 = 하락 지속 가능성
</Typography>
</Box>
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1 }}>🔹 3. 히스토그램 변화</Typography>
<Typography variant="body2" sx={{ pl: 2, mb: 1 }}>히스토그램은 추세의 힘을 보여줍니다.</Typography>
<Box sx={{ pl: 2, mb: 2 }}>
<Typography variant="body2"> 막대가 점점 커짐 추세 강화</Typography>
<Typography variant="body2"> 막대가 줄어듦 추세 약화</Typography>
<Typography variant="body2"> 방향 전환 추세 전환 가능성</Typography>
</Box>
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1 }}>🔹 4. 다이버전스 (중요)</Typography>
<Typography variant="body2" sx={{ pl: 2, mb: 1 }}>주가와 MACD의 방향이 다를 나타납니다.</Typography>
<Box sx={{ pl: 2, mb: 2 }}>
<Typography variant="body2" sx={{ color: 'success.main', fontWeight: 600 }}> 강세 다이버전스</Typography>
<Typography variant="body2" sx={{ pl: 2, mb: 1 }}>
주가는 하락하는데 MACD는 저점 상승<br />
반등 가능성
</Typography>
<Typography variant="body2" sx={{ color: 'error.main', fontWeight: 600 }}> 약세 다이버전스</Typography>
<Typography variant="body2" sx={{ pl: 2 }}>
주가는 상승하는데 MACD는 고점 하락<br />
조정 가능성
</Typography>
</Box>
</Box>
<Typography variant="h6" sx={{ fontWeight: 600, mb: 2 }}>4️⃣ 실전 활용 </Typography>
<Box sx={{ pl: 2, mb: 3 }}>
<Typography variant="body2" sx={{ mb: 1 }}> MACD는 추세장에서 강력</Typography>
<Typography variant="body2" sx={{ pl: 2, mb: 1, fontSize: '0.85rem' }}>횡보장에서는 잦은 헛신호 발생</Typography>
<Typography variant="body2" sx={{ mb: 1 }}> 다른 지표와 함께 사용</Typography>
<Typography variant="body2" sx={{ pl: 2, mb: 1, fontSize: '0.85rem' }}>RSI, 이동평균선, 거래량과 병행 추천</Typography>
<Typography variant="body2" sx={{ mb: 1 }}> 시간봉에 따라 성격이 다름</Typography>
<Typography variant="body2" sx={{ pl: 2, fontSize: '0.85rem' }}>
일봉 중기 추세<br />
분봉 단기 매매 (노이즈 많음)
</Typography>
</Box>
<Typography variant="h6" sx={{ fontWeight: 600, mb: 2 }}>5️⃣ MACD의 한계 (중요)</Typography>
<Box sx={{ pl: 2 }}>
<Typography variant="body2" sx={{ color: 'error.main', mb: 1 }}> 후행성 지표</Typography>
<Typography variant="body2" sx={{ pl: 2, mb: 2, fontSize: '0.85rem' }}>이동평균 기반 신호가 늦을 있음</Typography>
<Typography variant="body2" sx={{ color: 'error.main', mb: 1 }}> 단독 사용은 위험</Typography>
<Typography variant="body2" sx={{ pl: 2, fontSize: '0.85rem' }}>반드시 차트 패턴·지지저항·거래량과 함께 확인</Typography>
</Box>
</Box>
);
case 'rsi':
return (
<Box>
<Typography variant="h6" sx={{ fontWeight: 600, mb: 2 }}>1️⃣ RSI란?</Typography>
<Typography variant="body2" paragraph>
RSI는 주가의 상승·하락 강도를 수치화한 모멘텀 지표입니다.
</Typography>
<Box sx={{ pl: 2, mb: 2 }}>
<Typography variant="body2"> 값의 범위: 0 ~ 100</Typography>
<Typography variant="body2"> "최근 일정 기간 동안 매수세와 매도세 중 누가 더 강한가" 보여줌</Typography>
</Box>
<Typography variant="body2" sx={{ color: 'warning.main', mb: 3 }}>
📌 과매수·과매도 판단에 가장 많이 쓰이는 지표 하나
</Typography>
<Typography variant="h6" sx={{ fontWeight: 600, mb: 2 }}>2️⃣ RSI 기본 설정</Typography>
<Box sx={{ pl: 2, mb: 3 }}>
<Typography variant="body2" sx={{ mb: 1 }}> 기간: 14 (표준)</Typography>
<Typography variant="body2"> 거의 모든 HTS/MTS에서 기본값</Typography>
</Box>
<Typography variant="h6" sx={{ fontWeight: 600, mb: 2 }}>3️⃣ 핵심 해석 방법</Typography>
<Box sx={{ mb: 3 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1 }}>🔹 과매수 / 과매도 구간</Typography>
<Typography variant="body2" sx={{ pl: 2, mb: 1 }}>가장 기본적인 사용법입니다.</Typography>
<Box sx={{ pl: 2, mb: 2 }}>
<Typography variant="body2" sx={{ mb: 1 }}> RSI 70 과매수 (조정 가능성)</Typography>
<Typography variant="body2" sx={{ mb: 1 }}> RSI 30 과매도 (반등 가능성)</Typography>
<Typography variant="body2" sx={{ color: 'error.main', mb: 1 }}>⚠️ 수치 도달 즉시 매매 </Typography>
<Typography variant="body2" sx={{ color: 'success.main' }}> 꺾임(되돌림) 확인 대응</Typography>
</Box>
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1 }}>🔹 50 기준 추세 판단</Typography>
<Box sx={{ pl: 2, mb: 2 }}>
<Typography variant="body2" sx={{ mb: 1 }}> 50 이상 상승 추세 우위</Typography>
<Typography variant="body2" sx={{ mb: 1 }}> 50 이하 하락 추세 우위</Typography>
<Typography variant="body2" sx={{ color: 'warning.main' }}>📌 추세 확인용으로 매우 유용</Typography>
</Box>
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1 }}>🔹 다이버전스 (중요)</Typography>
<Box sx={{ pl: 2, mb: 2 }}>
<Typography variant="body2" sx={{ color: 'success.main', fontWeight: 600, mb: 1 }}> 강세 다이버전스</Typography>
<Typography variant="body2" sx={{ pl: 2, fontSize: '0.85rem', mb: 1 }}>
주가: 저점 낮아짐<br />
RSI: 저점 높아짐<br />
반등 가능성
</Typography>
<Typography variant="body2" sx={{ color: 'error.main', fontWeight: 600, mb: 1 }}> 약세 다이버전스</Typography>
<Typography variant="body2" sx={{ pl: 2, fontSize: '0.85rem' }}>
주가: 고점 높아짐<br />
RSI: 고점 낮아짐<br />
하락 가능성
</Typography>
</Box>
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1 }}>🔹 RSI 추세선 활용</Typography>
<Box sx={{ pl: 2 }}>
<Typography variant="body2" sx={{ mb: 1 }}> RSI에도 지지·저항선을 그릴 있음</Typography>
<Typography variant="body2"> RSI 추세선 이탈 = 주가 방향 전환 신호</Typography>
</Box>
</Box>
<Typography variant="h6" sx={{ fontWeight: 600, mb: 2 }}>4️⃣ 실전 활용 예시</Typography>
<Box sx={{ pl: 2, mb: 3 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1 }}>📌 매수 타점 </Typography>
<Typography variant="body2" sx={{ pl: 2, mb: 2, fontSize: '0.85rem' }}>
RSI 30 이하 진입<br />
바닥 다지고 30 상향 돌파<br />
거래량 증가 확인 분할 매수
</Typography>
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1 }}>📌 매도 타점 </Typography>
<Typography variant="body2" sx={{ pl: 2, fontSize: '0.85rem' }}>
RSI 70 이상 진입<br />
고점 형성 70 하향 이탈<br />
분할 매도
</Typography>
</Box>
<Typography variant="h6" sx={{ fontWeight: 600, mb: 2 }}>5️⃣ 매매 스타일별 RSI 설정</Typography>
<Box sx={{ pl: 2, mb: 3 }}>
<Box sx={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 1, mb: 2 }}>
<Typography variant="body2" sx={{ fontWeight: 600 }}>스타일</Typography>
<Typography variant="body2" sx={{ fontWeight: 600 }}>RSI 설정</Typography>
<Typography variant="body2">단타</Typography>
<Typography variant="body2">7 ~ 9</Typography>
<Typography variant="body2">스윙</Typography>
<Typography variant="body2">14</Typography>
<Typography variant="body2">중·장기</Typography>
<Typography variant="body2">21</Typography>
</Box>
<Typography variant="body2" sx={{ color: 'warning.main', fontSize: '0.85rem', mb: 1 }}>📌 기간 신호 빠름 (노이즈 )</Typography>
<Typography variant="body2" sx={{ color: 'warning.main', fontSize: '0.85rem' }}>📌 기간 안정적 (신호 느림)</Typography>
</Box>
<Typography variant="h6" sx={{ fontWeight: 600, mb: 2 }}>6️⃣ RSI의 장점과 한계</Typography>
<Box sx={{ pl: 2, mb: 3 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1 }}> 장점</Typography>
<Typography variant="body2" sx={{ pl: 2, mb: 2, fontSize: '0.85rem' }}>
직관적<br />
과열 구간 파악 쉬움<br />
대부분의 시장에 적용 가능
</Typography>
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1 }}> 한계</Typography>
<Typography variant="body2" sx={{ pl: 2, mb: 2, fontSize: '0.85rem' }}>
강한 추세장에서는 신호 왜곡<br />
단독 사용 헛신호
</Typography>
<Typography variant="body2" sx={{ color: 'info.main' }}>👉 MACD·이동평균·거래량과 병행 필수</Typography>
</Box>
<Typography variant="h6" sx={{ fontWeight: 600, mb: 2 }}> 요약</Typography>
<Typography variant="body2" sx={{ fontStyle: 'italic', color: 'primary.main' }}>
RSI는 매수·매도 세력의 힘을 보여주는 지표로, 30·70 기준과 다이버전스가 핵심 포인트입니다.
</Typography>
</Box>
);
case 'stochastic':
return (
<Box>
<Typography variant="h6" sx={{ fontWeight: 600, mb: 2 }}>1️⃣ Stochastic Slow란?</Typography>
<Typography variant="body2" paragraph>
스토캐스틱은
</Typography>
<Typography variant="body2" sx={{ fontStyle: 'italic', color: 'primary.main', mb: 2 }}>
"현재 가격이 최근 일정 기간의 고가–저가 범위 중 어디에 위치하는가"
</Typography>
<Typography variant="body2" paragraph>
보여주는 지표입니다.
</Typography>
<Box sx={{ pl: 2, mb: 2 }}>
<Typography variant="body2"> 가격이 고가 근처 매수 과열 가능</Typography>
<Typography variant="body2"> 가격이 저가 근처 매도 과열 가능</Typography>
</Box>
<Typography variant="body2" sx={{ color: 'warning.main', mb: 3 }}>
📌 MACD가 추세 지표라면, Stochastic은 속도·과열 지표입니다.
</Typography>
<Typography variant="h6" sx={{ fontWeight: 600, mb: 2 }}>2️⃣ Stochastic Slow의 구성요소</Typography>
<Box sx={{ pl: 2, mb: 3 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1 }}>🔹 %K (느린 K)</Typography>
<Typography variant="body2" sx={{ pl: 2, mb: 2 }}>
가격의 위치를 나타내는 주선<br />
Fast Stochastic의 %K를 이동평균 처리한
</Typography>
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1 }}>🔹 %D</Typography>
<Typography variant="body2" sx={{ pl: 2, mb: 2 }}>
%K의 이동평균 (신호선 역할)
</Typography>
<Typography variant="body2" sx={{ color: 'info.main' }}>
👉 Slow Stochastic은 %K와 %D 선으로 해석합니다.
</Typography>
</Box>
<Typography variant="h6" sx={{ fontWeight: 600, mb: 2 }}>3️⃣ 계산 방식 (개념적으로)</Typography>
<Box sx={{ pl: 2, mb: 3 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1 }}>기본 설정 기준:</Typography>
<Typography variant="body2" sx={{ pl: 2, mb: 1 }}>(14, 3, 3) 가장 보편적</Typography>
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1 }}>의미:</Typography>
<Typography variant="body2" sx={{ pl: 2, mb: 2, fontSize: '0.85rem' }}>
14 기준 기간<br />
3 %K를 느리게 만드는 이동평균<br />
3 %D 이동평균
</Typography>
<Typography variant="body2" sx={{ fontSize: '0.85rem' }}>
실제 계산은 자동이므로 원리만 이해하면 충분합니다.
</Typography>
</Box>
<Typography variant="h6" sx={{ fontWeight: 600, mb: 2 }}>4️⃣ 핵심 해석 방법</Typography>
<Box sx={{ mb: 3 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1 }}>🔸 1. 과매수 / 과매도 구간</Typography>
<Typography variant="body2" sx={{ pl: 2, mb: 1 }}>가장 기본적인 사용법입니다.</Typography>
<Box sx={{ pl: 2, mb: 2 }}>
<Typography variant="body2" sx={{ mb: 1 }}> 80 이상 과매수 구간</Typography>
<Typography variant="body2" sx={{ mb: 1 }}> 20 이하 과매도 구간</Typography>
<Typography variant="body2" sx={{ color: 'warning.main' }}>
⚠️ 바로 매도·매수하는 아니라 전환 신호를 기다리는 것이 중요
</Typography>
</Box>
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1 }}>🔸 2. 골든크로스 / 데드크로스</Typography>
<Box sx={{ pl: 2, mb: 2 }}>
<Typography variant="body2" sx={{ color: 'success.main', fontWeight: 600, mb: 1 }}> 매수 신호</Typography>
<Typography variant="body2" sx={{ pl: 2, fontSize: '0.85rem', mb: 1 }}>
20 이하 과매도 구간에서<br />
%K가 %D를 아래에서 위로 돌파
</Typography>
<Typography variant="body2" sx={{ color: 'error.main', fontWeight: 600, mb: 1 }}> 매도 신호</Typography>
<Typography variant="body2" sx={{ pl: 2, fontSize: '0.85rem', mb: 1 }}>
80 이상 과매수 구간에서<br />
%K가 %D를 위에서 아래로 이탈
</Typography>
<Typography variant="body2" sx={{ color: 'warning.main' }}>📌 구간 크로스는 신뢰도 낮음</Typography>
</Box>
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1 }}>🔸 3. 다이버전스 (중요)</Typography>
<Box sx={{ pl: 2 }}>
<Typography variant="body2" sx={{ color: 'success.main', fontWeight: 600, mb: 1 }}> 강세 다이버전스</Typography>
<Typography variant="body2" sx={{ pl: 2, fontSize: '0.85rem', mb: 1 }}>
주가는 저점 갱신<br />
Stochastic은 저점 상승<br />
반등 가능성
</Typography>
<Typography variant="body2" sx={{ color: 'error.main', fontWeight: 600, mb: 1 }}> 약세 다이버전스</Typography>
<Typography variant="body2" sx={{ pl: 2, fontSize: '0.85rem' }}>
주가는 고점 갱신<br />
Stochastic은 고점 하락<br />
조정 가능성
</Typography>
</Box>
</Box>
<Typography variant="h6" sx={{ fontWeight: 600, mb: 2 }}>5️⃣ Fast vs Slow 차이</Typography>
<Box sx={{ pl: 2, mb: 3 }}>
<Box sx={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 1, mb: 2 }}>
<Typography variant="body2" sx={{ fontWeight: 600 }}>구분</Typography>
<Typography variant="body2" sx={{ fontWeight: 600 }}>Fast</Typography>
<Typography variant="body2" sx={{ fontWeight: 600 }}>Slow</Typography>
<Typography variant="body2">반응 속도</Typography>
<Typography variant="body2">매우 빠름</Typography>
<Typography variant="body2">느림</Typography>
<Typography variant="body2">노이즈</Typography>
<Typography variant="body2">많음</Typography>
<Typography variant="body2">적음</Typography>
<Typography variant="body2">실전 활용</Typography>
<Typography variant="body2">제한적</Typography>
<Typography variant="body2">가장 많이 사용</Typography>
</Box>
<Typography variant="body2" sx={{ color: 'info.main' }}>
👉 실전에서는 Slow Stochastic을 기본으로 사용
</Typography>
</Box>
<Typography variant="h6" sx={{ fontWeight: 600, mb: 2 }}>6️⃣ 실전 활용 </Typography>
<Box sx={{ pl: 2, mb: 3 }}>
<Typography variant="body2" sx={{ mb: 1 }}> 횡보장·조정장에 강함</Typography>
<Typography variant="body2" sx={{ pl: 2, mb: 1, fontSize: '0.85rem', color: 'error.main' }}>
강한 상승장에서는 과매수 상태가 오래 지속될 있음
</Typography>
<Typography variant="body2" sx={{ mb: 1 }}> MACD, 이동평균과 병행 추천</Typography>
<Typography variant="body2" sx={{ pl: 2, mb: 1, fontSize: '0.85rem' }}>
MACD로 추세 확인<br />
Stochastic으로 진입 타이밍
</Typography>
<Typography variant="body2" sx={{ mb: 1 }}> 분봉에서는 설정값 늘리면 안정적</Typography>
</Box>
<Typography variant="h6" sx={{ fontWeight: 600, mb: 2 }}>7️⃣ 대표적인 설정값</Typography>
<Box sx={{ pl: 2, mb: 3 }}>
<Typography variant="body2" sx={{ mb: 1 }}> 기본: (14, 3, 3)</Typography>
<Typography variant="body2" sx={{ mb: 1 }}> 단타: (9, 3, 3)</Typography>
<Typography variant="body2"> 보수적: (21, 5, 5)</Typography>
</Box>
<Typography variant="h6" sx={{ fontWeight: 600, mb: 2 }}> 요약</Typography>
<Typography variant="body2" sx={{ fontStyle: 'italic', color: 'primary.main' }}>
Stochastic Slow는 가격의 과열 정도를 보여주는 지표로, 20·80 구간에서의 골든/데드크로스가 핵심 신호입니다.
</Typography>
</Box>
);
case 'volume':
return (
<Box>
<Typography variant="h6" sx={{ fontWeight: 600, mb: 2 }}>1️⃣ 거래량이란?</Typography>
<Typography variant="body2" paragraph>
거래량은 일정 기간 동안 거래된 주식의 수량을 나타내며, 주가 변동의 신뢰도를 확인하는 핵심 지표입니다.
</Typography>
<Typography variant="body2" sx={{ fontStyle: 'italic', color: 'primary.main', mb: 3 }}>
"거래량이 많다 = 많은 사람들이 참여했다 = 신뢰도 높은 움직임"
</Typography>
<Typography variant="h6" sx={{ fontWeight: 600, mb: 2 }}>2️⃣ 거래량 해석 방법</Typography>
<Box sx={{ pl: 2, mb: 3 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1 }}>🔹 가격과 거래량의 관계</Typography>
<Box sx={{ pl: 2, mb: 2 }}>
<Typography variant="body2" sx={{ color: 'success.main' }}> 상승 + 거래량 증가 강한 상승 (신뢰도 높음)</Typography>
<Typography variant="body2" sx={{ color: 'warning.main' }}> 상승 + 거래량 감소 약한 상승 (조정 가능성)</Typography>
<Typography variant="body2" sx={{ color: 'error.main' }}> 하락 + 거래량 증가 강한 하락 (추가 하락 가능성)</Typography>
<Typography variant="body2" sx={{ color: 'info.main' }}> 하락 + 거래량 감소 약한 하락 (바닥 근접)</Typography>
</Box>
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1 }}>🔹 거래량 패턴</Typography>
<Box sx={{ pl: 2 }}>
<Typography variant="body2" sx={{ mb: 1 }}> 거래량 급증 추세 전환 신호</Typography>
<Typography variant="body2" sx={{ mb: 1 }}> 거래량 감소 추세 약화 또는 횡보 가능성</Typography>
<Typography variant="body2"> 거래량 평균 이상 신뢰할 있는 움직임</Typography>
</Box>
</Box>
<Typography variant="h6" sx={{ fontWeight: 600, mb: 2 }}>3️⃣ 실전 활용 </Typography>
<Box sx={{ pl: 2 }}>
<Typography variant="body2" sx={{ mb: 1 }}> 모든 매매 판단의 기본</Typography>
<Typography variant="body2" sx={{ mb: 1 }}> 돌파 구간에서 거래량 확인 필수</Typography>
<Typography variant="body2"> 이동평균선과 함께 보면 효과적</Typography>
</Box>
</Box>
);
case 'cci':
return (
<Box>
<Typography variant="h6" sx={{ fontWeight: 600, mb: 2 }}>1️⃣ CCI란?</Typography>
<Typography variant="body2" paragraph>
CCI는
</Typography>
<Typography variant="body2" sx={{ fontStyle: 'italic', color: 'primary.main', mb: 2 }}>
"현재 가격이 최근 평균 가격보다 비싸게 거래되는지, 싸게 거래되는지"
</Typography>
<Typography variant="body2" paragraph>
수치로 나타낸 지표입니다.
</Typography>
<Box sx={{ pl: 2, mb: 2 }}>
<Typography variant="body2"> + 방향 평균보다 강함</Typography>
<Typography variant="body2"> 방향 평균보다 약함</Typography>
</Box>
<Typography variant="body2" sx={{ color: 'warning.main', mb: 3 }}>
📌 RSI·Stochastic과 달리 기준이 고정(0)이고, 과매수·과매도 범위가 유연한 것이 특징
</Typography>
<Typography variant="h6" sx={{ fontWeight: 600, mb: 2 }}>2️⃣ CCI 기본 설정</Typography>
<Box sx={{ pl: 2, mb: 3 }}>
<Typography variant="body2" sx={{ mb: 1 }}> 기간: 14 또는 20 (가장 많이 사용)</Typography>
<Typography variant="body2" sx={{ mb: 1 }}> 기준선: 0선</Typography>
<Typography variant="subtitle2" sx={{ fontWeight: 600, mt: 2, mb: 1 }}>일반적인 과열 기준:</Typography>
<Typography variant="body2" sx={{ pl: 2, mb: 1 }}> +100 이상 과매수</Typography>
<Typography variant="body2" sx={{ pl: 2 }}> 100 이하 과매도</Typography>
</Box>
<Typography variant="h6" sx={{ fontWeight: 600, mb: 2 }}>3️⃣ CCI 계산 개념 (이해만 하면 충분)</Typography>
<Typography variant="body2" paragraph>
CCI는 아래 개념으로 계산됩니다.
</Typography>
<Box sx={{ pl: 2, mb: 2 }}>
<Typography variant="body2" sx={{ mb: 1 }}> TP(대표가격) = (고가 + 저가 + 종가) ÷ 3</Typography>
<Typography variant="body2"> TP가 평균(TP의 이동평균)에서 얼마나 떨어져 있는지를 수치화</Typography>
</Box>
<Typography variant="body2" sx={{ color: 'info.main', mb: 3 }}>
👉 복잡해 보이지만 자동 계산되므로 해석이 핵심입니다.
</Typography>
<Typography variant="h6" sx={{ fontWeight: 600, mb: 2 }}>4️⃣ 핵심 해석 방법</Typography>
<Box sx={{ mb: 3 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1 }}>🔹 0 돌파 (추세 판단)</Typography>
<Box sx={{ pl: 2, mb: 2 }}>
<Typography variant="body2" sx={{ mb: 1 }}> CCI &gt; 0 상승 모멘텀 우위</Typography>
<Typography variant="body2" sx={{ mb: 1 }}> CCI &lt; 0 하락 모멘텀 우위</Typography>
<Typography variant="body2" sx={{ color: 'warning.main' }}>📌 0 돌파는 추세 시작 신호로 자주 활용</Typography>
</Box>
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1 }}>🔹 과매수 / 과매도</Typography>
<Box sx={{ pl: 2, mb: 2 }}>
<Typography variant="body2" sx={{ mb: 1 }}> +100 이상 과매수 구간</Typography>
<Typography variant="body2" sx={{ mb: 1 }}> 100 이하 과매도 구간</Typography>
<Typography variant="body2" sx={{ color: 'error.main', mb: 1 }}>⚠️ 도달 즉시 매매 </Typography>
<Typography variant="body2" sx={{ color: 'success.main' }}> 되돌림(재진입) 기다리는 중요</Typography>
</Box>
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1 }}>🔹 되돌림 매매 (가장 많이 사용)</Typography>
<Box sx={{ pl: 2, mb: 2 }}>
<Typography variant="body2" sx={{ color: 'success.main', fontWeight: 600, mb: 1 }}> 매수 신호</Typography>
<Typography variant="body2" sx={{ pl: 2, mb: 1, fontSize: '0.85rem' }}>
CCI가 100 이하 진입<br />
다시 100 위로 회복
</Typography>
<Typography variant="body2" sx={{ color: 'error.main', fontWeight: 600, mb: 1 }}> 매도 신호</Typography>
<Typography variant="body2" sx={{ pl: 2, mb: 1, fontSize: '0.85rem' }}>
CCI가 +100 이상 진입<br />
다시 +100 아래로 이탈
</Typography>
<Typography variant="body2" sx={{ color: 'warning.main' }}>📌 단기 매매에서 특히 효과적</Typography>
</Box>
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1 }}>🔹 다이버전스</Typography>
<Box sx={{ pl: 2, mb: 2 }}>
<Typography variant="body2" sx={{ color: 'success.main', fontWeight: 600, mb: 1 }}> 강세 다이버전스</Typography>
<Typography variant="body2" sx={{ pl: 2, mb: 1, fontSize: '0.85rem' }}>
주가 저점 하락<br />
CCI 저점 상승<br />
반등 가능성
</Typography>
<Typography variant="body2" sx={{ color: 'error.main', fontWeight: 600, mb: 1 }}> 약세 다이버전스</Typography>
<Typography variant="body2" sx={{ pl: 2, fontSize: '0.85rem' }}>
주가 고점 상승<br />
CCI 고점 하락<br />
하락 가능성
</Typography>
</Box>
</Box>
<Typography variant="h6" sx={{ fontWeight: 600, mb: 2 }}>5️⃣ RSI·Stochastic과 차이점</Typography>
<Box sx={{ pl: 2, mb: 3 }}>
<Box sx={{ display: 'grid', gridTemplateColumns: '1fr 2fr', gap: 1, mb: 1 }}>
<Typography variant="body2" sx={{ fontWeight: 600 }}>기준</Typography>
<Typography variant="body2">CCI: 0선 / RSI: 30·70 / Stochastic: 20·80</Typography>
</Box>
<Box sx={{ display: 'grid', gridTemplateColumns: '1fr 2fr', gap: 1, mb: 1 }}>
<Typography variant="body2" sx={{ fontWeight: 600 }}>범위</Typography>
<Typography variant="body2">CCI: 제한 없음 / RSI: 0~100 / Stochastic: 0~100</Typography>
</Box>
<Box sx={{ display: 'grid', gridTemplateColumns: '1fr 2fr', gap: 1, mb: 1 }}>
<Typography variant="body2" sx={{ fontWeight: 600 }}>성격</Typography>
<Typography variant="body2">CCI: 변동성 민감 / RSI: 힘의 균형 / Stochastic: 가격 위치</Typography>
</Box>
<Box sx={{ display: 'grid', gridTemplateColumns: '1fr 2fr', gap: 1, mb: 2 }}>
<Typography variant="body2" sx={{ fontWeight: 600 }}>단타 활용</Typography>
<Typography variant="body2">CCI: ★★★★☆ / RSI: ★★★ / Stochastic: ★★★★</Typography>
</Box>
<Typography variant="body2" sx={{ color: 'info.main' }}>👉 CCI는 짧은 파동 포착에 강함</Typography>
</Box>
<Typography variant="h6" sx={{ fontWeight: 600, mb: 2 }}>6️⃣ 실전 활용 </Typography>
<Box sx={{ pl: 2, mb: 3 }}>
<Typography variant="body2" sx={{ mb: 1 }}> 횡보·조정장에서 강력</Typography>
<Typography variant="body2" sx={{ pl: 2, mb: 1, fontSize: '0.85rem', color: 'error.main' }}> 강한 추세장에서는 과열 지속 가능</Typography>
<Typography variant="body2" sx={{ mb: 1 }}> 분봉에서는 기간 늘리면 안정적</Typography>
<Typography variant="body2" sx={{ pl: 2, mb: 1, fontSize: '0.85rem' }}>: 20 30</Typography>
<Typography variant="body2" sx={{ mb: 1 }}> MACD·이동평균과 병행 추천</Typography>
<Typography variant="body2" sx={{ pl: 2, fontSize: '0.85rem' }}>
추세는 MACD<br />
타점은 CCI
</Typography>
</Box>
<Typography variant="h6" sx={{ fontWeight: 600, mb: 2 }}>7️⃣ 대표적인 설정값</Typography>
<Box sx={{ pl: 2 }}>
<Typography variant="body2" sx={{ mb: 1 }}> 기본: CCI(20)</Typography>
<Typography variant="body2" sx={{ mb: 1 }}> 단타: CCI(9~14)</Typography>
<Typography variant="body2"> 보수적: CCI(30)</Typography>
</Box>
</Box>
);
case 'adx':
return (
<Box>
<Typography variant="h6" sx={{ fontWeight: 600, mb: 2 }}>1️⃣ ADX란?</Typography>
<Typography variant="body2" paragraph>
ADX(Average Directional Index) 추세의 강도를 측정하는 지표입니다. 상승인지 하락인지가 아니라 "추세가 얼마나 강한지" 보여줍니다.
</Typography>
<Typography variant="h6" sx={{ fontWeight: 600, mb: 2 }}>2️⃣ ADX 해석 방법</Typography>
<Box sx={{ pl: 2, mb: 3 }}>
<Typography variant="body2" sx={{ mb: 1 }}> ADX 25 미만 약한 추세 (횡보)</Typography>
<Typography variant="body2" sx={{ mb: 1 }}> ADX 25~50 강한 추세</Typography>
<Typography variant="body2"> ADX 50 이상 매우 강한 추세</Typography>
</Box>
<Typography variant="h6" sx={{ fontWeight: 600, mb: 2 }}>3️⃣ 실전 활용 </Typography>
<Box sx={{ pl: 2 }}>
<Typography variant="body2" sx={{ mb: 1 }}> 추세 추종 전략 사용 여부 결정</Typography>
<Typography variant="body2"> +DI, -DI와 함께 보면 방향까지 확인 가능</Typography>
</Box>
</Box>
);
case 'trix':
return (
<Box>
<Typography variant="h6" sx={{ fontWeight: 600, mb: 2 }}>TRIX (Triple Exponential Average Oscillator)</Typography>
<Typography variant="body2" paragraph>
📈 추세의 방향과 전환을 비교적 깔끔하게 보여주는 모멘텀 보조지표입니다.
</Typography>
<Typography variant="body2" sx={{ color: 'primary.main', mb: 3 }}>
이동평균의 노이즈를 크게 줄인 것이 가장 특징입니다.
</Typography>
<Typography variant="h6" sx={{ fontWeight: 600, mb: 2 }}>1️⃣ TRIX란?</Typography>
<Typography variant="body2" paragraph>
TRIX는
</Typography>
<Typography variant="body2" sx={{ fontWeight: 600, mb: 2 }}>
종가에 3 지수이동평균(Triple EMA) 적용한 , 변화율(기울기) 지표로 만든 것입니다.
</Typography>
<Typography variant="body2" paragraph>
,
</Typography>
<Box sx={{ pl: 2, mb: 2 }}>
<Typography variant="body2"> 단순 이동평균보다 훨씬 부드럽고</Typography>
<Typography variant="body2"> 잔파동(노이즈) 많이 제거한</Typography>
<Typography variant="body2"> 추세 중심 지표입니다.</Typography>
</Box>
<Typography variant="body2" sx={{ color: 'warning.main', mb: 3 }}>
📌 단타보다는 스윙·중기 매매에 적합
</Typography>
<Typography variant="h6" sx={{ fontWeight: 600, mb: 2 }}>2️⃣ TRIX의 구성 요소</Typography>
<Box sx={{ pl: 2, mb: 3 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1 }}>🔹 TRIX </Typography>
<Typography variant="body2" sx={{ pl: 2, mb: 2 }}>
3 EMA의 변화율<br />
추세의 방향과 속도를 나타냄
</Typography>
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1 }}>🔹 시그널 (선택)</Typography>
<Typography variant="body2" sx={{ pl: 2 }}>
TRIX의 이동평균<br />
매매 신호 보조
</Typography>
</Box>
<Typography variant="h6" sx={{ fontWeight: 600, mb: 2 }}>3️⃣ 기본 설정값</Typography>
<Box sx={{ pl: 2, mb: 3 }}>
<Typography variant="body2" sx={{ mb: 1 }}> TRIX 기간: 14 (또는 15)</Typography>
<Typography variant="body2" sx={{ mb: 1 }}> 시그널: 9</Typography>
<Typography variant="body2" sx={{ color: 'warning.main' }}>
📌 RSI·MACD보다 신호가 느린 대신 신뢰도가 높음
</Typography>
</Box>
<Typography variant="h6" sx={{ fontWeight: 600, mb: 2 }}>4️⃣ 핵심 해석 방법</Typography>
<Box sx={{ mb: 3 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1 }}>🔸 0 기준 해석 (가장 중요)</Typography>
<Box sx={{ pl: 2, mb: 2 }}>
<Typography variant="body2" sx={{ mb: 1 }}> TRIX &gt; 0 상승 추세</Typography>
<Typography variant="body2" sx={{ mb: 1 }}> TRIX &lt; 0 하락 추세</Typography>
<Typography variant="body2" sx={{ color: 'success.main' }}> 0 돌파 = 추세 전환 신호</Typography>
</Box>
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1 }}>🔸 골든크로스 / 데드크로스</Typography>
<Typography variant="body2" sx={{ pl: 2, mb: 1, fontSize: '0.85rem' }}>(시그널 사용 )</Typography>
<Box sx={{ pl: 2, mb: 2 }}>
<Typography variant="body2" sx={{ color: 'success.main', fontWeight: 600, mb: 1 }}> 매수 신호</Typography>
<Typography variant="body2" sx={{ pl: 2, fontSize: '0.85rem', mb: 1 }}>
TRIX가 시그널을 아래에서 위로 돌파<br />
0 근처에서 발생 신뢰도
</Typography>
<Typography variant="body2" sx={{ color: 'error.main', fontWeight: 600, mb: 1 }}> 매도 신호</Typography>
<Typography variant="body2" sx={{ pl: 2, fontSize: '0.85rem' }}>
TRIX가 시그널을 위에서 아래로 이탈
</Typography>
</Box>
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1 }}>🔸 기울기(각도) 해석</Typography>
<Box sx={{ pl: 2, mb: 2 }}>
<Typography variant="body2" sx={{ mb: 1 }}> 기울기 가팔라짐 추세 가속</Typography>
<Typography variant="body2" sx={{ mb: 1 }}> 기울기 둔화 추세 약화</Typography>
<Typography variant="body2" sx={{ color: 'warning.main' }}>
📌 히스토그램처럼 힘의 변화를 직관적으로 파악 가능
</Typography>
</Box>
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1 }}>🔸 다이버전스</Typography>
<Box sx={{ pl: 2 }}>
<Typography variant="body2" sx={{ color: 'success.main', fontWeight: 600, mb: 1 }}> 강세 다이버전스</Typography>
<Typography variant="body2" sx={{ pl: 2, fontSize: '0.85rem', mb: 1 }}>
주가 저점 하락<br />
TRIX 저점 상승<br />
반등 가능성
</Typography>
<Typography variant="body2" sx={{ color: 'error.main', fontWeight: 600, mb: 1 }}> 약세 다이버전스</Typography>
<Typography variant="body2" sx={{ pl: 2, fontSize: '0.85rem' }}>
주가 고점 상승<br />
TRIX 고점 하락<br />
하락 가능성
</Typography>
</Box>
</Box>
<Typography variant="h6" sx={{ fontWeight: 600, mb: 2 }}>5️⃣ MACD와의 차이</Typography>
<Box sx={{ pl: 2, mb: 3 }}>
<Box sx={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 1, mb: 2 }}>
<Typography variant="body2" sx={{ fontWeight: 600 }}>구분</Typography>
<Typography variant="body2" sx={{ fontWeight: 600 }}>TRIX</Typography>
<Typography variant="body2" sx={{ fontWeight: 600 }}>MACD</Typography>
<Typography variant="body2">이동평균</Typography>
<Typography variant="body2">3 EMA</Typography>
<Typography variant="body2">2 EMA</Typography>
<Typography variant="body2">반응 속도</Typography>
<Typography variant="body2">느림</Typography>
<Typography variant="body2">빠름</Typography>
<Typography variant="body2">노이즈</Typography>
<Typography variant="body2">매우 적음</Typography>
<Typography variant="body2">중간</Typography>
<Typography variant="body2">적합 매매</Typography>
<Typography variant="body2">스윙·중기</Typography>
<Typography variant="body2">단타~중기</Typography>
</Box>
<Typography variant="body2" sx={{ color: 'info.main' }}>
👉 TRIX = 깔끔한 추세 판단용
</Typography>
</Box>
<Typography variant="h6" sx={{ fontWeight: 600, mb: 2 }}>6️⃣ 실전 활용 </Typography>
<Box sx={{ pl: 2, mb: 3 }}>
<Typography variant="body2" sx={{ mb: 1 }}> 횡보장보다는 추세장에서 강력</Typography>
<Typography variant="body2" sx={{ pl: 2, mb: 1, fontSize: '0.85rem', color: 'error.main' }}>
짧은 분봉에서는 신호 지연
</Typography>
<Typography variant="body2" sx={{ mb: 1 }}> 이동평균선과 궁합 좋음</Typography>
<Typography variant="body2" sx={{ pl: 2, mb: 1, fontSize: '0.85rem' }}>
MA 정배열 + TRIX 0 상방 = 상승 신뢰도
</Typography>
<Typography variant="body2" sx={{ mb: 1 }}> 단타에서는 기간 줄여 사용 가능</Typography>
<Typography variant="body2" sx={{ pl: 2, fontSize: '0.85rem' }}>
: TRIX(7, 5)
</Typography>
</Box>
<Typography variant="h6" sx={{ fontWeight: 600, mb: 2 }}>7️⃣ TRIX의 한계</Typography>
<Box sx={{ pl: 2, mb: 3 }}>
<Typography variant="body2" sx={{ mb: 1 }}> 후행성 강함</Typography>
<Typography variant="body2" sx={{ mb: 2 }}> 급등락 초기 신호 포착 어려움</Typography>
<Typography variant="body2" sx={{ color: 'warning.main', fontSize: '0.85rem', mb: 1 }}>
📌 진입 타점은 RSI·CCI,
</Typography>
<Typography variant="body2" sx={{ color: 'warning.main', fontSize: '0.85rem' }}>
📌 방향 확인은 TRIX로 분리해서 쓰면 효과적
</Typography>
</Box>
<Typography variant="h6" sx={{ fontWeight: 600, mb: 2 }}> 요약</Typography>
<Typography variant="body2" sx={{ fontStyle: 'italic', color: 'primary.main' }}>
TRIX는 3 EMA 기반의 추세 지표로, 0 돌파와 기울기 변화가 핵심 포인트입니다.
</Typography>
</Box>
);
case 'disparity':
return (
<Box>
<Typography variant="h6" sx={{ fontWeight: 600, mb: 2 }}>이격도(乖離度, Disparity Index)</Typography>
<Typography variant="body2" paragraph>
📊 현재 주가가 이동평균선에서 얼마나 떨어져 있는지를 % 나타낸 아주 직관적인 과열·과매도 보조지표입니다.
</Typography>
<Typography variant="h6" sx={{ fontWeight: 600, mb: 2 }}>1️⃣ 이격도란?</Typography>
<Typography variant="body2" paragraph>
이격도는 아래 질문에 답하는 지표입니다.
</Typography>
<Typography variant="body2" sx={{ fontStyle: 'italic', color: 'primary.main', mb: 2 }}>
"지금 주가는 평균 가격(이동평균)보다 얼마나 위(과열) 또는 아래(과매도)에 있는가?"
</Typography>
<Box sx={{ pl: 2, mb: 3 }}>
<Typography variant="body2"> 100% 이동평균과 동일</Typography>
<Typography variant="body2"> 100% 초과 평균보다 비쌈</Typography>
<Typography variant="body2"> 100% 미만 평균보다 </Typography>
</Box>
<Typography variant="h6" sx={{ fontWeight: 600, mb: 2 }}>2️⃣ 이격도 계산식</Typography>
<Typography variant="body2" sx={{ mb: 1 }}>개념만 이해하시면 충분합니다.</Typography>
<Typography variant="body2" sx={{ fontWeight: 600, mb: 2 }}>
이격도(%) = (현재가 ÷ 이동평균가) × 100
</Typography>
<Box sx={{ pl: 2, mb: 3 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1 }}>)</Typography>
<Typography variant="body2" sx={{ fontSize: '0.85rem' }}> 현재가 105, 20일선 100 이격도 105%</Typography>
<Typography variant="body2" sx={{ fontSize: '0.85rem' }}> 현재가 95, 20일선 100 이격도 95%</Typography>
</Box>
<Typography variant="h6" sx={{ fontWeight: 600, mb: 2 }}>3️⃣ 기준 이동평균선 선택</Typography>
<Typography variant="body2" sx={{ mb: 1 }}>이격도는 어떤 이동평균을 쓰느냐가 매우 중요합니다.</Typography>
<Box sx={{ pl: 2, mb: 3 }}>
<Box sx={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 1, mb: 2 }}>
<Typography variant="body2" sx={{ fontWeight: 600 }}>이동평균</Typography>
<Typography variant="body2" sx={{ fontWeight: 600 }}>활용</Typography>
<Typography variant="body2">5</Typography>
<Typography variant="body2">초단기 과열</Typography>
<Typography variant="body2">20</Typography>
<Typography variant="body2">단기 매매(가장 많이 사용)</Typography>
<Typography variant="body2">60</Typography>
<Typography variant="body2">중기 추세</Typography>
<Typography variant="body2">120</Typography>
<Typography variant="body2">장기 과열</Typography>
</Box>
<Typography variant="body2" sx={{ color: 'warning.main' }}>📌 20 이격도가 가장 보편적</Typography>
</Box>
<Typography variant="h6" sx={{ fontWeight: 600, mb: 2 }}>4️⃣ 핵심 해석 방법</Typography>
<Box sx={{ mb: 3 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1 }}>🔹 기준값 (과열/과매도)</Typography>
<Typography variant="body2" sx={{ pl: 2, mb: 1 }}>종목·시장마다 다르지만 일반적인 기준은 다음과 같습니다.</Typography>
<Box sx={{ pl: 2, mb: 2 }}>
<Typography variant="body2" sx={{ color: 'error.main', fontWeight: 600, mb: 1 }}>📈 상승 과열</Typography>
<Typography variant="body2" sx={{ pl: 2, fontSize: '0.85rem', mb: 1 }}>
이격도 105~110% 이상<br />
단기 과열, 조정 가능성
</Typography>
<Typography variant="body2" sx={{ color: 'success.main', fontWeight: 600, mb: 1 }}>📉 하락 과매도</Typography>
<Typography variant="body2" sx={{ pl: 2, fontSize: '0.85rem', mb: 1 }}>
이격도 90~95% 이하<br />
과매도, 반등 가능성
</Typography>
<Typography variant="body2" sx={{ color: 'warning.main', fontWeight: 600, mb: 1 }}>⚠️ 기준값은 고정이 아님</Typography>
<Typography variant="body2" sx={{ pl: 2, fontSize: '0.85rem' }}>
변동성 종목 기준 확대<br />
우량주 기준 축소
</Typography>
</Box>
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1 }}>🔹 평균 회귀 (Mean Reversion)</Typography>
<Typography variant="body2" sx={{ pl: 2, mb: 1 }}>이격도의 핵심 개념입니다.</Typography>
<Box sx={{ pl: 2, mb: 2 }}>
<Typography variant="body2" sx={{ fontSize: '0.85rem', mb: 1 }}>
주가는 결국 이동평균으로 되돌아가려는 성질이 있음
</Typography>
<Typography variant="body2" sx={{ fontSize: '0.85rem', mb: 1 }}>
이격 ↑↑ 조정 평균 회귀
</Typography>
<Typography variant="body2" sx={{ fontSize: '0.85rem' }}>
이격 ↓↓ 반등 평균 회귀
</Typography>
</Box>
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1 }}>🔹 추세와 함께 사용</Typography>
<Box sx={{ pl: 2 }}>
<Typography variant="body2" sx={{ fontSize: '0.85rem', mb: 1 }}>
상승 추세 + 이격 과열 눌림목 대기
</Typography>
<Typography variant="body2" sx={{ fontSize: '0.85rem', mb: 1 }}>
하락 추세 + 과매도 기술적 반등 주의
</Typography>
<Typography variant="body2" sx={{ color: 'warning.main' }}>📌 추세 무시하고 역매매 </Typography>
</Box>
</Box>
<Typography variant="h6" sx={{ fontWeight: 600, mb: 2 }}>5️⃣ 실전 활용 예시</Typography>
<Box sx={{ pl: 2, mb: 3 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1 }}>📌 매수 타점</Typography>
<Typography variant="body2" sx={{ pl: 2, mb: 2, fontSize: '0.85rem' }}>
상승 추세 유지<br />
조정으로 이격도 95% 내외<br />
지지 확인 분할 매수
</Typography>
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1 }}>📌 매도 타점</Typography>
<Typography variant="body2" sx={{ pl: 2, fontSize: '0.85rem' }}>
급등 이격도 110% 이상<br />
거래량 감소<br />
분할 매도
</Typography>
</Box>
<Typography variant="h6" sx={{ fontWeight: 600, mb: 2 }}>6️⃣ RSI·CCI와 차이점</Typography>
<Box sx={{ pl: 2, mb: 3 }}>
<Box sx={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr 1fr', gap: 1, mb: 2 }}>
<Typography variant="body2" sx={{ fontWeight: 600 }}>구분</Typography>
<Typography variant="body2" sx={{ fontWeight: 600 }}>이격도</Typography>
<Typography variant="body2" sx={{ fontWeight: 600 }}>RSI</Typography>
<Typography variant="body2" sx={{ fontWeight: 600 }}>CCI</Typography>
<Typography variant="body2">기준</Typography>
<Typography variant="body2">이동평균</Typography>
<Typography variant="body2">30·70</Typography>
<Typography variant="body2">±100</Typography>
<Typography variant="body2">직관성</Typography>
<Typography variant="body2">매우 높음</Typography>
<Typography variant="body2">중간</Typography>
<Typography variant="body2">중간</Typography>
<Typography variant="body2">추세 판단</Typography>
<Typography variant="body2">약함</Typography>
<Typography variant="body2">보통</Typography>
<Typography variant="body2">보통</Typography>
<Typography variant="body2">평균회귀</Typography>
<Typography variant="body2">★★★★★</Typography>
<Typography variant="body2">★★★</Typography>
<Typography variant="body2">★★★</Typography>
</Box>
<Typography variant="body2" sx={{ color: 'info.main' }}>
👉 이격도 = 평균에서 얼마나 벗어났는지 한눈에 파악
</Typography>
</Box>
<Typography variant="h6" sx={{ fontWeight: 600, mb: 2 }}>7️⃣ 이격도의 장점과 한계</Typography>
<Box sx={{ pl: 2, mb: 3 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1 }}> 장점</Typography>
<Typography variant="body2" sx={{ pl: 2, mb: 2, fontSize: '0.85rem' }}>
계산·해석 쉬움<br />
과열/과매도 직관적<br />
초보자도 사용 가능
</Typography>
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1 }}> 한계</Typography>
<Typography variant="body2" sx={{ pl: 2, mb: 2, fontSize: '0.85rem' }}>
강한 추세장에서는 과열 지속<br />
단독 사용 역추세 매매 위험
</Typography>
<Typography variant="body2" sx={{ color: 'warning.main' }}>
📌 이동평균·거래량·추세 지표와 병행 필수
</Typography>
</Box>
<Typography variant="h6" sx={{ fontWeight: 600, mb: 2 }}> 요약</Typography>
<Typography variant="body2" sx={{ fontStyle: 'italic', color: 'primary.main' }}>
이격도는 주가가 이동평균에서 얼마나 떨어졌는지를 % 보여주는 지표로, 평균 회귀 관점에서 과열·과매도를 판단합니다.
</Typography>
</Box>
);
case 'newPsychological':
return (
<Box>
<Typography variant="h6" sx={{ fontWeight: 600, mb: 2 }}>신심리도(心理度, Psychological Line)</Typography>
<Typography variant="body2" paragraph>
📊 투자자들의 '심리 상태' 수치로 표현한 주식 보조지표입니다.
</Typography>
<Typography variant="body2" sx={{ fontStyle: 'italic', color: 'primary.main', mb: 3 }}>
"요즘 시장이 너무 낙관적인가, 너무 비관적인가?" 판단할 사용합니다.
</Typography>
<Typography variant="h6" sx={{ fontWeight: 600, mb: 2 }}>1️⃣ 신심리도란?</Typography>
<Typography variant="body2" paragraph>
신심리도는 일정 기간 동안
</Typography>
<Typography variant="body2" sx={{ fontStyle: 'italic', color: 'primary.main', mb: 2 }}>
주가가 오른 날의 비율
</Typography>
<Typography variant="body2" paragraph>
계산한 지표입니다.
</Typography>
<Box sx={{ pl: 2, mb: 2 }}>
<Typography variant="body2" sx={{ mb: 1 }}> 오르는 날이 많을수록 낙관(과열)</Typography>
<Typography variant="body2"> 내리는 날이 많을수록 비관(침체)</Typography>
</Box>
<Typography variant="body2" sx={{ color: 'warning.main', mb: 3 }}>
📌 가격 크기보다 '올랐는지/내렸는지' 본다는 특징입니다.
</Typography>
<Typography variant="h6" sx={{ fontWeight: 600, mb: 2 }}>2️⃣ 신심리도 계산 방식 (개념)</Typography>
<Typography variant="body2" sx={{ fontStyle: 'italic', color: 'primary.main', mb: 2 }}>
신심리도 = (상승한 ÷ 전체 기간) × 100
</Typography>
<Box sx={{ pl: 2, mb: 3 }}>
<Typography variant="body2" sx={{ fontSize: '0.9rem', mb: 1 }}>)</Typography>
<Typography variant="body2" sx={{ pl: 2, fontSize: '0.85rem', mb: 1 }}>
12 9 상승 신심리도 = 75
</Typography>
<Typography variant="body2" sx={{ pl: 2, fontSize: '0.85rem' }}>
12 3 상승 신심리도 = 25
</Typography>
</Box>
<Typography variant="h6" sx={{ fontWeight: 600, mb: 2 }}>3️⃣ 기본 설정</Typography>
<Box sx={{ pl: 2, mb: 3 }}>
<Typography variant="body2" sx={{ mb: 1 }}> 기간: 12일 (가장 많이 사용)</Typography>
<Typography variant="body2"> 범위: 0 ~ 100</Typography>
</Box>
<Typography variant="h6" sx={{ fontWeight: 600, mb: 2 }}>4️⃣ 핵심 해석 방법</Typography>
<Box sx={{ mb: 3 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1 }}>🔹 과매수 / 과매도 구간</Typography>
<Typography variant="body2" sx={{ pl: 2, mb: 1 }}>가장 중요합니다.</Typography>
<Box sx={{ pl: 2, mb: 2 }}>
<Typography variant="body2" sx={{ mb: 1 }}> 75 이상 과매수 (낙관 과도)</Typography>
<Typography variant="body2" sx={{ mb: 1 }}> 25 이하 과매도 (비관 과도)</Typography>
<Typography variant="body2" sx={{ color: 'error.main', mb: 1 }}>⚠️ 도달 즉시 매매 </Typography>
<Typography variant="body2" sx={{ color: 'success.main' }}> 되돌림(구간 이탈) 확인</Typography>
</Box>
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1 }}>🔹 평균 회귀 관점</Typography>
<Typography variant="body2" sx={{ pl: 2, mb: 1 }}>신심리도는 역추세 지표 성격이 강합니다.</Typography>
<Box sx={{ pl: 2, mb: 2 }}>
<Typography variant="body2" sx={{ mb: 1 }}> 과도한 낙관 조정 가능성</Typography>
<Typography variant="body2" sx={{ mb: 1 }}> 과도한 비관 반등 가능성</Typography>
<Typography variant="body2" sx={{ color: 'warning.main' }}>📌 횡보장·조정장에서 효과적</Typography>
</Box>
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1 }}>🔹 추세 확인 보조</Typography>
<Box sx={{ pl: 2 }}>
<Typography variant="body2" sx={{ mb: 1 }}> 50 이상 상승 심리 우위</Typography>
<Typography variant="body2"> 50 이하 하락 심리 우위</Typography>
</Box>
</Box>
<Typography variant="h6" sx={{ fontWeight: 600, mb: 2 }}>5️⃣ RSI·이격도와의 차이</Typography>
<Box sx={{ pl: 2, mb: 3 }}>
<Typography variant="body2" sx={{ fontWeight: 600, mb: 1 }}>구분 | 신심리도 | RSI | 이격도</Typography>
<Typography variant="body2" sx={{ fontSize: '0.85rem', mb: 1 }}> 기준: 상승일 비율 | 상승·하락 강도 | 평균 대비 거리</Typography>
<Typography variant="body2" sx={{ fontSize: '0.85rem', mb: 1 }}> 가격 크기 반영: | | </Typography>
<Typography variant="body2" sx={{ fontSize: '0.85rem', mb: 1 }}> 성격: 심리 | 모멘텀 | 평균회귀</Typography>
<Typography variant="body2" sx={{ fontSize: '0.85rem', mb: 2 }}> 역추세: ★★★★☆ | ★★★ | ★★★★★</Typography>
<Typography variant="body2" sx={{ color: 'info.main' }}>
👉 신심리도는 "분위기 과열" 보는 지표
</Typography>
</Box>
<Typography variant="h6" sx={{ fontWeight: 600, mb: 2 }}>6️⃣ 실전 활용 </Typography>
<Box sx={{ pl: 2, mb: 3 }}>
<Typography variant="body2" sx={{ color: 'success.main', mb: 1 }}> 단독 사용보다는 보조용</Typography>
<Typography variant="body2" sx={{ pl: 2, mb: 2, fontSize: '0.85rem' }}>RSI, CCI와 함께 쓰면 신뢰도 </Typography>
<Typography variant="body2" sx={{ color: 'success.main', mb: 1 }}> 강한 추세장에서는 신호 무력</Typography>
<Typography variant="body2" sx={{ pl: 2, mb: 2, fontSize: '0.85rem' }}>상승장에서는 75 이상 유지 가능</Typography>
<Typography variant="body2" sx={{ color: 'success.main', mb: 1 }}> 기간 조절</Typography>
<Box sx={{ pl: 2, fontSize: '0.85rem' }}>
<Typography variant="body2" sx={{ mb: 1 }}> 단타: 9</Typography>
<Typography variant="body2" sx={{ mb: 1 }}> 기본: 12</Typography>
<Typography variant="body2"> 보수적: 20</Typography>
</Box>
</Box>
<Typography variant="h6" sx={{ fontWeight: 600, mb: 2 }}>7️⃣ 신심리도의 장점과 한계</Typography>
<Box sx={{ pl: 2, mb: 3 }}>
<Typography variant="body2" sx={{ color: 'success.main', mb: 1 }}> 장점</Typography>
<Box sx={{ pl: 2, mb: 2, fontSize: '0.85rem' }}>
<Typography variant="body2" sx={{ mb: 1 }}> 계산·해석 매우 쉬움</Typography>
<Typography variant="body2"> 투자심리 파악에 직관적</Typography>
</Box>
<Typography variant="body2" sx={{ color: 'error.main', mb: 1 }}> 한계</Typography>
<Box sx={{ pl: 2, mb: 2, fontSize: '0.85rem' }}>
<Typography variant="body2" sx={{ mb: 1 }}> 가격 변동폭 반영 </Typography>
<Typography variant="body2"> 추세 추종 능력 약함</Typography>
</Box>
<Typography variant="body2" sx={{ color: 'warning.main' }}>
📌 반드시 추세 지표와 병행
</Typography>
</Box>
<Typography variant="h6" sx={{ fontWeight: 600, mb: 2 }}> 요약</Typography>
<Typography variant="body2" sx={{ fontStyle: 'italic', color: 'primary.main' }}>
신심리도는 상승한 날의 비율로 투자자 심리를 수치화한 지표로, 25·75 구간 이탈이 핵심 매매 신호입니다.
</Typography>
</Box>
);
default:
return (
<Box>
<Typography variant="body2" color="text.secondary">
{indicatorInfo[type]?.name} 지표에 대한 상세 설명은 준비 중입니다.
</Typography>
</Box>
);
}
};
const IndicatorSettingsPopup: React.FC<IndicatorSettingsPopupProps> = ({
open,
onClose,
indicatorType,
}) => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
const { colors, updateColors } = useChartColors();
const {
indicatorSettings,
updateIndicatorSettings,
saveIndicatorSettings,
} = useIndicatorSettings();
// 로컬 상태
const [localColors, setLocalColors] = useState<Partial<ChartColorSettings>>({});
const [localSettings, setLocalSettings] = useState<Partial<IndicatorSettings>>({});
const [showHelpDialog, setShowHelpDialog] = useState(false);
// ✅ 색상 input ref 추적 (리렌더링 방지를 위해)
const colorInputRefsRef = useRef<Map<string, HTMLInputElement>>(new Map());
// 팝업 위치 상태 (드래그 시 유지)
const [dialogPosition, setDialogPosition] = useState<{ x: number; y: number }>({ x: 0, y: 0 });
// 다이얼로그가 열릴 때 현재 값으로 초기화 (open/indicatorType 변경 시에만)
useEffect(() => {
if (open && indicatorType) {
setLocalColors({ ...colors });
setLocalSettings({ ...indicatorSettings });
}
// 다이얼로그가 닫힐 때 위치 초기화
if (!open) {
setDialogPosition({ x: 0, y: 0 });
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [open, indicatorType]);
if (!indicatorType) return null;
const info = indicatorInfo[indicatorType];
const handleColorChange = (key: keyof ChartColorSettings, value: string | number) => {
console.log(`🎨 [IndicatorSettingsPopup] handleColorChange - key: ${key}, value: ${value}`);
// ✅ 로컬 상태만 업데이트 (적용 버튼 클릭 시 Context에 반영)
setLocalColors(prev => {
const newColors = { ...prev, [key]: value };
console.log(`✅ [IndicatorSettingsPopup] localColors 업데이트:`, newColors);
return newColors;
});
};
const handleSettingChange = (key: keyof IndicatorSettings, value: number | boolean) => {
// 로컬 상태만 업데이트 (적용 버튼 클릭 시 Context에 반영)
setLocalSettings(prev => ({ ...prev, [key]: value }));
};
const handleSave = () => {
// 모든 색상 input의 현재 값을 읽어서 localColors에 반영
const finalColors: Partial<ChartColorSettings> = { ...localColors };
colorInputRefsRef.current.forEach((input, key) => {
if (input && input.value) {
(finalColors as any)[key] = input.value;
}
});
// 색상 업데이트
updateColors({ ...colors, ...finalColors });
// 설정값이 실제로 변경되었는지 확인
const settingsChanged = Object.keys(localSettings).some((key) => {
const localValue = (localSettings as any)[key];
const currentValue = (indicatorSettings as any)[key];
return localValue !== currentValue;
});
// 설정값이 변경된 경우 Context 업데이트 (settingsVersion 증가) 및 DB 저장
if (settingsChanged) {
console.log('🔄 [IndicatorSettingsPopup] 설정 변경 감지 - updateIndicatorSettings 호출:', localSettings);
// ✅ updateIndicatorSettings 호출 (settingsVersion 증가 → 차트 재계산 트리거)
updateIndicatorSettings(localSettings);
// ✅ DB 저장
saveIndicatorSettings({ ...indicatorSettings, ...localSettings });
}
onClose();
};
// 파라미터 테이블 컴포넌트
const ParamTable = ({
params,
}: {
params: { label: string; paramKey: keyof IndicatorSettings; min?: number; max?: number }[];
}) => {
// min/max 값을 기준으로 선택 옵션 생성 함수
const generateOptions = (min: number, max: number, step: number = 1) => {
const options: number[] = [];
for (let i = min; i <= max; i += step) {
options.push(i);
}
return options;
};
return (
<Box sx={{ mb: 2 }}>
<Typography variant="caption" fontWeight="bold" color="text.secondary" sx={{ display: 'block', mb: 1 }}>
📊 파라미터
</Typography>
{/* 헤더 행 */}
<Box sx={{ p: 1, mb: 0.5, bgcolor: 'primary.main', borderRadius: 1, opacity: 0.85 }}>
<Grid container spacing={1} alignItems="center">
{params.map((param) => (
<Grid item xs={12 / params.length} key={`header-${param.paramKey}`}>
<Typography variant="caption" fontWeight="bold" color="white" textAlign="center" display="block">
{param.label}
</Typography>
</Grid>
))}
</Grid>
</Box>
{/* 값 행 - 드롭다운으로 변경 */}
<Box sx={{ p: 1, bgcolor: 'background.paper', borderRadius: 1, border: '1px solid', borderColor: 'divider' }}>
<Grid container spacing={1} alignItems="center">
{params.map((param) => {
const min = param.min ?? 1;
const max = param.max ?? 100;
const currentValue = Number(localSettings[param.paramKey] ?? indicatorSettings[param.paramKey] ?? min);
const options = generateOptions(min, max);
return (
<Grid item xs={12 / params.length} key={param.paramKey}>
<Select
size="small"
fullWidth
value={currentValue}
onChange={(e) => {
const newValue = Number(e.target.value);
handleSettingChange(param.paramKey, newValue);
console.log(`🔄 [ParamTable] ${param.label} - 선택: ${newValue}`);
}}
sx={{
fontSize: '12px',
height: 32,
'& .MuiSelect-select': {
py: 0.5,
textAlign: 'center'
},
'& .MuiOutlinedInput-root': { bgcolor: 'background.paper' }
}}
>
{options.map((value) => (
<MenuItem key={value} value={value} sx={{ fontSize: '12px' }}>
{value}
</MenuItem>
))}
</Select>
</Grid>
);
})}
</Grid>
</Box>
</Box>
);
};
// 색상 및 굵기 테이블 컴포넌트
const ColorTable = ({
configs,
}: {
configs: {
label: string;
colorKey?: keyof ChartColorSettings;
widthKey?: keyof ChartColorSettings;
lineStyleKey?: keyof ChartColorSettings;
valueKey?: keyof IndicatorSettings;
valueMin?: number;
valueMax?: number;
valueStep?: number;
enableKey?: keyof IndicatorSettings;
disableToggle?: boolean;
}[];
}) => {
const hasValueColumn = configs.some(cfg => cfg.valueKey !== undefined);
const hasEnableColumn = configs.some(cfg => cfg.enableKey !== undefined);
const hasWidthColumn = configs.some(cfg => cfg.widthKey !== undefined);
const hasLineStyleColumn = configs.some(cfg => cfg.lineStyleKey !== undefined);
// 기준값 입력 필드의 임시 값 관리 (입력 중 포커스 유지를 위한 로컬 상태)
const [inputValues, setInputValues] = React.useState<Record<string, string>>({});
// 타이틀 텍스트 생성
const getTitleText = () => {
const parts = ['🎨 색상'];
if (hasWidthColumn) parts.push('선 굵기');
if (hasLineStyleColumn) parts.push('선 유형');
return parts.join(', ');
};
return (
<Box sx={{ mb: 0.5 }}>
<Typography variant="caption" fontWeight="bold" color="text.secondary" sx={{ display: 'block', mb: 1, fontSize: '12px' }}>
{getTitleText()}
</Typography>
{/* 헤더 행 */}
<Box sx={{
p: 1,
mb: 0.5,
bgcolor: (theme) => theme.palette.mode === 'dark' ? '#616161' : '#9e9e9e',
borderRadius: 1,
}}>
<Grid container spacing={1.5} alignItems="center">
{hasEnableColumn && (
<Grid item xs={hasValueColumn ? 0.8 : 1}>
<Typography variant="caption" fontWeight="bold" color="white">ON/OFF</Typography>
</Grid>
)}
<Grid item xs={hasEnableColumn ? (hasValueColumn ? 1.5 : 2) : (hasValueColumn ? 1.8 : (!hasWidthColumn && !hasLineStyleColumn ? 4 : 2.5))}>
<Typography variant="caption" fontWeight="bold" color="white">항목</Typography>
</Grid>
{hasValueColumn && (
<Grid item xs={1.5}>
<Typography variant="caption" fontWeight="bold" color="white">기준값</Typography>
</Grid>
)}
<Grid item xs={hasEnableColumn ? (hasValueColumn ? 1 : 1.5) : (hasValueColumn ? 1.2 : (!hasWidthColumn && !hasLineStyleColumn ? 4 : 1.5))}>
<Typography variant="caption" fontWeight="bold" color="white">색상</Typography>
</Grid>
{hasWidthColumn && (
<Grid item xs={hasEnableColumn ? (hasValueColumn ? 2.5 : 2.5) : (hasValueColumn ? 3 : 3)}>
<Typography variant="caption" fontWeight="bold" color="white">굵기</Typography>
</Grid>
)}
{hasLineStyleColumn && (
<Grid item xs={hasEnableColumn ? (hasValueColumn ? 2 : 2.5) : (hasValueColumn ? 2.5 : 2.5)}>
<Typography variant="caption" fontWeight="bold" color="white"> 유형</Typography>
</Grid>
)}
<Grid item xs={hasEnableColumn ? (hasValueColumn ? (hasWidthColumn && hasLineStyleColumn ? 2.7 : 4) : (hasWidthColumn && hasLineStyleColumn ? 2.5 : 4)) : (hasValueColumn ? (hasWidthColumn && hasLineStyleColumn ? 3.5 : 4) : (hasWidthColumn && hasLineStyleColumn ? 2.5 : 4))}>
<Typography variant="caption" fontWeight="bold" color="white">미리보기</Typography>
</Grid>
</Grid>
</Box>
{/* 각 색상 설정 행 */}
{configs.map((cfg, index) => {
const colorValue = cfg.colorKey ? String(localColors[cfg.colorKey] || colors[cfg.colorKey] || '#666666') : '#666666';
const widthValue = cfg.widthKey ? Number(localColors[cfg.widthKey] ?? colors[cfg.widthKey] ?? 1.5) : 1.5;
const lineStyleValue = cfg.lineStyleKey ? String(localColors[cfg.lineStyleKey] ?? colors[cfg.lineStyleKey] ?? 'solid') : 'solid';
const paramValue = cfg.valueKey ? Number(localSettings[cfg.valueKey] ?? indicatorSettings[cfg.valueKey] ?? 0) : 0;
const enabledValue = cfg.enableKey ? Boolean(localSettings[cfg.enableKey] ?? indicatorSettings[cfg.enableKey] ?? false) : true;
return (
<Box key={cfg.colorKey || `param-${index}`} sx={{ p: 1, mb: 0.5, bgcolor: 'background.paper', borderRadius: 1, border: '1px solid', borderColor: 'divider' }}>
<Grid container spacing={1} alignItems="center">
{/* ON/OFF 토글 */}
{hasEnableColumn && (
<Grid item xs={hasValueColumn ? 0.8 : 1} sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
{cfg.enableKey ? (
<Switch
checked={enabledValue}
onChange={(e) => handleSettingChange(cfg.enableKey!, e.target.checked)}
disabled={cfg.disableToggle === true}
size="small"
/>
) : (
<Typography variant="caption" color="text.disabled">-</Typography>
)}
</Grid>
)}
{/* 항목 이름 */}
<Grid item xs={hasEnableColumn ? (hasValueColumn ? 1.5 : 2) : (hasValueColumn ? 1.8 : 2.5)} sx={{ display: 'flex', alignItems: 'center' }}>
<Typography variant="body2" sx={{ fontSize: '12px', fontWeight: 'medium' }}>
{cfg.label}
</Typography>
</Grid>
{/* 기준값 선택 (valueKey가 있을 때만 표시) */}
{hasValueColumn && (
<Grid item xs={1.5} sx={{ display: 'flex', alignItems: 'center' }}>
{cfg.valueKey ? (
<Autocomplete
id={`input-${cfg.valueKey}`}
value={paramValue}
onChange={(event, newValue) => {
// 드롭다운에서 선택했을 때
if (newValue !== null) {
const numValue = typeof newValue === 'number' ? newValue : Number(newValue);
if (!isNaN(numValue)) {
const min = cfg.valueMin ?? 0;
const max = cfg.valueMax ?? 100;
// 기준값(Period, Length 등)은 양수만 허용
const isPeriodValue = cfg.valueKey && (
cfg.valueKey.toLowerCase().includes('period') ||
cfg.valueKey.toLowerCase().includes('length') ||
cfg.valueKey.toLowerCase().includes('slowing')
);
if (numValue >= min && numValue <= max) {
// 기준값인 경우 양수 확인
if (isPeriodValue && numValue < 0) {
return;
}
handleSettingChange(cfg.valueKey!, numValue);
setInputValues(prev => ({ ...prev, [cfg.valueKey!]: String(numValue) }));
console.log(`🔄 [IndicatorSettingsPopup] ${cfg.label} - 선택: ${numValue}`);
}
}
}
}}
inputValue={inputValues[cfg.valueKey] ?? String(paramValue)}
onInputChange={(event, newInputValue, reason) => {
// 입력값이 변경될 때는 로컬 상태만 업데이트 (필터링을 위해)
if (reason === 'input') {
// 기준값(Period, Length 등)은 양수만, 기준선(Overbought, Oversold 등)은 음수도 허용
const isPeriodValue = cfg.valueKey && (
cfg.valueKey.toLowerCase().includes('period') ||
cfg.valueKey.toLowerCase().includes('length') ||
cfg.valueKey.toLowerCase().includes('slowing')
);
const numberPattern = isPeriodValue ? /^\d*\.?\d*$/ : /^-?\d*\.?\d*$/;
// 빈 문자열이거나 숫자만 허용
if (newInputValue === '' || numberPattern.test(newInputValue)) {
setInputValues(prev => ({ ...prev, [cfg.valueKey!]: newInputValue }));
}
} else if (reason === 'reset') {
// 리셋 시에는 현재 paramValue로 복원
setInputValues(prev => ({ ...prev, [cfg.valueKey!]: String(paramValue) }));
} else if (reason === 'clear') {
// 클리어 시 빈 문자열
setInputValues(prev => ({ ...prev, [cfg.valueKey!]: '' }));
}
}}
options={(() => {
const min = cfg.valueMin ?? 0;
const max = cfg.valueMax ?? 100;
const step = cfg.valueStep ?? 1;
const values: number[] = [];
for (let i = min; i <= max; i += step) {
values.push(i);
}
return values;
})()}
getOptionLabel={(option) => String(option)}
filterOptions={(options, state) => {
// 입력값으로 필터링
const inputValue = state.inputValue.toLowerCase();
if (!inputValue) return options;
return options.filter(option =>
String(option).toLowerCase().includes(inputValue)
);
}}
freeSolo
size="small"
fullWidth
disableClearable
renderInput={(params) => (
<TextField
{...params}
inputProps={{
...params.inputProps,
style: { textAlign: 'center', fontSize: '12px' }
}}
onBlur={(e) => {
// 포커스 아웃 시 실제 값 업데이트
const inputStr = e.target.value.trim();
if (inputStr === '') {
// 빈 값이면 원래 값으로 복원
setInputValues(prev => ({ ...prev, [cfg.valueKey!]: String(paramValue) }));
return;
}
const numValue = Number(inputStr);
const min = cfg.valueMin ?? 0;
const max = cfg.valueMax ?? 100;
// 기준값(Period, Length 등)은 양수만 허용
const isPeriodValue = cfg.valueKey && (
cfg.valueKey.toLowerCase().includes('period') ||
cfg.valueKey.toLowerCase().includes('length') ||
cfg.valueKey.toLowerCase().includes('slowing')
);
if (!isNaN(numValue) && numValue >= min && numValue <= max) {
// 기준값인 경우 양수 확인
if (isPeriodValue && numValue < 0) {
setInputValues(prev => ({ ...prev, [cfg.valueKey!]: String(paramValue) }));
return;
}
handleSettingChange(cfg.valueKey!, numValue);
setInputValues(prev => ({ ...prev, [cfg.valueKey!]: String(numValue) }));
console.log(`🔄 [IndicatorSettingsPopup] ${cfg.label} - 입력 완료: ${numValue}`);
} else {
// 유효하지 않은 값이면 원래 값으로 복원
setInputValues(prev => ({ ...prev, [cfg.valueKey!]: String(paramValue) }));
}
}}
onKeyDown={(e) => {
// 엔터 키 입력 시 실제 값 업데이트
if (e.key === 'Enter') {
const inputStr = (e.target as HTMLInputElement).value.trim();
if (inputStr === '') {
setInputValues(prev => ({ ...prev, [cfg.valueKey!]: String(paramValue) }));
(e.target as HTMLInputElement).blur();
return;
}
const numValue = Number(inputStr);
const min = cfg.valueMin ?? 0;
const max = cfg.valueMax ?? 100;
// 기준값(Period, Length 등)은 양수만 허용
const isPeriodValue = cfg.valueKey && (
cfg.valueKey.toLowerCase().includes('period') ||
cfg.valueKey.toLowerCase().includes('length') ||
cfg.valueKey.toLowerCase().includes('slowing')
);
if (!isNaN(numValue) && numValue >= min && numValue <= max) {
// 기준값인 경우 양수 확인
if (isPeriodValue && numValue < 0) {
setInputValues(prev => ({ ...prev, [cfg.valueKey!]: String(paramValue) }));
(e.target as HTMLInputElement).blur();
return;
}
handleSettingChange(cfg.valueKey!, numValue);
setInputValues(prev => ({ ...prev, [cfg.valueKey!]: String(numValue) }));
console.log(`🔄 [IndicatorSettingsPopup] ${cfg.label} - 엔터 입력: ${numValue}`);
(e.target as HTMLInputElement).blur(); // 포커스 해제
} else {
// 유효하지 않은 값이면 원래 값으로 복원
setInputValues(prev => ({ ...prev, [cfg.valueKey!]: String(paramValue) }));
}
}
}}
sx={{
'& .MuiInputBase-root': {
height: 32,
fontSize: '12px'
},
'& .MuiInputBase-input': {
py: 0.5
}
}}
/>
)}
ListboxProps={{
style: { maxHeight: '200px', fontSize: '12px' }
}}
sx={{
'& .MuiAutocomplete-option': {
fontSize: '12px',
minHeight: '32px'
}
}}
/>
) : (
<Typography variant="caption" color="text.disabled">-</Typography>
)}
</Grid>
)}
{/* 색상 선택 */}
<Grid item xs={hasEnableColumn ? (hasValueColumn ? 1 : 1.5) : (hasValueColumn ? 1.2 : (!hasWidthColumn && !hasLineStyleColumn ? 4 : 1.5))} sx={{ display: 'flex', alignItems: 'center' }}>
{cfg.colorKey ? (
<Box sx={{ position: 'relative', display: 'inline-block' }}>
<Box
sx={{
width: 28,
height: 28,
borderRadius: 1,
bgcolor: colorValue,
border: '1px solid',
borderColor: 'divider',
cursor: 'pointer',
'&:hover': { boxShadow: '0 0 0 2px rgba(25, 118, 210, 0.5)' },
}}
/>
<input
type="color"
defaultValue={colorValue.startsWith('#') ? colorValue : '#666666'}
ref={(el) => {
if (el) {
colorInputRefsRef.current.set(cfg.colorKey!, el);
}
}}
onInput={(e) => {
// ✅ 색상 선택 중: 미리보기만 업데이트 (리렌더링 방지)
const target = e.target as HTMLInputElement;
const box = target.previousElementSibling as HTMLElement;
if (box) {
box.style.backgroundColor = target.value;
}
}}
style={{ position: 'absolute', top: 0, left: 0, width: 28, height: 28, opacity: 0, cursor: 'pointer' }}
/>
</Box>
) : (
<Typography variant="caption" color="text.disabled">-</Typography>
)}
</Grid>
{/* 선 굵기 슬라이더 */}
{hasWidthColumn && (
<Grid item xs={hasEnableColumn ? (hasValueColumn ? 2.5 : 2.5) : (hasValueColumn ? 3 : 3)} sx={{ display: 'flex', alignItems: 'center' }}>
{cfg.widthKey ? (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, width: '100%' }}>
<Slider
value={widthValue}
onChange={(_, value) => handleColorChange(cfg.widthKey!, value as number)}
min={0.5}
max={5}
step={0.5}
size="small"
sx={{ flex: 1 }}
/>
<Typography variant="caption" sx={{ minWidth: 30, textAlign: 'right', fontSize: '11px' }}>
{widthValue}px
</Typography>
</Box>
) : (
<Typography variant="caption" color="text.disabled">-</Typography>
)}
</Grid>
)}
{/* 선 유형 선택 */}
{hasLineStyleColumn && (
<Grid item xs={hasEnableColumn ? (hasValueColumn ? 2 : 2.5) : (hasValueColumn ? 2.5 : 2.5)} sx={{ display: 'flex', alignItems: 'center' }}>
{cfg.lineStyleKey ? (
<Select
value={lineStyleValue}
onChange={(e) => handleColorChange(cfg.lineStyleKey!, e.target.value)}
size="small"
fullWidth
sx={{
fontSize: '11px',
height: 28,
'& .MuiSelect-select': { py: 0.5 }
}}
>
<MenuItem value="solid" sx={{ fontSize: '11px' }}>━━ 실선</MenuItem>
<MenuItem value="dashed" sx={{ fontSize: '11px' }}>╍╍ 대시</MenuItem>
<MenuItem value="dotted" sx={{ fontSize: '11px' }}>··· 점선</MenuItem>
</Select>
) : (
<Typography variant="caption" color="text.disabled">-</Typography>
)}
</Grid>
)}
{/* 미리보기 */}
<Grid item xs={hasEnableColumn ? (hasValueColumn ? (hasWidthColumn && hasLineStyleColumn ? 2.7 : 4) : (hasWidthColumn && hasLineStyleColumn ? 2.5 : 4)) : (hasValueColumn ? (hasWidthColumn && hasLineStyleColumn ? 3.5 : 4) : (hasWidthColumn && hasLineStyleColumn ? 2.5 : 4))} sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
{cfg.colorKey ? (
<svg width="100" height="20" style={{ display: 'block' }}>
<line
x1="0"
y1="10"
x2="100"
y2="10"
stroke={colorValue}
strokeWidth={widthValue}
strokeDasharray={
lineStyleValue === 'dashed' ? '5,5' :
lineStyleValue === 'dotted' ? '2,2' : '0'
}
/>
</svg>
) : (
<Typography variant="caption" color="text.disabled">-</Typography>
)}
</Grid>
</Grid>
</Box>
);
})}
</Box>
);
};
// 보조지표별 설정 렌더링
const renderIndicatorSettings = () => {
switch (indicatorType) {
case 'volume':
return (
<>
<ColorTable configs={[
{ label: '상승', colorKey: 'volumeRising', enableKey: 'volumeRisingEnabled' },
{ label: '하락', colorKey: 'volumeFalling', enableKey: 'volumeFallingEnabled' },
]} />
</>
);
case 'macd':
return (
<>
<ColorTable configs={[
{ label: 'MACD', colorKey: 'macdLine', widthKey: 'macdLineWidth', lineStyleKey: 'macdLineStyle', valueKey: 'macdFastPeriod', valueMin: 1, valueMax: 500, valueStep: 1, enableKey: 'macdEnabled' },
{ label: '장기 EMA', colorKey: 'slowEmaColor', widthKey: 'slowEmaWidth', lineStyleKey: 'slowEmaLineStyle', valueKey: 'macdSlowPeriod', valueMin: 1, valueMax: 500, valueStep: 1, enableKey: 'macdEnabled' },
{ label: '시그널', colorKey: 'signalLine', widthKey: 'signalLineWidth', lineStyleKey: 'signalLineStyle', valueKey: 'macdSignalPeriod', valueMin: 1, valueMax: 500, valueStep: 1, enableKey: 'macdEnabled' },
{ label: '0선', colorKey: 'macdZeroLineColor', widthKey: 'macdZeroLineWidth', lineStyleKey: 'macdZeroLineStyle', enableKey: 'macdZeroLineEnabled' },
{ label: '히스토그램(상승)', colorKey: 'histogramPositive', enableKey: 'histogramPositiveEnabled' },
{ label: '히스토그램(하락)', colorKey: 'histogramNegative', enableKey: 'histogramNegativeEnabled' },
]} />
</>
);
case 'stochastic':
return (
<>
<Box sx={{ mb: 2, p: 2, bgcolor: 'info.main', color: 'white', borderRadius: 1 }}>
<Typography variant="caption" sx={{ fontWeight: 'bold', display: 'block', mb: 0.5 }}>
Stochastic Slow 설명
</Typography>
<Typography variant="caption" sx={{ fontSize: '11px', display: 'block' }}>
기간: Raw Stochastic %K 계산 기간 (업비트 기본값: 12일)
</Typography>
<Typography variant="caption" sx={{ fontSize: '11px', display: 'block' }}>
%K: Slow %K의 Smoothing 기간 (업비트 기본값: 5일)
</Typography>
<Typography variant="caption" sx={{ fontSize: '11px', display: 'block' }}>
%D: Slow %D (시그널선) 계산 기간 (업비트 기본값: 5일)
</Typography>
</Box>
<ColorTable configs={[
{ label: '기간', colorKey: 'stochasticK', widthKey: 'stochasticKWidth', lineStyleKey: 'stochasticKLineStyle', valueKey: 'stochasticKPeriod', valueMin: 1, valueMax: 500, valueStep: 1, enableKey: 'stochasticEnabled' },
{ label: '%K', colorKey: 'stochasticK', widthKey: 'stochasticKWidth', lineStyleKey: 'stochasticKLineStyle', valueKey: 'stochasticSlowing', valueMin: 1, valueMax: 500, valueStep: 1, enableKey: 'stochasticEnabled' },
{ label: '%D', colorKey: 'stochasticD', widthKey: 'stochasticDWidth', lineStyleKey: 'stochasticDLineStyle', valueKey: 'stochasticDPeriod', valueMin: 1, valueMax: 500, valueStep: 1, enableKey: 'stochasticEnabled' },
{ label: '과매도 기준선', colorKey: 'stochasticOversoldColor', widthKey: 'stochasticOversoldWidth', lineStyleKey: 'stochasticOversoldLineStyle', valueKey: 'stochasticOversold', valueMin: 0, valueMax: 50, valueStep: 1, enableKey: 'stochasticOversoldEnabled' },
{ label: '중앙값 기준선', colorKey: 'stochasticNeutralColor', widthKey: 'stochasticNeutralWidth', lineStyleKey: 'stochasticNeutralLineStyle', valueKey: 'stochasticNeutral', valueMin: 40, valueMax: 60, valueStep: 1, enableKey: 'stochasticNeutralEnabled' },
{ label: '과매수 기준선', colorKey: 'stochasticOverboughtColor', widthKey: 'stochasticOverboughtWidth', lineStyleKey: 'stochasticOverboughtLineStyle', valueKey: 'stochasticOverbought', valueMin: 50, valueMax: 100, valueStep: 1, enableKey: 'stochasticOverboughtEnabled' },
]} />
</>
);
case 'rsi':
return (
<>
<ColorTable configs={[
{ label: 'RSI', colorKey: 'rsiColor', widthKey: 'rsiWidth', lineStyleKey: 'rsiLineStyle', valueKey: 'rsiPeriod', valueMin: 1, valueMax: 500, valueStep: 1, enableKey: 'rsiEnabled' },
{ label: '신호', colorKey: 'rsiSignalColor', widthKey: 'rsiSignalWidth', lineStyleKey: 'rsiSignalLineStyle', valueKey: 'rsiSignalPeriod', valueMin: 1, valueMax: 500, valueStep: 1, enableKey: 'rsiSignalEnabled' },
{ label: '과매수', colorKey: 'rsiOverboughtColor', widthKey: 'rsiOverboughtWidth', lineStyleKey: 'rsiOverboughtLineStyle', valueKey: 'rsiOverbought', valueMin: -1000, valueMax: 1000, valueStep: 1, enableKey: 'rsiOverboughtEnabled' },
{ label: '중앙값', colorKey: 'rsiMiddleColor', widthKey: 'rsiMiddleWidth', lineStyleKey: 'rsiMiddleLineStyle', valueKey: 'rsiMiddle', valueMin: -1000, valueMax: 1000, valueStep: 1, enableKey: 'rsiMiddleEnabled' },
{ label: '과매도', colorKey: 'rsiOversoldColor', widthKey: 'rsiOversoldWidth', lineStyleKey: 'rsiOversoldLineStyle', valueKey: 'rsiOversold', valueMin: -1000, valueMax: 1000, valueStep: 1, enableKey: 'rsiOversoldEnabled' },
]} />
</>
);
case 'adx':
return (
<>
<ColorTable configs={[
{ label: 'ADX', colorKey: 'adxColor', widthKey: 'adxWidth', lineStyleKey: 'adxLineStyle', valueKey: 'adxPeriod', valueMin: 1, valueMax: 500, valueStep: 1, enableKey: 'adxEnabled' },
{ label: '+DI', colorKey: 'plusDiColor', widthKey: 'plusDiWidth', lineStyleKey: 'plusDiLineStyle', enableKey: 'plusDiEnabled' },
{ label: '-DI', colorKey: 'minusDiColor', widthKey: 'minusDiWidth', lineStyleKey: 'minusDiLineStyle', enableKey: 'minusDiEnabled' },
{ label: '강한 추세', colorKey: 'adxStrongColor', widthKey: 'adxStrongWidth', lineStyleKey: 'adxStrongLineStyle', valueKey: 'adxStrongThreshold', valueMin: -1000, valueMax: 1000, valueStep: 1, enableKey: 'adxStrongEnabled' },
{ label: '약한 추세', colorKey: 'adxWeakColor', widthKey: 'adxWeakWidth', lineStyleKey: 'adxWeakLineStyle', valueKey: 'adxWeakThreshold', valueMin: -1000, valueMax: 1000, valueStep: 1, enableKey: 'adxWeakEnabled' },
]} />
</>
);
case 'cci':
return (
<>
<ColorTable configs={[
{ label: 'CCI', colorKey: 'cciColor', widthKey: 'cciWidth', lineStyleKey: 'cciLineStyle', valueKey: 'cciPeriod', valueMin: 1, valueMax: 500, valueStep: 1, enableKey: 'cciEnabled' },
{ label: '신호', colorKey: 'cciSignalColor', widthKey: 'cciSignalWidth', lineStyleKey: 'cciSignalLineStyle', valueKey: 'cciSignalPeriod', valueMin: 1, valueMax: 500, valueStep: 1, enableKey: 'cciSignalEnabled' },
{ label: '과열', colorKey: 'cciOverboughtColor', widthKey: 'cciOverboughtWidth', lineStyleKey: 'cciOverboughtLineStyle', valueKey: 'cciOverbought', valueMin: -1000, valueMax: 1000, valueStep: 1, enableKey: 'cciOverboughtEnabled' },
{ label: '중앙값', colorKey: 'cciMiddleColor', widthKey: 'cciMiddleWidth', lineStyleKey: 'cciMiddleLineStyle', valueKey: 'cciMiddle', valueMin: -1000, valueMax: 1000, valueStep: 1, enableKey: 'cciMiddleEnabled' },
{ label: '침체', colorKey: 'cciOversoldColor', widthKey: 'cciOversoldWidth', lineStyleKey: 'cciOversoldLineStyle', valueKey: 'cciOversold', valueMin: -1000, valueMax: 1000, valueStep: 1, enableKey: 'cciOversoldEnabled' },
]} />
</>
);
case 'trix':
return (
<>
<ColorTable configs={[
{ label: 'TRIX', colorKey: 'trixColor', widthKey: 'trixWidth', lineStyleKey: 'trixLineStyle', valueKey: 'trixPeriod', valueMin: 1, valueMax: 500, valueStep: 1, enableKey: 'trixEnabled' },
{ label: '신호', colorKey: 'trixSignalColor', widthKey: 'trixSignalWidth', lineStyleKey: 'trixSignalLineStyle', valueKey: 'trixSignalPeriod', valueMin: 1, valueMax: 500, valueStep: 1, enableKey: 'trixEnabled' },
{ label: '과매수', colorKey: 'trixOverboughtColor', widthKey: 'trixOverboughtWidth', lineStyleKey: 'trixOverboughtLineStyle', valueKey: 'trixOverbought', valueMin: -1000, valueMax: 1000, valueStep: 1, enableKey: 'trixOverboughtEnabled' },
{ label: '중앙값', colorKey: 'trixMiddleColor', widthKey: 'trixMiddleWidth', lineStyleKey: 'trixMiddleLineStyle', valueKey: 'trixMiddle', valueMin: -1000, valueMax: 1000, valueStep: 1, enableKey: 'trixMiddleEnabled' },
{ label: '과매도', colorKey: 'trixOversoldColor', widthKey: 'trixOversoldWidth', lineStyleKey: 'trixOversoldLineStyle', valueKey: 'trixOversold', valueMin: -1000, valueMax: 1000, valueStep: 1, enableKey: 'trixOversoldEnabled' },
]} />
</>
);
case 'disparity':
return (
<>
<ColorTable configs={[
{ label: '초단기', colorKey: 'disparityUltraShortColor', widthKey: 'disparityUltraShortWidth', lineStyleKey: 'disparityUltraShortLineStyle', valueKey: 'disparityUltraShortPeriod', valueMin: 1, valueMax: 500, valueStep: 1, enableKey: 'disparityUltraShortEnabled' },
{ label: '단기', colorKey: 'disparityShortColor', widthKey: 'disparityShortWidth', lineStyleKey: 'disparityShortLineStyle', valueKey: 'disparityShortPeriod', valueMin: 1, valueMax: 500, valueStep: 1, enableKey: 'disparityShortEnabled' },
{ label: '중기', colorKey: 'disparityMidColor', widthKey: 'disparityMidWidth', lineStyleKey: 'disparityMidLineStyle', valueKey: 'disparityMidPeriod', valueMin: 1, valueMax: 500, valueStep: 1, enableKey: 'disparityMidEnabled' },
{ label: '장기', colorKey: 'disparityLongColor', widthKey: 'disparityLongWidth', lineStyleKey: 'disparityLongLineStyle', valueKey: 'disparityLongPeriod', valueMin: 1, valueMax: 500, valueStep: 1, enableKey: 'disparityLongEnabled' },
{ label: '100 기준선', colorKey: 'disparityBaseLineColor', widthKey: 'disparityBaseLineWidth', lineStyleKey: 'disparityBaseLineStyle', enableKey: 'disparityBaseLineEnabled' },
]} />
</>
);
case 'newPsychological':
return (
<>
<ColorTable configs={[
{ label: '신심리도', colorKey: 'newPsychologicalColor', widthKey: 'newPsychologicalWidth', lineStyleKey: 'newPsychologicalLineStyle', valueKey: 'newPsychologicalPeriod', valueMin: 1, valueMax: 500, valueStep: 1, enableKey: 'newPsychologicalEnabled' },
{ label: '과열', colorKey: 'newPsychologicalOverboughtColor', widthKey: 'newPsychologicalOverboughtWidth', lineStyleKey: 'newPsychologicalOverboughtLineStyle', valueKey: 'newPsychologicalOverbought', valueMin: -1000, valueMax: 1000, valueStep: 1, enableKey: 'newPsychologicalOverboughtEnabled' },
{ label: '중앙값', colorKey: 'newPsychologicalNeutralColor', widthKey: 'newPsychologicalNeutralWidth', lineStyleKey: 'newPsychologicalNeutralLineStyle', valueKey: 'newPsychologicalMiddle', valueMin: -1000, valueMax: 1000, valueStep: 1, enableKey: 'newPsychologicalMiddleEnabled' },
{ label: '침체', colorKey: 'newPsychologicalOversoldColor', widthKey: 'newPsychologicalOversoldWidth', lineStyleKey: 'newPsychologicalOversoldLineStyle', valueKey: 'newPsychologicalOversold', valueMin: -1000, valueMax: 1000, valueStep: 1, enableKey: 'newPsychologicalOversoldEnabled' },
]} />
</>
);
case 'investPsychological':
return (
<>
<ColorTable configs={[
{ label: '투자심리도', colorKey: 'investPsychologicalColor', widthKey: 'investPsychologicalWidth', lineStyleKey: 'investPsychologicalLineStyle', valueKey: 'investPsychologicalPeriod', valueMin: 1, valueMax: 500, valueStep: 1, enableKey: 'investPsychologicalEnabled' },
{ label: '과열', colorKey: 'investPsychologicalOverboughtColor', widthKey: 'investPsychologicalOverboughtWidth', lineStyleKey: 'investPsychologicalOverboughtLineStyle', valueKey: 'investPsychologicalOverbought', valueMin: -1000, valueMax: 1000, valueStep: 1, enableKey: 'investPsychologicalOverboughtEnabled' },
{ label: '중앙값', colorKey: 'investPsychologicalMiddleColor', widthKey: 'investPsychologicalMiddleWidth', lineStyleKey: 'investPsychologicalMiddleLineStyle', valueKey: 'investPsychologicalMiddle', valueMin: -1000, valueMax: 1000, valueStep: 1, enableKey: 'investPsychologicalMiddleEnabled' },
{ label: '침체', colorKey: 'investPsychologicalOversoldColor', widthKey: 'investPsychologicalOversoldWidth', lineStyleKey: 'investPsychologicalOversoldLineStyle', valueKey: 'investPsychologicalOversold', valueMin: -1000, valueMax: 1000, valueStep: 1, enableKey: 'investPsychologicalOversoldEnabled' },
]} />
</>
);
case 'williamsR':
return (
<>
<ColorTable configs={[
{ label: 'Williams%R', colorKey: 'williamsRColor', widthKey: 'williamsRWidth', lineStyleKey: 'williamsRLineStyle', valueKey: 'williamsRPeriod', valueMin: 1, valueMax: 500, valueStep: 1, enableKey: 'williamsREnabled' },
{ label: '과열', colorKey: 'williamsROverboughtColor', widthKey: 'williamsROverboughtWidth', lineStyleKey: 'williamsROverboughtLineStyle', valueKey: 'williamsROverbought', valueMin: -1000, valueMax: 1000, valueStep: 1, enableKey: 'williamsROverboughtEnabled' },
{ label: '중앙값', colorKey: 'williamsRMiddleColor', widthKey: 'williamsRMiddleWidth', lineStyleKey: 'williamsRMiddleLineStyle', valueKey: 'williamsRMiddle', valueMin: -1000, valueMax: 1000, valueStep: 1, enableKey: 'williamsRMiddleEnabled' },
{ label: '침체', colorKey: 'williamsROversoldColor', widthKey: 'williamsROversoldWidth', lineStyleKey: 'williamsROversoldLineStyle', valueKey: 'williamsROversold', valueMin: -1000, valueMax: 1000, valueStep: 1, enableKey: 'williamsROversoldEnabled' },
]} />
</>
);
case 'bwi':
return (
<>
<ColorTable configs={[
{ label: 'BWI', colorKey: 'bwiColor', widthKey: 'bwiWidth', lineStyleKey: 'bwiLineStyle', valueKey: 'bwiPeriod', valueMin: 1, valueMax: 500, valueStep: 1, enableKey: 'bwiEnabled' },
]} />
</>
);
case 'obv':
return (
<>
<ColorTable configs={[
{ label: 'OBV', colorKey: 'obvColor', widthKey: 'obvWidth', lineStyleKey: 'obvLineStyle', enableKey: 'obvEnabled' },
{ label: '신호', colorKey: 'obvSignalColor', widthKey: 'obvSignalWidth', lineStyleKey: 'obvSignalLineStyle', valueKey: 'obvSignalPeriod', valueMin: 1, valueMax: 500, valueStep: 1, enableKey: 'obvSignalEnabled' },
]} />
</>
);
case 'volumeOsc':
return (
<>
<ColorTable configs={[
{ label: 'VolumeOSC', colorKey: 'volumeOscColor', widthKey: 'volumeOscWidth', lineStyleKey: 'volumeOscLineStyle', enableKey: 'volumeOscEnabled' },
{ label: '단기', colorKey: 'volumeOscColor', widthKey: 'volumeOscWidth', lineStyleKey: 'volumeOscLineStyle', valueKey: 'volumeOscShortPeriod', valueMin: 1, valueMax: 500, valueStep: 1, enableKey: 'volumeOscEnabled' },
{ label: '장기', colorKey: 'volumeOscColor', widthKey: 'volumeOscWidth', lineStyleKey: 'volumeOscLineStyle', valueKey: 'volumeOscLongPeriod', valueMin: 1, valueMax: 500, valueStep: 1, enableKey: 'volumeOscEnabled' },
{ label: '0선', colorKey: 'volumeOscZeroLineColor', widthKey: 'volumeOscZeroLineWidth', lineStyleKey: 'volumeOscZeroLineStyle', enableKey: 'volumeOscZeroLineEnabled' },
]} />
</>
);
case 'vr':
return (
<>
<ColorTable configs={[
{ label: 'VR', colorKey: 'vrColor', widthKey: 'vrWidth', lineStyleKey: 'vrLineStyle', valueKey: 'vrPeriod', valueMin: 1, valueMax: 500, valueStep: 1, enableKey: 'vrEnabled' },
{ label: 'VR2', colorKey: 'vr2Color', widthKey: 'vr2Width', lineStyleKey: 'vr2LineStyle', valueKey: 'vr2Period', valueMin: 1, valueMax: 500, valueStep: 1, enableKey: 'vr2Enabled' },
{ label: '과열', colorKey: 'vrOverboughtColor', widthKey: 'vrOverboughtWidth', lineStyleKey: 'vrOverboughtLineStyle', valueKey: 'vrOverbought', valueMin: -1000, valueMax: 1000, valueStep: 1, enableKey: 'vrOverboughtEnabled' },
{ label: '기준', colorKey: 'vrBaseLineColor', widthKey: 'vrBaseLineWidth', lineStyleKey: 'vrBaseLineStyle', valueKey: 'vrBaseLine', valueMin: -1000, valueMax: 1000, valueStep: 1, enableKey: 'vrBaseLineEnabled' },
{ label: '침체', colorKey: 'vrOversoldColor', widthKey: 'vrOversoldWidth', lineStyleKey: 'vrOversoldLineStyle', valueKey: 'vrOversold', valueMin: -1000, valueMax: 1000, valueStep: 1, enableKey: 'vrOversoldEnabled' },
]} />
</>
);
case 'dmi':
return (
<>
<ColorTable configs={[
{ label: '기간', valueKey: 'dmiPeriod', valueMin: 1, valueMax: 500, valueStep: 1 },
{ label: 'DI+', colorKey: 'plusDiColor', widthKey: 'plusDiWidth', lineStyleKey: 'plusDiLineStyle', enableKey: 'plusDiEnabled' },
{ label: 'DI-', colorKey: 'minusDiColor', widthKey: 'minusDiWidth', lineStyleKey: 'minusDiLineStyle', enableKey: 'minusDiEnabled' },
{ label: '과열', colorKey: 'dmiOverboughtColor', widthKey: 'dmiOverboughtWidth', lineStyleKey: 'dmiOverboughtLineStyle', valueKey: 'dmiOverbought', valueMin: 0, valueMax: 100, valueStep: 1, enableKey: 'dmiOverboughtEnabled' },
{ label: '중간값', colorKey: 'dmiMiddleColor', widthKey: 'dmiMiddleWidth', lineStyleKey: 'dmiMiddleLineStyle', valueKey: 'dmiMiddle', valueMin: 0, valueMax: 100, valueStep: 1, enableKey: 'dmiMiddleEnabled' },
{ label: '침체', colorKey: 'dmiOversoldColor', widthKey: 'dmiOversoldWidth', lineStyleKey: 'dmiOversoldLineStyle', valueKey: 'dmiOversold', valueMin: 0, valueMax: 100, valueStep: 1, enableKey: 'dmiOversoldEnabled' },
]} />
</>
);
case 'momentum':
return (
<>
<ColorTable configs={[
{ label: 'Momentum', colorKey: 'momentumColor', widthKey: 'momentumWidth', lineStyleKey: 'momentumLineStyle', valueKey: 'momentumPeriod', valueMin: 1, valueMax: 500, valueStep: 1, enableKey: 'momentumEnabled' },
{ label: '0선 기준선', colorKey: 'momentumZeroLineColor', widthKey: 'momentumZeroLineWidth', lineStyleKey: 'momentumZeroLineStyle', enableKey: 'momentumZeroLineEnabled' },
]} />
</>
);
default:
return <Typography>설정 항목이 없습니다.</Typography>;
}
};
// 드래그 가능한 Paper 컴포넌트
function DraggablePaper(props: PaperProps) {
return (
<Draggable
handle="#draggable-dialog-title"
cancel={'[class*="MuiDialogContent-root"], [class*="MuiIconButton-root"], [class*="MuiButton-root"]'}
position={dialogPosition}
onStop={(e, data) => {
setDialogPosition({ x: data.x, y: data.y });
}}
>
<Paper {...props} />
</Draggable>
);
}
return (
<>
<Dialog
open={open}
onClose={onClose}
maxWidth={false}
fullScreen={isMobile}
PaperComponent={isMobile ? Paper : DraggablePaper}
PaperProps={{
sx: {
bgcolor: 'background.paper',
backgroundImage: 'none',
width: isMobile ? '100%' : '900px',
minWidth: isMobile ? '100%' : '900px',
maxWidth: isMobile ? '100%' : '90vw',
minHeight: indicatorType === 'macd' ? '550px' : (indicatorType === 'cci' ? '450px' : '350px'),
maxHeight: '80vh',
resize: isMobile ? 'none' : 'both',
overflow: 'auto',
'&::after': isMobile ? {} : {
content: '""',
position: 'absolute',
right: 0,
bottom: 0,
width: '16px',
height: '16px',
background: 'linear-gradient(135deg, transparent 50%, rgba(0,0,0,0.2) 50%)',
cursor: 'nwse-resize',
pointerEvents: 'none',
},
},
}}
>
<DialogTitle
id="draggable-dialog-title"
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
bgcolor: '#1976d2',
color: '#fff',
py: 1.2,
cursor: isMobile ? 'default' : 'move',
}}
>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<SettingsIcon sx={{ color: '#fff' }} />
<Typography variant="h6" sx={{ fontWeight: 600, fontSize: '16px', color: '#fff' }}>
{info.name} 설정
</Typography>
<IconButton
onClick={() => setShowHelpDialog(true)}
size="small"
sx={{
ml: 0.5,
color: '#fff',
'&:hover': { bgcolor: 'rgba(255,255,255,0.1)' }
}}
>
<HelpOutlineIcon fontSize="small" />
</IconButton>
</Box>
<Box
sx={{ display: 'flex', alignItems: 'center', gap: 1 }}
onMouseDown={(e) => e.stopPropagation()}
>
<Button
onClick={(e) => {
e.stopPropagation();
onClose();
}}
sx={{
color: '#fff',
borderColor: '#fff',
'&:hover': {
bgcolor: 'rgba(255,255,255,0.1)',
borderColor: '#fff'
},
minWidth: '70px',
fontSize: '13px',
fontWeight: 600,
}}
variant="outlined"
size="small"
>
취소
</Button>
<Button
onClick={(e) => {
e.stopPropagation();
handleSave();
}}
sx={{
bgcolor: '#fff',
color: '#1976d2',
'&:hover': {
bgcolor: 'rgba(255,255,255,0.9)'
},
minWidth: '70px',
fontSize: '13px',
fontWeight: 600,
}}
variant="contained"
size="small"
>
적용
</Button>
</Box>
</DialogTitle>
<DialogContent sx={{
pt: 1.5,
pb: 2,
px: 2
}}>
{renderIndicatorSettings()}
</DialogContent>
{/* ✅ 하단 적용 버튼 (테스트용) */}
<DialogActions sx={{
px: 2,
pb: 2,
pt: 1,
justifyContent: 'flex-end',
borderTop: '1px solid',
borderColor: 'divider'
}}>
<Button
onClick={handleSave}
variant="contained"
color="primary"
size="medium"
sx={{
minWidth: '100px',
fontWeight: 600,
}}
>
적용
</Button>
</DialogActions>
</Dialog>
{/* 상세 설명 다이얼로그 */}
<Dialog
open={showHelpDialog}
onClose={() => setShowHelpDialog(false)}
maxWidth="md"
fullWidth
fullScreen={isMobile}
PaperProps={{
sx: {
bgcolor: 'background.paper',
backgroundImage: 'none',
},
}}
>
<DialogTitle sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
pb: 1,
borderBottom: '1px solid',
borderColor: 'divider',
bgcolor: 'primary.main',
color: 'white',
}}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<HelpOutlineIcon />
<Typography variant="h6" sx={{ fontWeight: 600 }}>
{info.name} 상세 설명
</Typography>
</Box>
<IconButton onClick={() => setShowHelpDialog(false)} size="small" sx={{ color: 'white' }}>
<CloseIcon />
</IconButton>
</DialogTitle>
<DialogContent sx={{ pt: 3, pb: 3, maxHeight: isMobile ? '65vh' : '55vh', overflowY: 'auto' }}>
{getIndicatorDetailedHelp(indicatorType)}
</DialogContent>
<DialogActions sx={{ p: 2, borderTop: '1px solid', borderColor: 'divider' }}>
<Button onClick={() => setShowHelpDialog(false)} variant="contained" color="primary" fullWidth={isMobile}>
확인
</Button>
</DialogActions>
</Dialog>
</>
);
};
export default IndicatorSettingsPopup;