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

1706 lines
69 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, useCallback } from 'react';
import {
Box,
TextField,
Grid,
Typography,
Button,
Alert,
Divider,
Accordion,
AccordionSummary,
AccordionDetails,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Chip,
IconButton,
Tooltip,
CircularProgress,
Switch,
FormControlLabel,
FormControl,
Select,
MenuItem,
InputLabel,
} from '@mui/material';
import { RestartAlt, ExpandMore, CheckCircle, Error, Palette, Add, Delete, Star, StarBorder, HelpOutline, Close } from '@mui/icons-material';
import { useChartColors, ChartColorSettings } from '../../contexts/ChartColorContext';
import {
useIndicatorSettings,
IndicatorSettings,
DEFAULT_INDICATOR_SETTINGS,
} from '../../contexts/IndicatorSettingsContext';
import {
getIndicatorPeriods,
addPeriod,
updatePeriod,
deletePeriod,
invalidatePeriodsCache,
IndicatorPeriodsResponse,
PeriodInfo,
} from '../../services/indicatorPeriodApi';
// 타입과 기본값을 Context에서 재내보내기 (하위 호환성 유지)
export type { IndicatorSettings } from '../../contexts/IndicatorSettingsContext';
export {
DEFAULT_INDICATOR_SETTINGS,
loadIndicatorSettingsFromStorage as loadIndicatorSettings,
saveIndicatorSettingsToStorage as saveIndicatorSettings,
} from '../../contexts/IndicatorSettingsContext';
// 기간 관리 컴포넌트
interface PeriodManagerProps {
indicatorType: string;
label: string;
}
const PeriodManager: React.FC<PeriodManagerProps> = ({ indicatorType, label }) => {
const [periods, setPeriods] = useState<IndicatorPeriodsResponse | null>(null);
const [loading, setLoading] = useState(true);
const [newPeriod, setNewPeriod] = useState<string>('');
const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState<string | null>(null);
const loadPeriods = useCallback(async () => {
try {
setLoading(true);
setError(null);
const data = await getIndicatorPeriods(indicatorType);
setPeriods(data);
} catch (err: any) {
console.error('기간 로드 실패:', err);
// API 오류 시 빈 응답으로 처리 (데이터 추가 가능하도록)
setPeriods({
indicatorType,
indicatorDisplayName: label,
periods: [],
defaultPeriod: null,
});
setError(`${label} 기간 데이터를 불러올 수 없습니다. 새 기간을 추가해주세요.`);
} finally {
setLoading(false);
}
}, [indicatorType, label]);
useEffect(() => {
loadPeriods();
}, [loadPeriods]);
const handleAddPeriod = async () => {
const periodNum = parseInt(newPeriod);
if (!periodNum || periodNum < 1) return;
try {
setError(null);
await addPeriod({
indicatorType,
period: periodNum,
isDefault: (periods?.periods.length || 0) === 0, // 첫 번째면 기본값
enabled: true,
});
invalidatePeriodsCache();
setNewPeriod('');
setSuccess(`${periodNum}일 기간이 추가되었습니다.`);
setTimeout(() => setSuccess(null), 2000);
loadPeriods();
} catch (err: any) {
const message = err.response?.data?.message || err.response?.data || '추가 실패';
setError(typeof message === 'string' ? message : JSON.stringify(message));
}
};
const handleDeletePeriod = async (id: number, period: number) => {
if (!window.confirm(`${period}일 기간을 삭제하시겠습니까?`)) return;
try {
setError(null);
await deletePeriod(id);
invalidatePeriodsCache();
setSuccess('삭제되었습니다.');
setTimeout(() => setSuccess(null), 2000);
loadPeriods();
} catch (err: any) {
setError('삭제 실패: ' + (err.response?.data?.message || err.message));
}
};
const handleSetDefault = async (id: number, period: number) => {
try {
setError(null);
await updatePeriod(id, { isDefault: true });
invalidatePeriodsCache();
setSuccess(`${period}일이 기본값으로 설정되었습니다.`);
setTimeout(() => setSuccess(null), 2000);
loadPeriods();
} catch (err: any) {
setError('기본값 설정 실패: ' + (err.response?.data?.message || err.message));
}
};
if (loading) {
return (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, py: 1 }}>
<CircularProgress size={16} />
<Typography variant="body2" color="text.secondary">기간 로드 ...</Typography>
</Box>
);
}
const hasPeriods = periods && periods.periods && periods.periods.length > 0;
return (
<Box sx={{ mt: 2, p: 2, bgcolor: 'action.hover', borderRadius: 1 }}>
<Typography variant="subtitle2" gutterBottom sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
📊 전략 규칙용 기간 관리 ({label})
</Typography>
{error && (
<Alert severity="warning" sx={{ mb: 1, py: 0 }} onClose={() => setError(null)}>
{error}
</Alert>
)}
{success && (
<Alert severity="success" sx={{ mb: 1, py: 0 }} onClose={() => setSuccess(null)}>
{success}
</Alert>
)}
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1, mb: 2, minHeight: 32, alignItems: 'center' }}>
{hasPeriods ? (
periods.periods.map((p: PeriodInfo) => (
<Chip
key={p.id}
label={`${p.period}일`}
color={p.isDefault ? 'primary' : 'default'}
variant={p.enabled ? 'filled' : 'outlined'}
size="small"
icon={p.isDefault ? <Star fontSize="small" /> : undefined}
onDelete={() => handleDeletePeriod(p.id, p.period)}
onClick={() => !p.isDefault && handleSetDefault(p.id, p.period)}
sx={{
cursor: p.isDefault ? 'default' : 'pointer',
'&:hover': { opacity: p.isDefault ? 1 : 0.8 }
}}
/>
))
) : (
<Typography variant="body2" color="text.secondary" sx={{ fontStyle: 'italic' }}>
등록된 기간이 없습니다. 아래에서 기간을 추가해주세요.
</Typography>
)}
</Box>
<Box sx={{ display: 'flex', gap: 1, alignItems: 'center', flexWrap: 'wrap' }}>
<TextField
size="small"
type="number"
placeholder="기간 (일)"
value={newPeriod}
onChange={(e) => setNewPeriod(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && handleAddPeriod()}
sx={{ width: 100 }}
inputProps={{ min: 1, max: 500 }}
/>
<Button
size="small"
variant="contained"
color="primary"
startIcon={<Add />}
onClick={handleAddPeriod}
disabled={!newPeriod || parseInt(newPeriod) < 1}
>
기간 추가
</Button>
<Typography variant="caption" color="text.secondary" sx={{ ml: 1 }}>
💡 클릭: 기본값 설정 | 기본값 | 삭제
</Typography>
</Box>
{!hasPeriods && (
<Alert severity="info" sx={{ mt: 2, py: 0.5 }}>
<Typography variant="caption">
전략 규칙 생성 선택할 있는 기간을 추가해주세요.
</Typography>
</Alert>
)}
</Box>
);
};
// 색상 선택 컴포넌트
const ColorPicker: React.FC<{
label: string;
value: string;
onChange: (color: string) => void;
}> = ({ label, value, onChange }) => (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 1 }}>
<Box
sx={{
width: 32,
height: 32,
borderRadius: 1,
bgcolor: value,
border: '2px solid',
borderColor: 'divider',
cursor: 'pointer',
position: 'relative',
overflow: 'hidden',
}}
>
<input
type="color"
value={value}
onChange={(e) => onChange(e.target.value)}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
opacity: 0,
cursor: 'pointer',
}}
/>
</Box>
<Typography variant="body2" sx={{ minWidth: 100 }}>{label}</Typography>
<TextField
size="small"
value={value}
onChange={(e) => onChange(e.target.value)}
sx={{ width: 100 }}
inputProps={{ style: { fontSize: 12 } }}
/>
</Box>
);
interface IndicatorSettingsTabProps {
onSave?: (settings: IndicatorSettings) => void;
}
const IndicatorSettingsTab: React.FC<IndicatorSettingsTabProps> = ({ onSave }) => {
const { indicatorSettings, saveIndicatorSettings, resetIndicatorSettings } = useIndicatorSettings();
const [settings, setSettings] = useState<IndicatorSettings>(indicatorSettings);
const { colors, updateColors } = useChartColors();
const [localColors, setLocalColors] = useState<Partial<ChartColorSettings>>({
macdLine: colors.macdLine,
signalLine: colors.signalLine,
histogramPositive: colors.histogramPositive,
histogramNegative: colors.histogramNegative,
stochasticK: colors.stochasticK,
stochasticD: colors.stochasticD,
});
const [resultDialogOpen, setResultDialogOpen] = useState(false);
const [saveResult, setSaveResult] = useState<{ success: boolean; message: string }>({ success: false, message: '' });
const [macdHelpOpen, setMacdHelpOpen] = useState(false);
// Context의 설정값이 변경되면 로컬 상태도 동기화
useEffect(() => {
setSettings(indicatorSettings);
}, [indicatorSettings]);
// 설정 변경 시 즉시 Context에 반영 (실시간 차트 업데이트)
const handleSettingChange = (newSettings: Partial<IndicatorSettings>) => {
const updated = { ...settings, ...newSettings };
setSettings(updated);
// 즉시 Context에 반영하여 차트에 실시간 적용
saveIndicatorSettings(updated);
};
const handleSave = () => {
try {
saveIndicatorSettings(settings);
if (onSave) {
onSave(settings);
}
updateColors(localColors);
setSaveResult({
success: true,
message: `보조지표 설정이 저장되었습니다.\n\n` +
`신심리도: (${settings.newPsychologicalPeriod})\n` +
`투자심리도: (${settings.investPsychologicalPeriod})\n` +
`MACD: (${settings.macdFastPeriod}, ${settings.macdSlowPeriod}, ${settings.macdSignalPeriod})\n` +
`CCI: (${settings.cciPeriod}, ${settings.cciSignalPeriod})\n` +
`ADX: (${settings.adxPeriod})\n` +
`BWI: (${settings.bwiPeriod})\n` +
`DMI: (${settings.dmiPeriod})\n` +
`OBV: (${settings.obvPeriod})\n` +
`RSI: (${settings.rsiPeriod})\n` +
`Stochastic Slow: (${settings.stochasticKPeriod}, ${settings.stochasticDPeriod}, ${settings.stochasticSlowing})\n` +
`Williams %R: (${settings.williamsRPeriod})\n` +
`TRIX: (${settings.trixPeriod}, ${settings.trixSignalPeriod})\n` +
`VolumeOSC: (${settings.volumeOscShortPeriod}, ${settings.volumeOscLongPeriod})\n` +
`VR: (${settings.vrPeriod})\n` +
`이격도: (초단기:${settings.disparityUltraShortPeriod}, 단기:${settings.disparityShortPeriod}, 중기:${settings.disparityMidPeriod}, 장기:${settings.disparityLongPeriod})`
});
setResultDialogOpen(true);
} catch (error) {
setSaveResult({ success: false, message: `저장 중 오류가 발생했습니다: ${error}` });
setResultDialogOpen(true);
}
};
const handleResetAll = () => {
if (window.confirm('모든 보조지표 설정을 기본값으로 초기화하시겠습니까?')) {
resetIndicatorSettings();
setSettings(DEFAULT_INDICATOR_SETTINGS);
if (onSave) onSave(DEFAULT_INDICATOR_SETTINGS);
setSaveResult({
success: true,
message: `모든 보조지표 설정이 기본값으로 초기화되었습니다.`
});
setResultDialogOpen(true);
}
};
return (
<Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
<Typography variant="h6">보조지표 파라미터 설정 (15가지)</Typography>
<Button
variant="outlined"
size="small"
startIcon={<RestartAlt />}
onClick={handleResetAll}
color="secondary"
>
전체 초기화
</Button>
</Box>
<Alert severity="info" sx={{ mb: 3 }}>
<Typography variant="body2">
차트에서 사용되는 15가지 보조지표의 파라미터를 설정합니다. 변경된 설정은 <strong>즉시 차트에 반영</strong>됩니다.
</Typography>
</Alert>
{/* 1. 신심리도 설정 */}
<Accordion defaultExpanded={false} sx={{ mb: 2 }}>
<AccordionSummary expandIcon={<ExpandMore />}>
<Typography variant="subtitle1" sx={{ fontWeight: 'bold' }}>
1️⃣ 신심리도 (New Psychological Line)
</Typography>
</AccordionSummary>
<AccordionDetails>
<Grid container spacing={3}>
<Grid item xs={12} md={3}>
<TextField
fullWidth
label="기간"
type="number"
value={settings.newPsychologicalPeriod}
onChange={(e) => handleSettingChange({ newPsychologicalPeriod: Number(e.target.value) })}
inputProps={{ min: 1, max: 50 }}
helperText="기본값: 5"
/>
</Grid>
<Grid item xs={12} md={3}>
<TextField
fullWidth
label="침체 기준"
type="number"
value={settings.newPsychologicalOversold}
onChange={(e) => handleSettingChange({ newPsychologicalOversold: Number(e.target.value) })}
inputProps={{ min: -100, max: 100 }}
helperText="기본값: -50"
/>
</Grid>
<Grid item xs={12} md={3}>
<TextField
fullWidth
label="중간"
type="number"
value={settings.newPsychologicalMiddle}
onChange={(e) => handleSettingChange({ newPsychologicalMiddle: Number(e.target.value) })}
inputProps={{ min: -100, max: 100 }}
helperText="기본값: 0"
/>
</Grid>
<Grid item xs={12} md={3}>
<TextField
fullWidth
label="과열 기준"
type="number"
value={settings.newPsychologicalOverbought}
onChange={(e) => handleSettingChange({ newPsychologicalOverbought: Number(e.target.value) })}
inputProps={{ min: -100, max: 100 }}
helperText="기본값: 50"
/>
</Grid>
</Grid>
<Alert severity="info" sx={{ mt: 2 }}>
<Typography variant="body2">
신심리도는 시장 참여자들의 심리 상태를 측정하는 지표입니다.
상승일수 비율로 과매수/과매도를 판단합니다.
</Typography>
</Alert>
</AccordionDetails>
</Accordion>
{/* 2. 투자심리도 설정 */}
<Accordion defaultExpanded={false} sx={{ mb: 2 }}>
<AccordionSummary expandIcon={<ExpandMore />}>
<Typography variant="subtitle1" sx={{ fontWeight: 'bold' }}>
2️⃣ 투자심리도 (Investor Psychological Line)
</Typography>
</AccordionSummary>
<AccordionDetails>
<Grid container spacing={3}>
<Grid item xs={12} md={6}>
<TextField
fullWidth
label="기간"
type="number"
value={settings.investPsychologicalPeriod}
onChange={(e) => handleSettingChange({ investPsychologicalPeriod: Number(e.target.value) })}
inputProps={{ min: 1, max: 50 }}
helperText="기본값: 10"
/>
</Grid>
</Grid>
<Alert severity="info" sx={{ mt: 2 }}>
<Typography variant="body2">
투자심리도는 일정 기간 동안의 상승일 비율을 백분율로 표시합니다.
75% 이상은 과매수, 25% 이하는 과매도로 판단합니다.
</Typography>
</Alert>
</AccordionDetails>
</Accordion>
{/* 3. MACD 설정 */}
<Accordion defaultExpanded sx={{ mb: 2 }}>
<AccordionSummary expandIcon={<ExpandMore />}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, flex: 1 }}>
<Typography variant="subtitle1" sx={{ fontWeight: 'bold' }}>
3️⃣ MACD (Moving Average Convergence Divergence)
</Typography>
<IconButton
size="small"
onClick={(e) => {
e.stopPropagation();
setMacdHelpOpen(true);
}}
sx={{
color: 'info.main',
'&:hover': { bgcolor: 'info.light', color: 'info.dark' }
}}
>
<HelpOutline fontSize="small" />
</IconButton>
</Box>
</AccordionSummary>
<AccordionDetails>
<Grid container spacing={3}>
<Grid item xs={12} md={4}>
<TextField
fullWidth
label="Fast EMA 기간"
type="number"
value={settings.macdFastPeriod}
onChange={(e) => handleSettingChange({ macdFastPeriod: Number(e.target.value) })}
inputProps={{ min: 1, max: 50 }}
helperText="기본값: 12"
/>
</Grid>
<Grid item xs={12} md={4}>
<TextField
fullWidth
label="Slow EMA 기간"
type="number"
value={settings.macdSlowPeriod}
onChange={(e) => handleSettingChange({ macdSlowPeriod: Number(e.target.value) })}
inputProps={{ min: 1, max: 100 }}
helperText="기본값: 26"
/>
</Grid>
<Grid item xs={12} md={4}>
<TextField
fullWidth
label="Signal 기간"
type="number"
value={settings.macdSignalPeriod}
onChange={(e) => handleSettingChange({ macdSignalPeriod: Number(e.target.value) })}
inputProps={{ min: 1, max: 50 }}
helperText="기본값: 9"
/>
</Grid>
</Grid>
<Divider sx={{ my: 3 }} />
<Box sx={{ mt: 2 }}>
<Typography variant="subtitle2" gutterBottom sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Palette fontSize="small" /> 라인 색상 설정
</Typography>
<Grid container spacing={2} sx={{ mt: 1 }}>
<Grid item xs={12} sm={6} md={3}>
<ColorPicker
label="MACD 라인"
value={localColors.macdLine || '#f44336'}
onChange={(color) => setLocalColors({ ...localColors, macdLine: color })}
/>
</Grid>
<Grid item xs={12} sm={6} md={3}>
<ColorPicker
label="Signal 라인"
value={localColors.signalLine || '#ff9800'}
onChange={(color) => setLocalColors({ ...localColors, signalLine: color })}
/>
</Grid>
<Grid item xs={12} sm={6} md={3}>
<ColorPicker
label="히스토그램 (+)"
value={localColors.histogramPositive || '#4caf50'}
onChange={(color) => setLocalColors({ ...localColors, histogramPositive: color })}
/>
</Grid>
<Grid item xs={12} sm={6} md={3}>
<ColorPicker
label="히스토그램 (-)"
value={localColors.histogramNegative || '#f44336'}
onChange={(color) => setLocalColors({ ...localColors, histogramNegative: color })}
/>
</Grid>
</Grid>
</Box>
</AccordionDetails>
</Accordion>
{/* 4. CCI 설정 */}
<Accordion defaultExpanded={false} sx={{ mb: 2 }}>
<AccordionSummary expandIcon={<ExpandMore />}>
<Typography variant="subtitle1" sx={{ fontWeight: 'bold' }}>
4️⃣ CCI (Commodity Channel Index)
</Typography>
</AccordionSummary>
<AccordionDetails>
<Grid container spacing={3}>
<Grid item xs={12} md={3}>
<TextField
fullWidth
label="CCI 기간"
type="number"
value={settings.cciPeriod}
onChange={(e) => handleSettingChange({ cciPeriod: Number(e.target.value) })}
inputProps={{ min: 5, max: 50 }}
helperText="기본값: 13"
/>
</Grid>
<Grid item xs={12} md={3}>
<TextField
fullWidth
label="Signal 기간"
type="number"
value={settings.cciSignalPeriod}
onChange={(e) => handleSettingChange({ cciSignalPeriod: Number(e.target.value) })}
inputProps={{ min: 1, max: 50 }}
helperText="기본값: 10"
/>
</Grid>
<Grid item xs={12} md={3}>
<TextField
fullWidth
label="과매도 기준"
type="number"
value={settings.cciOversold}
onChange={(e) => handleSettingChange({ cciOversold: Number(e.target.value) })}
inputProps={{ min: -300, max: -50 }}
helperText="기본값: -100"
/>
</Grid>
<Grid item xs={12} md={3}>
<TextField
fullWidth
label="중앙값"
type="number"
value={settings.cciMiddle}
onChange={(e) => handleSettingChange({ cciMiddle: Number(e.target.value) })}
inputProps={{ min: -50, max: 50 }}
helperText="기본값: 0"
/>
</Grid>
<Grid item xs={12} md={3}>
<TextField
fullWidth
label="과매수 기준"
type="number"
value={settings.cciOverbought}
onChange={(e) => handleSettingChange({ cciOverbought: Number(e.target.value) })}
inputProps={{ min: 50, max: 300 }}
helperText="기본값: 100"
/>
</Grid>
</Grid>
<PeriodManager indicatorType="CCI" label="CCI" />
</AccordionDetails>
</Accordion>
{/* 5. ADX 설정 */}
<Accordion defaultExpanded={false} sx={{ mb: 2 }}>
<AccordionSummary expandIcon={<ExpandMore />}>
<Typography variant="subtitle1" sx={{ fontWeight: 'bold' }}>
5️⃣ ADX (Average Directional Index)
</Typography>
</AccordionSummary>
<AccordionDetails>
<Grid container spacing={3}>
<Grid item xs={12} md={6}>
<TextField
fullWidth
label="ADX 기간"
type="number"
value={settings.adxPeriod}
onChange={(e) => handleSettingChange({ adxPeriod: Number(e.target.value) })}
inputProps={{ min: 5, max: 50 }}
helperText="기본값: 11"
/>
</Grid>
<Grid item xs={12} md={6}>
<TextField
fullWidth
label="강한 추세 기준"
type="number"
value={settings.adxStrongThreshold}
onChange={(e) => handleSettingChange({ adxStrongThreshold: Number(e.target.value) })}
inputProps={{ min: 15, max: 50 }}
helperText="기본값: 25"
/>
</Grid>
</Grid>
<Alert severity="info" sx={{ mt: 2 }}>
<Typography variant="body2">
ADX가 강한 추세 기준값 이상일 추세가 강하다고 판단합니다.
</Typography>
</Alert>
<PeriodManager indicatorType="ADX" label="ADX" />
</AccordionDetails>
</Accordion>
{/* 6. BWI 설정 */}
<Accordion defaultExpanded={false} sx={{ mb: 2 }}>
<AccordionSummary expandIcon={<ExpandMore />}>
<Typography variant="subtitle1" sx={{ fontWeight: 'bold' }}>
6️⃣ BWI (Bollinger Band Width Index)
</Typography>
</AccordionSummary>
<AccordionDetails>
<Grid container spacing={3}>
<Grid item xs={12} md={6}>
<TextField
fullWidth
label="기간"
type="number"
value={settings.bwiPeriod}
onChange={(e) => handleSettingChange({ bwiPeriod: Number(e.target.value) })}
inputProps={{ min: 5, max: 50 }}
helperText="기본값: 20"
/>
</Grid>
</Grid>
<Alert severity="info" sx={{ mt: 2 }}>
<Typography variant="body2">
BWI는 볼린저 밴드의 폭을 측정하여 변동성을 나타내는 지표입니다.
값이 클수록 변동성이 높고, 값이 작을수록 변동성이 낮습니다.
</Typography>
</Alert>
</AccordionDetails>
</Accordion>
{/* 7. DMI 설정 */}
<Accordion defaultExpanded={false} sx={{ mb: 2 }}>
<AccordionSummary expandIcon={<ExpandMore />}>
<Typography variant="subtitle1" sx={{ fontWeight: 'bold' }}>
7️⃣ DMI (Directional Movement Index)
</Typography>
</AccordionSummary>
<AccordionDetails>
<Grid container spacing={3}>
<Grid item xs={12} md={6}>
<TextField
fullWidth
label="DMI 기간"
type="number"
value={settings.dmiPeriod}
onChange={(e) => handleSettingChange({ dmiPeriod: Number(e.target.value) })}
inputProps={{ min: 5, max: 50 }}
helperText="기본값: 14"
/>
</Grid>
</Grid>
<Alert severity="info" sx={{ mt: 2 }}>
<Typography variant="body2">
DMI는 +DI와 -DI로 구성되며 추세의 방향을 나타냅니다.
+DI {'>'} -DI 이면 상승 추세, -DI {'>'} +DI 이면 하락 추세입니다.
</Typography>
</Alert>
</AccordionDetails>
</Accordion>
{/* 8. OBV 설정 */}
<Accordion defaultExpanded={false} sx={{ mb: 2 }}>
<AccordionSummary expandIcon={<ExpandMore />}>
<Typography variant="subtitle1" sx={{ fontWeight: 'bold' }}>
8️⃣ OBV (On Balance Volume)
</Typography>
</AccordionSummary>
<AccordionDetails>
<Grid container spacing={3}>
<Grid item xs={12} md={6}>
<TextField
fullWidth
label="이동평균 기간"
type="number"
value={settings.obvPeriod}
onChange={(e) => handleSettingChange({ obvPeriod: Number(e.target.value) })}
inputProps={{ min: 1, max: 50 }}
helperText="기본값: 9"
/>
</Grid>
</Grid>
<Alert severity="info" sx={{ mt: 2 }}>
<Typography variant="body2">
OBV는 거래량의 누적치로 가격 움직임의 선행 지표입니다.
가격이 상승하면 거래량을 더하고, 하락하면 거래량을 뺍니다.
</Typography>
</Alert>
</AccordionDetails>
</Accordion>
{/* 9. RSI 설정 */}
<Accordion defaultExpanded={false} sx={{ mb: 2 }}>
<AccordionSummary expandIcon={<ExpandMore />}>
<Typography variant="subtitle1" sx={{ fontWeight: 'bold' }}>
9️⃣ RSI (Relative Strength Index)
</Typography>
</AccordionSummary>
<AccordionDetails>
<Grid container spacing={3}>
<Grid item xs={12} md={4}>
<TextField
fullWidth
label="RSI 기간"
type="number"
value={settings.rsiPeriod}
onChange={(e) => handleSettingChange({ rsiPeriod: Number(e.target.value) })}
inputProps={{ min: 1, max: 50 }}
helperText="기본값: 9"
/>
</Grid>
<Grid item xs={12} md={4}>
<TextField
fullWidth
label="과매도 기준"
type="number"
value={settings.rsiOversold}
onChange={(e) => handleSettingChange({ rsiOversold: Number(e.target.value) })}
inputProps={{ min: 0, max: 100 }}
helperText="기본값: 30 (매수 신호)"
/>
</Grid>
<Grid item xs={12} md={4}>
<TextField
fullWidth
label="중앙값"
type="number"
value={settings.rsiMiddle}
onChange={(e) => handleSettingChange({ rsiMiddle: Number(e.target.value) })}
inputProps={{ min: 0, max: 100 }}
helperText="기본값: 50"
/>
</Grid>
<Grid item xs={12} md={4}>
<TextField
fullWidth
label="과매수 기준"
type="number"
value={settings.rsiOverbought}
onChange={(e) => handleSettingChange({ rsiOverbought: Number(e.target.value) })}
inputProps={{ min: 0, max: 100 }}
helperText="기본값: 70 (매도 신호)"
/>
</Grid>
</Grid>
<PeriodManager indicatorType="RSI" label="RSI" />
</AccordionDetails>
</Accordion>
{/* 10. Stochastic Slow 설정 */}
<Accordion defaultExpanded sx={{ mb: 2 }}>
<AccordionSummary expandIcon={<ExpandMore />}>
<Typography variant="subtitle1" sx={{ fontWeight: 'bold' }}>
🔟 Stochastic Slow
</Typography>
</AccordionSummary>
<AccordionDetails>
<Typography variant="subtitle2" gutterBottom sx={{ mb: 2, color: 'text.secondary' }}>
기간 설정
</Typography>
<Grid container spacing={3}>
<Grid item xs={12} md={4}>
<TextField
fullWidth
label="기간"
type="number"
value={settings.stochasticKPeriod}
onChange={(e) => handleSettingChange({ stochasticKPeriod: Number(e.target.value) })}
inputProps={{ min: 1, max: 50 }}
helperText="기본값: 12"
/>
</Grid>
<Grid item xs={12} md={4}>
<TextField
fullWidth
label="%K"
type="number"
value={settings.stochasticSlowing}
onChange={(e) => handleSettingChange({ stochasticSlowing: Number(e.target.value) })}
inputProps={{ min: 1, max: 50 }}
helperText="기본값: 5"
/>
</Grid>
<Grid item xs={12} md={4}>
<TextField
fullWidth
label="%D"
type="number"
value={settings.stochasticDPeriod}
onChange={(e) => handleSettingChange({ stochasticDPeriod: Number(e.target.value) })}
inputProps={{ min: 1, max: 50 }}
helperText="기본값: 5"
/>
</Grid>
</Grid>
<Divider sx={{ my: 3 }} />
<Typography variant="subtitle2" gutterBottom sx={{ mb: 2, color: 'text.secondary' }}>
매매 기준값 설정
</Typography>
<Grid container spacing={3}>
<Grid item xs={12} md={4}>
<TextField
fullWidth
label="과매도 기준"
type="number"
value={settings.stochasticOversold}
onChange={(e) => handleSettingChange({ stochasticOversold: Number(e.target.value) })}
inputProps={{ min: 5, max: 40 }}
helperText="기본값: 20 (매수 신호)"
/>
</Grid>
<Grid item xs={12} md={4}>
<TextField
fullWidth
label="중앙값 기준"
type="number"
value={settings.stochasticNeutral}
onChange={(e) => handleSettingChange({ stochasticNeutral: Number(e.target.value) })}
inputProps={{ min: 40, max: 60 }}
helperText="기본값: 50 (중립 구간)"
/>
</Grid>
<Grid item xs={12} md={4}>
<TextField
fullWidth
label="과매수 기준"
type="number"
value={settings.stochasticOverbought}
onChange={(e) => handleSettingChange({ stochasticOverbought: Number(e.target.value) })}
inputProps={{ min: 60, max: 95 }}
helperText="기본값: 80 (매도 신호)"
/>
</Grid>
</Grid>
<Divider sx={{ my: 3 }} />
<Box sx={{ mt: 2 }}>
<Typography variant="subtitle2" gutterBottom sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Palette fontSize="small" /> 라인 색상 설정
</Typography>
<Grid container spacing={2} sx={{ mt: 1 }}>
<Grid item xs={12} sm={6}>
<ColorPicker
label="%K 라인"
value={localColors.stochasticK || '#2196f3'}
onChange={(color) => setLocalColors({ ...localColors, stochasticK: color })}
/>
</Grid>
<Grid item xs={12} sm={6}>
<ColorPicker
label="%D 라인"
value={localColors.stochasticD || '#ff5722'}
onChange={(color) => setLocalColors({ ...localColors, stochasticD: color })}
/>
</Grid>
</Grid>
</Box>
<Box sx={{ mt: 3 }}>
<Typography variant="subtitle2" gutterBottom sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Palette fontSize="small" /> 기준선 색상 설정
</Typography>
<Grid container spacing={2} sx={{ mt: 1 }}>
<Grid item xs={12} sm={4}>
<ColorPicker
label="과매도 기준선"
value={localColors.stochasticOversoldColor || '#4caf50'}
onChange={(color) => setLocalColors({ ...localColors, stochasticOversoldColor: color })}
/>
</Grid>
<Grid item xs={12} sm={4}>
<ColorPicker
label="중앙값 기준선"
value={localColors.stochasticNeutralColor || '#999999'}
onChange={(color) => setLocalColors({ ...localColors, stochasticNeutralColor: color })}
/>
</Grid>
<Grid item xs={12} sm={4}>
<ColorPicker
label="과매수 기준선"
value={localColors.stochasticOverboughtColor || '#ef5350'}
onChange={(color) => setLocalColors({ ...localColors, stochasticOverboughtColor: color })}
/>
</Grid>
</Grid>
</Box>
<PeriodManager indicatorType="STOCHASTIC" label="Stochastic" />
</AccordionDetails>
</Accordion>
{/* 11. Williams %R 설정 */}
<Accordion defaultExpanded={false} sx={{ mb: 2 }}>
<AccordionSummary expandIcon={<ExpandMore />}>
<Typography variant="subtitle1" sx={{ fontWeight: 'bold' }}>
1️⃣1️⃣ Williams %R
</Typography>
</AccordionSummary>
<AccordionDetails>
<Grid container spacing={3}>
<Grid item xs={12} md={3}>
<TextField
fullWidth
label="기간"
type="number"
value={settings.williamsRPeriod}
onChange={(e) => handleSettingChange({ williamsRPeriod: Number(e.target.value) })}
inputProps={{ min: 1, max: 50 }}
helperText="기본값: 11"
/>
</Grid>
<Grid item xs={12} md={3}>
<TextField
fullWidth
label="침체 기준"
type="number"
value={settings.williamsROversold}
onChange={(e) => handleSettingChange({ williamsROversold: Number(e.target.value) })}
inputProps={{ min: -100, max: 0 }}
helperText="기본값: -80"
/>
</Grid>
<Grid item xs={12} md={3}>
<TextField
fullWidth
label="중간"
type="number"
value={settings.williamsRMiddle}
onChange={(e) => handleSettingChange({ williamsRMiddle: Number(e.target.value) })}
inputProps={{ min: -100, max: 0 }}
helperText="기본값: -50"
/>
</Grid>
<Grid item xs={12} md={3}>
<TextField
fullWidth
label="과열 기준"
type="number"
value={settings.williamsROverbought}
onChange={(e) => handleSettingChange({ williamsROverbought: Number(e.target.value) })}
inputProps={{ min: -100, max: 0 }}
helperText="기본값: -20"
/>
</Grid>
</Grid>
{/* 기준선 색상 설정 */}
<Typography variant="subtitle2" sx={{ mt: 3, mb: 2, fontWeight: 'bold' }}>
기준선 스타일 설정
</Typography>
<Grid container spacing={2}>
{/* 침체선 */}
<Grid item xs={12} md={4}>
<Box sx={{ p: 2, border: '1px solid', borderColor: 'divider', borderRadius: 1 }}>
<Typography variant="body2" sx={{ mb: 1, fontWeight: 'bold' }}>침체선 (-80)</Typography>
<Grid container spacing={1}>
<Grid item xs={6}>
<TextField
fullWidth
label="색상"
type="color"
value={colors.williamsROversoldColor}
onChange={(e) => updateColors({ williamsROversoldColor: e.target.value })}
size="small"
/>
</Grid>
<Grid item xs={6}>
<TextField
fullWidth
label="굵기"
type="number"
value={colors.williamsROversoldWidth}
onChange={(e) => updateColors({ williamsROversoldWidth: Number(e.target.value) })}
inputProps={{ min: 0.5, max: 5, step: 0.5 }}
size="small"
/>
</Grid>
</Grid>
</Box>
</Grid>
{/* 중간선 */}
<Grid item xs={12} md={4}>
<Box sx={{ p: 2, border: '1px solid', borderColor: 'divider', borderRadius: 1 }}>
<Typography variant="body2" sx={{ mb: 1, fontWeight: 'bold' }}>중간선 (-50)</Typography>
<Grid container spacing={1}>
<Grid item xs={6}>
<TextField
fullWidth
label="색상"
type="color"
value={colors.williamsRMiddleColor}
onChange={(e) => updateColors({ williamsRMiddleColor: e.target.value })}
size="small"
/>
</Grid>
<Grid item xs={6}>
<TextField
fullWidth
label="굵기"
type="number"
value={colors.williamsRMiddleWidth}
onChange={(e) => updateColors({ williamsRMiddleWidth: Number(e.target.value) })}
inputProps={{ min: 0.5, max: 5, step: 0.5 }}
size="small"
/>
</Grid>
</Grid>
</Box>
</Grid>
{/* 과열선 */}
<Grid item xs={12} md={4}>
<Box sx={{ p: 2, border: '1px solid', borderColor: 'divider', borderRadius: 1 }}>
<Typography variant="body2" sx={{ mb: 1, fontWeight: 'bold' }}>과열선 (-20)</Typography>
<Grid container spacing={1}>
<Grid item xs={6}>
<TextField
fullWidth
label="색상"
type="color"
value={colors.williamsROverboughtColor}
onChange={(e) => updateColors({ williamsROverboughtColor: e.target.value })}
size="small"
/>
</Grid>
<Grid item xs={6}>
<TextField
fullWidth
label="굵기"
type="number"
value={colors.williamsROverboughtWidth}
onChange={(e) => updateColors({ williamsROverboughtWidth: Number(e.target.value) })}
inputProps={{ min: 0.5, max: 5, step: 0.5 }}
size="small"
/>
</Grid>
</Grid>
</Box>
</Grid>
</Grid>
<Alert severity="info" sx={{ mt: 2 }}>
<Typography variant="body2">
Williams %R은 0에서 -100 사이의 값을 가집니다.
-80 이하는 침체, -20 이상은 과열로 판단합니다.
</Typography>
</Alert>
</AccordionDetails>
</Accordion>
{/* 12. TRIX 설정 */}
<Accordion defaultExpanded={false} sx={{ mb: 2 }}>
<AccordionSummary expandIcon={<ExpandMore />}>
<Typography variant="subtitle1" sx={{ fontWeight: 'bold' }}>
1️⃣2️⃣ TRIX (Triple Exponential Moving Average)
</Typography>
</AccordionSummary>
<AccordionDetails>
<Grid container spacing={3}>
<Grid item xs={12} md={6}>
<TextField
fullWidth
label="TRIX 기간"
type="number"
value={settings.trixPeriod}
onChange={(e) => handleSettingChange({ trixPeriod: Number(e.target.value) })}
inputProps={{ min: 5, max: 50 }}
helperText="기본값: 12"
/>
</Grid>
<Grid item xs={12} md={6}>
<TextField
fullWidth
label="Signal 기간"
type="number"
value={settings.trixSignalPeriod}
onChange={(e) => handleSettingChange({ trixSignalPeriod: Number(e.target.value) })}
inputProps={{ min: 1, max: 20 }}
helperText="기본값: 9"
/>
</Grid>
<Grid item xs={12} md={4}>
<TextField
fullWidth
label="과매도 기준"
type="number"
value={settings.trixOversold}
onChange={(e) => handleSettingChange({ trixOversold: Number(e.target.value) })}
inputProps={{ min: -2, max: -0.1, step: 0.1 }}
helperText="기본값: -0.5"
/>
</Grid>
<Grid item xs={12} md={4}>
<TextField
fullWidth
label="중앙값"
type="number"
value={settings.trixMiddle}
onChange={(e) => handleSettingChange({ trixMiddle: Number(e.target.value) })}
inputProps={{ min: -0.5, max: 0.5, step: 0.1 }}
helperText="기본값: 0"
/>
</Grid>
<Grid item xs={12} md={4}>
<TextField
fullWidth
label="과매수 기준"
type="number"
value={settings.trixOverbought}
onChange={(e) => handleSettingChange({ trixOverbought: Number(e.target.value) })}
inputProps={{ min: 0.1, max: 2, step: 0.1 }}
helperText="기본값: 0.5"
/>
</Grid>
</Grid>
<Alert severity="info" sx={{ mt: 2 }}>
<Typography variant="body2">
TRIX는 EMA의 EMA의 EMA를 사용하여 노이즈를 필터링한 모멘텀 지표입니다.
0 상향 돌파 매수, 하향 돌파 매도 신호로 해석합니다.
</Typography>
</Alert>
<PeriodManager indicatorType="TRIX" label="TRIX" />
</AccordionDetails>
</Accordion>
{/* 13. VolumeOSC 설정 */}
<Accordion defaultExpanded={false} sx={{ mb: 2 }}>
<AccordionSummary expandIcon={<ExpandMore />}>
<Typography variant="subtitle1" sx={{ fontWeight: 'bold' }}>
1️⃣3️⃣ VolumeOSC (Volume Oscillator)
</Typography>
</AccordionSummary>
<AccordionDetails>
<Grid container spacing={3}>
<Grid item xs={12} md={6}>
<TextField
fullWidth
label="단기 기간"
type="number"
value={settings.volumeOscShortPeriod}
onChange={(e) => handleSettingChange({ volumeOscShortPeriod: Number(e.target.value) })}
inputProps={{ min: 1, max: 50 }}
helperText="기본값: 10"
/>
</Grid>
<Grid item xs={12} md={6}>
<TextField
fullWidth
label="장기 기간"
type="number"
value={settings.volumeOscLongPeriod}
onChange={(e) => handleSettingChange({ volumeOscLongPeriod: Number(e.target.value) })}
inputProps={{ min: 10, max: 100 }}
helperText="기본값: 20"
/>
</Grid>
</Grid>
<Alert severity="info" sx={{ mt: 2 }}>
<Typography variant="body2">
VolumeOSC는 단기 거래량 이동평균과 장기 거래량 이동평균의 차이를 백분율로 나타냅니다.
양수면 거래량 증가 추세, 음수면 거래량 감소 추세입니다.
</Typography>
</Alert>
</AccordionDetails>
</Accordion>
{/* 14. VR 설정 */}
<Accordion defaultExpanded={false} sx={{ mb: 2 }}>
<AccordionSummary expandIcon={<ExpandMore />}>
<Typography variant="subtitle1" sx={{ fontWeight: 'bold' }}>
1️⃣4️⃣ VR (Volume Ratio)
</Typography>
</AccordionSummary>
<AccordionDetails>
<Grid container spacing={3}>
<Grid item xs={12} md={6}>
<TextField
fullWidth
label="기간"
type="number"
value={settings.vrPeriod}
onChange={(e) => handleSettingChange({ vrPeriod: Number(e.target.value) })}
inputProps={{ min: 5, max: 50 }}
helperText="기본값: 10"
/>
</Grid>
</Grid>
<Alert severity="info" sx={{ mt: 2 }}>
<Typography variant="body2">
VR은 상승일 거래량과 하락일 거래량의 비율입니다.
일반적으로 70% 이하는 과매도, 250% 이상은 과매수로 판단합니다.
</Typography>
</Alert>
</AccordionDetails>
</Accordion>
{/* 15. 이격도 설정 */}
<Accordion defaultExpanded={false} sx={{ mb: 2 }}>
<AccordionSummary expandIcon={<ExpandMore />}>
<Typography variant="subtitle1" sx={{ fontWeight: 'bold' }}>
1️⃣5️⃣ 이격도 (Disparity Index)
</Typography>
</AccordionSummary>
<AccordionDetails>
{/* 각 선 활성화 스위치 */}
<Typography variant="subtitle2" sx={{ mb: 2, fontWeight: 'bold' }}>
표시 설정
</Typography>
<Grid container spacing={2} sx={{ mb: 3 }}>
<Grid item xs={6} md={3}>
<FormControlLabel
control={
<Switch
checked={settings.disparityUltraShortEnabled ?? true}
onChange={(e) => handleSettingChange({ disparityUltraShortEnabled: e.target.checked })}
/>
}
label="초단기 (5일)"
/>
</Grid>
<Grid item xs={6} md={3}>
<FormControlLabel
control={
<Switch
checked={settings.disparityShortEnabled ?? true}
onChange={(e) => handleSettingChange({ disparityShortEnabled: e.target.checked })}
/>
}
label="단기 (20일)"
/>
</Grid>
<Grid item xs={6} md={3}>
<FormControlLabel
control={
<Switch
checked={settings.disparityMidEnabled ?? true}
onChange={(e) => handleSettingChange({ disparityMidEnabled: e.target.checked })}
/>
}
label="중기 (60일)"
/>
</Grid>
<Grid item xs={6} md={3}>
<FormControlLabel
control={
<Switch
checked={settings.disparityLongEnabled ?? true}
onChange={(e) => handleSettingChange({ disparityLongEnabled: e.target.checked })}
/>
}
label="장기 (120일)"
/>
</Grid>
</Grid>
<Divider sx={{ my: 2 }} />
{/* 기간 설정 */}
<Typography variant="subtitle2" sx={{ mb: 2, fontWeight: 'bold' }}>
기간 설정
</Typography>
<Grid container spacing={3}>
{/* 초단기 (5일) */}
<Grid item xs={12} md={3}>
<TextField
fullWidth
label="초단기 기간"
type="number"
value={settings.disparityUltraShortPeriod}
onChange={(e) => handleSettingChange({ disparityUltraShortPeriod: Number(e.target.value) })}
inputProps={{ min: 1, max: 20 }}
helperText="기본값: 5일"
disabled={!settings.disparityUltraShortEnabled}
/>
</Grid>
{/* 단기 (20일) */}
<Grid item xs={12} md={3}>
<TextField
fullWidth
label="단기 기간"
type="number"
value={settings.disparityShortPeriod}
onChange={(e) => handleSettingChange({ disparityShortPeriod: Number(e.target.value) })}
inputProps={{ min: 10, max: 50 }}
helperText="기본값: 20일"
disabled={!settings.disparityShortEnabled}
/>
</Grid>
{/* 중기 (60일) */}
<Grid item xs={12} md={3}>
<TextField
fullWidth
label="중기 기간"
type="number"
value={settings.disparityMidPeriod}
onChange={(e) => handleSettingChange({ disparityMidPeriod: Number(e.target.value) })}
inputProps={{ min: 30, max: 100 }}
helperText="기본값: 60일"
disabled={!settings.disparityMidEnabled}
/>
</Grid>
{/* 장기 (120일) */}
<Grid item xs={12} md={3}>
<TextField
fullWidth
label="장기 기간"
type="number"
value={settings.disparityLongPeriod}
onChange={(e) => handleSettingChange({ disparityLongPeriod: Number(e.target.value) })}
inputProps={{ min: 60, max: 200 }}
helperText="기본값: 120일"
disabled={!settings.disparityLongEnabled}
/>
</Grid>
</Grid>
<Alert severity="info" sx={{ mt: 2 }}>
<Typography variant="body2">
이격도는 현재 주가와 이동평균선 사이의 괴리율을 나타내는 지표입니다.
100 기준으로 100 이상이면 주가가 이동평균선보다 위에, 100 이하면 아래에 있습니다.
초단기(5), 단기(20), 중기(60), 장기(120) 4개의 이격도를 동시에 표시합니다.
</Typography>
</Alert>
{/* 기준선 설정 */}
<Typography variant="subtitle2" sx={{ mt: 3, mb: 2, fontWeight: 'bold' }}>
기준선 스타일 설정
</Typography>
<Grid container spacing={2}>
<Grid item xs={12} md={12}>
<Box sx={{ p: 2, border: '1px solid', borderColor: 'divider', borderRadius: 1 }}>
<FormControlLabel
control={
<Switch
checked={settings.disparityBaseLineEnabled ?? true}
onChange={(e) => handleSettingChange({ disparityBaseLineEnabled: e.target.checked })}
/>
}
label="기준선 표시 (100)"
sx={{ mb: 2 }}
/>
<Grid container spacing={2}>
<Grid item xs={12} md={4}>
<TextField
fullWidth
label="색상"
type="color"
value={colors.disparityBaseLineColor}
onChange={(e) => updateColors({ disparityBaseLineColor: e.target.value })}
size="small"
disabled={!settings.disparityBaseLineEnabled}
/>
</Grid>
<Grid item xs={12} md={4}>
<TextField
fullWidth
label="굵기"
type="number"
value={colors.disparityBaseLineWidth}
onChange={(e) => updateColors({ disparityBaseLineWidth: Number(e.target.value) })}
inputProps={{ min: 0.5, max: 5, step: 0.5 }}
size="small"
disabled={!settings.disparityBaseLineEnabled}
/>
</Grid>
<Grid item xs={12} md={4}>
<FormControl fullWidth size="small" disabled={!settings.disparityBaseLineEnabled}>
<InputLabel> 스타일</InputLabel>
<Select
value={colors.disparityBaseLineStyle}
label="선 스타일"
onChange={(e) => updateColors({ disparityBaseLineStyle: e.target.value as 'solid' | 'dashed' | 'dotted' })}
>
<MenuItem value="solid">실선</MenuItem>
<MenuItem value="dashed">파선</MenuItem>
<MenuItem value="dotted">점선</MenuItem>
</Select>
</FormControl>
</Grid>
</Grid>
</Box>
</Grid>
</Grid>
<PeriodManager indicatorType="DISPARITY" label="이격도" />
</AccordionDetails>
</Accordion>
{/* 이동평균 기간 관리 섹션 */}
<Accordion defaultExpanded={false} sx={{ mb: 2 }}>
<AccordionSummary expandIcon={<ExpandMore />}>
<Typography variant="subtitle1" sx={{ fontWeight: 'bold' }}>
📊 이동평균 기간 관리 (전략 규칙용)
</Typography>
</AccordionSummary>
<AccordionDetails>
<Alert severity="info" sx={{ mb: 2 }}>
<Typography variant="body2">
전략 규칙 생성 선택 가능한 이동평균 기간을 관리합니다.
</Typography>
</Alert>
<Grid container spacing={2}>
<Grid item xs={12} md={6}>
<PeriodManager indicatorType="MA" label="단순이동평균 (SMA)" />
</Grid>
<Grid item xs={12} md={6}>
<PeriodManager indicatorType="EMA" label="지수이동평균 (EMA)" />
</Grid>
</Grid>
<Box sx={{ mt: 2 }}>
<PeriodManager indicatorType="VOLUME_MA" label="거래량 이동평균" />
</Box>
</AccordionDetails>
</Accordion>
<Divider sx={{ my: 3 }} />
<Box sx={{ display: 'flex', justifyContent: 'flex-end', gap: 2 }}>
<Button variant="contained" onClick={handleSave} size="large">
저장
</Button>
</Box>
{/* 저장 결과 팝업 */}
<Dialog open={resultDialogOpen} onClose={() => setResultDialogOpen(false)} maxWidth="sm" fullWidth>
<DialogTitle sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
bgcolor: '#1976d2',
color: '#fff',
py: 1.2,
}}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
{saveResult.success ? <CheckCircle sx={{ color: '#fff' }} /> : <Error sx={{ color: '#fff' }} />}
<Typography variant="h6" sx={{ fontWeight: 600, fontSize: '16px', color: '#fff' }}>
{saveResult.success ? '저장 완료' : '저장 실패'}
</Typography>
</Box>
<IconButton onClick={() => setResultDialogOpen(false)} size="small" sx={{ color: '#fff' }}>
<Close />
</IconButton>
</DialogTitle>
<DialogContent sx={{ mt: 2 }}>
<Typography variant="body1" sx={{ whiteSpace: 'pre-line' }}>
{saveResult.message}
</Typography>
</DialogContent>
<DialogActions>
<Button onClick={() => setResultDialogOpen(false)} variant="contained" autoFocus>
확인
</Button>
</DialogActions>
</Dialog>
{/* MACD 상세 설명 다이얼로그 */}
<Dialog
open={macdHelpOpen}
onClose={() => setMacdHelpOpen(false)}
maxWidth="md"
fullWidth
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 }}>
<HelpOutline />
<Typography variant="h6" sx={{ fontWeight: 600 }}>
MACD 상세 설명
</Typography>
</Box>
<IconButton onClick={() => setMacdHelpOpen(false)} size="small" sx={{ color: 'white' }}>
<ExpandMore sx={{ transform: 'rotate(180deg)' }} />
</IconButton>
</DialogTitle>
<DialogContent sx={{ pt: 3, pb: 3, maxHeight: '65vh', overflowY: 'auto' }}>
<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>
</DialogContent>
<DialogActions sx={{ p: 2, borderTop: '1px solid', borderColor: 'divider' }}>
<Button onClick={() => setMacdHelpOpen(false)} variant="contained" color="primary" fullWidth>
확인
</Button>
</DialogActions>
</Dialog>
</Box>
);
};
export default IndicatorSettingsTab;