/** 추세검색 조건 정의 — 정배열 / 상승세 / 거래량 / 보조지표 */ export type TrendConditionMode = 'filter'; export type TrendCategoryKey = | 'maAlignEnabled' | 'trendEnabled' | 'volumePowerEnabled' | 'indicatorEnabled'; export interface TrendConditionDef { id: string; category: 'ma' | 'trend' | 'volume' | 'indicator'; categoryLabel: string; label: string; desc: string; mode: TrendConditionMode; requestKey: keyof import('./backendApi').TrendSearchRequest; categoryKey: TrendCategoryKey; /** 조건 파라미터 UI */ params?: 'maConvergencePct' | 'ma20SlopeBars' | 'newHighBreakout' | 'volumeVsPrior' | 'minTurnover' | 'rsiBand' | 'adxMin'; } export const TREND_CONDITION_DEFS: TrendConditionDef[] = [ // Ⅰ. 정배열 { id: 'priceAboveMa', category: 'ma', categoryLabel: 'Ⅰ. 정배열', label: '주가 > 5·20·60 이평', desc: '종가가 5·20·60일 이동평균선 위', mode: 'filter', requestKey: 'priceAboveMa', categoryKey: 'maAlignEnabled', }, { id: 'maAlignment', category: 'ma', categoryLabel: 'Ⅰ. 정배열', label: '5 > 20 > 60 > 120 정배열', desc: '단기 이평이 장기 이평 위에 배열', mode: 'filter', requestKey: 'maAlignment', categoryKey: 'maAlignEnabled', }, { id: 'maConvergence', category: 'ma', categoryLabel: 'Ⅰ. 정배열', label: '이평밀집도 수렴 (선택)', desc: '5·20·60 이평 간격이 N% 이내 (확산 전 포착)', mode: 'filter', requestKey: 'maConvergence', categoryKey: 'maAlignEnabled', params: 'maConvergencePct', }, // Ⅱ. 상승세 { id: 'ma20SlopeUp', category: 'trend', categoryLabel: 'Ⅱ. 상승세', label: '20일 이평 연속 상승', desc: '20일선(생명선) N봉 연속 우상향', mode: 'filter', requestKey: 'ma20SlopeUp', categoryKey: 'trendEnabled', params: 'ma20SlopeBars', }, { id: 'newHighBreakout', category: 'trend', categoryLabel: 'Ⅱ. 상승세', label: '신고가 돌파·근접', desc: 'N일 고가 돌파 또는 고가 대비 -M% 이내', mode: 'filter', requestKey: 'newHighBreakout', categoryKey: 'trendEnabled', params: 'newHighBreakout', }, { id: 'ichimokuAboveCloud', category: 'trend', categoryLabel: 'Ⅱ. 상승세', label: '일목 구름대 상단', desc: '주가 > 선행스팬1 · 선행스팬2', mode: 'filter', requestKey: 'ichimokuAboveCloud', categoryKey: 'trendEnabled', }, // Ⅲ. 돈과 힘 { id: 'volumeVsPrior200', category: 'volume', categoryLabel: 'Ⅲ. 돈과 힘', label: '직전 봉 대비 거래량', desc: '일봉=전일, 분봉=직전 봉 대비 거래량 %', mode: 'filter', requestKey: 'volumeVsPrior200', categoryKey: 'volumePowerEnabled', params: 'volumeVsPrior', }, { id: 'minTurnover5dAvg', category: 'volume', categoryLabel: 'Ⅲ. 돈과 힘', label: '5일 평균 거래대금', desc: '최근 5일 평균 거래대금 하한 (억 원)', mode: 'filter', requestKey: 'minTurnover5dAvg', categoryKey: 'volumePowerEnabled', params: 'minTurnover', }, { id: 'volMa5Over20', category: 'volume', categoryLabel: 'Ⅲ. 돈과 힘', label: '거래량 이평 정배열', desc: '5일 거래량 이평 > 20일 거래량 이평', mode: 'filter', requestKey: 'volMa5Over20', categoryKey: 'volumePowerEnabled', }, // Ⅳ. 보조지표 { id: 'macdGoldenCross', category: 'indicator', categoryLabel: 'Ⅳ. 보조지표', label: 'MACD 골든크로스', desc: 'MACD Line > Signal 또는 MACD > 0', mode: 'filter', requestKey: 'macdGoldenCross', categoryKey: 'indicatorEnabled', }, { id: 'rsiBand', category: 'indicator', categoryLabel: 'Ⅳ. 보조지표', label: 'RSI 구간', desc: '과열·침체 구간 제외 (기본 50~70)', mode: 'filter', requestKey: 'rsiBand', categoryKey: 'indicatorEnabled', params: 'rsiBand', }, { id: 'dmiBullish', category: 'indicator', categoryLabel: 'Ⅳ. 보조지표', label: '+DI > -DI · ADX', desc: '추세 강도 ADX 하한 이상', mode: 'filter', requestKey: 'dmiBullish', categoryKey: 'indicatorEnabled', params: 'adxMin', }, ]; export const TREND_CATEGORY_ORDER: { key: TrendCategoryKey; label: string; cat: TrendConditionDef['category']; en: string; }[] = [ { key: 'maAlignEnabled', label: 'Ⅰ. 정배열', cat: 'ma', en: 'Moving Average Alignment' }, { key: 'trendEnabled', label: 'Ⅱ. 상승세', cat: 'trend', en: 'Trend & Momentum' }, { key: 'volumePowerEnabled', label: 'Ⅲ. 돈과 힘', cat: 'volume', en: 'Volume & Liquidity' }, { key: 'indicatorEnabled', label: 'Ⅳ. 보조지표', cat: 'indicator', en: 'Indicators' }, ]; /** 카테고리 OFF 시 하위 조건까지 함께 끄기 위한 패치 */ export function buildCategoryTogglePatch( catKey: TrendCategoryKey, on: boolean, ): Partial { const patch: Partial = { [catKey]: on }; if (!on) { for (const def of TREND_CONDITION_DEFS.filter(d => d.categoryKey === catKey)) { patch[def.requestKey] = false as never; } } return patch; } export function countActiveTrendFilters( filters: import('./backendApi').TrendSearchRequest, ): number { let n = 0; for (const def of TREND_CONDITION_DEFS) { if (filters[def.categoryKey] && filters[def.requestKey]) n++; } return n; }