/** * 추세검색 앱 설정 (gc_app_settings.trend_search_settings_json) */ import type { TrendSearchRequest } from './backendApi'; import { DEFAULT_TREND_SEARCH_REQUEST } from './backendApi'; import { BULLISH_WEIGHT_ITEMS, DEFAULT_BULLISH_WEIGHTS, type BullishWeightKey, } from './trendSearchBullishWeights'; /** 결과 목록 표시: chart | summary(신호) */ export type TrendSearchDisplayMode = 'chart' | 'summary'; export interface TrendSearchAppSettings { /** 결과 UI 표시 모드 (기본 summary) */ displayMode?: TrendSearchDisplayMode; weightMaAlignment: number; weightMaSlope: number; weightAdxTrend: number; weightMacdMomentum: number; weightPricePosition: number; minTrendScore: number; /** 결과 목록 최대 개수 */ limit: number; /** 스캔 대상 종목 수 (거래대금 상위) */ scanLimit: number; /** 자동 갱신 ON (백엔드 스케줄러·화면 폴링) */ autoRefreshEnabled: boolean; /** 자동 갱신 간격(초) */ autoRefreshSeconds: number; /** 검색 후 상위 N위까지 투자대상 자동추가 */ autoAddTargetsEnabled: boolean; autoAddTopRank: number; /** 자동추가 시 추세점수(matchRate) 하한 */ autoAddMinScore: number; } export const DEFAULT_TREND_SEARCH_APP_SETTINGS: TrendSearchAppSettings = { displayMode: 'summary', weightMaAlignment: DEFAULT_BULLISH_WEIGHTS.weightMaAlignment, weightMaSlope: DEFAULT_BULLISH_WEIGHTS.weightMaSlope, weightAdxTrend: DEFAULT_BULLISH_WEIGHTS.weightAdxTrend, weightMacdMomentum: DEFAULT_BULLISH_WEIGHTS.weightMacdMomentum, weightPricePosition: DEFAULT_BULLISH_WEIGHTS.weightPricePosition, minTrendScore: DEFAULT_TREND_SEARCH_REQUEST.minTrendScore, limit: DEFAULT_TREND_SEARCH_REQUEST.limit, scanLimit: DEFAULT_TREND_SEARCH_REQUEST.scanLimit, autoRefreshEnabled: true, autoRefreshSeconds: 3, autoAddTargetsEnabled: false, autoAddTopRank: 5, autoAddMinScore: DEFAULT_TREND_SEARCH_REQUEST.minTrendScore, }; /** 자동 갱신 간격 선택지 (초) */ export const TREND_SEARCH_REFRESH_OPTIONS = [3, 5, 10, 30, 60] as const; export function resolveTrendSearchAppSettings( raw?: Partial | null, ): TrendSearchAppSettings { const d = DEFAULT_TREND_SEARCH_APP_SETTINGS; if (!raw || typeof raw !== 'object') return { ...d }; const sec = Number(raw.autoRefreshSeconds); const rank = Number(raw.autoAddTopRank); const limit = Number(raw.limit); const scanLimit = Number(raw.scanLimit); const displayMode = raw.displayMode === 'chart' || raw.displayMode === 'summary' ? raw.displayMode : d.displayMode ?? 'summary'; return { displayMode, weightMaAlignment: clampWeight(raw.weightMaAlignment, d.weightMaAlignment), weightMaSlope: clampWeight(raw.weightMaSlope, d.weightMaSlope), weightAdxTrend: clampWeight(raw.weightAdxTrend, d.weightAdxTrend), weightMacdMomentum: clampWeight(raw.weightMacdMomentum, d.weightMacdMomentum), weightPricePosition: clampWeight(raw.weightPricePosition, d.weightPricePosition), minTrendScore: clampInt(raw.minTrendScore, 0, 100, d.minTrendScore), limit: clampInt(limit, 5, 50, d.limit), scanLimit: clampInt(scanLimit, 20, 120, d.scanLimit), autoRefreshEnabled: raw.autoRefreshEnabled !== false, autoRefreshSeconds: TREND_SEARCH_REFRESH_OPTIONS.includes(sec as typeof TREND_SEARCH_REFRESH_OPTIONS[number]) ? sec : d.autoRefreshSeconds, autoAddTargetsEnabled: raw.autoAddTargetsEnabled === true, autoAddTopRank: clampInt(rank, 1, 50, d.autoAddTopRank), autoAddMinScore: clampInt(raw.autoAddMinScore, 0, 100, d.autoAddMinScore), }; } function clampWeight(v: unknown, fallback: number): number { const n = Number(v); if (!Number.isFinite(n)) return fallback; return Math.max(0, Math.min(100, Math.round(n / 5) * 5)); } function clampInt(v: unknown, min: number, max: number, fallback: number): number { const n = Number(v); if (!Number.isFinite(n)) return fallback; return Math.min(max, Math.max(min, Math.floor(n))); } export function mergeTrendSearchRequest( base: TrendSearchRequest, settings: TrendSearchAppSettings, ): TrendSearchRequest { return { ...base, weightMaAlignment: settings.weightMaAlignment, weightMaSlope: settings.weightMaSlope, weightAdxTrend: settings.weightAdxTrend, weightMacdMomentum: settings.weightMacdMomentum, weightPricePosition: settings.weightPricePosition, minTrendScore: settings.minTrendScore, limit: settings.limit, scanLimit: settings.scanLimit, }; } /** 검색 요청 → 앱 설정에 반영할 필드 */ export function extractTrendSearchAppSettings( req: Pick, ): Pick< TrendSearchAppSettings, BullishWeightKey | 'minTrendScore' | 'limit' | 'scanLimit' > { return { weightMaAlignment: req.weightMaAlignment, weightMaSlope: req.weightMaSlope, weightAdxTrend: req.weightAdxTrend, weightMacdMomentum: req.weightMacdMomentum, weightPricePosition: req.weightPricePosition, minTrendScore: req.minTrendScore, limit: req.limit, scanLimit: req.scanLimit, }; } export function formatTrendRefreshLabel(seconds: number): string { if (seconds < 60) return `${seconds}초`; return `${seconds / 60}분`; } /** 설정 변경 감지용 (useEffect 의존성) */ export function trendSearchSettingsSignature(s: TrendSearchAppSettings): string { return JSON.stringify(s); } /** 캐시/설정 → 추세검색 조건 패널 필드 병합 (timeframe 등 화면 전용 값 유지) */ export function mergeFiltersFromTrendSearchSettings( filters: TrendSearchRequest, settings: TrendSearchAppSettings, ): TrendSearchRequest { return mergeTrendSearchRequest(filters, settings); } /** 추세검색 조건 변경 → 저장할 전체 추세설정 */ export function mergeTrendSearchSettingsFromRequest( current: TrendSearchAppSettings, req: Pick, ): TrendSearchAppSettings { return resolveTrendSearchAppSettings({ ...current, ...extractTrendSearchAppSettings(req), }); }