Files
goldenChart/frontend/src/utils/trendSearchAppSettings.ts
T
2026-05-28 14:44:19 +09:00

167 lines
6.0 KiB
TypeScript

/**
* 추세검색 앱 설정 (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<TrendSearchAppSettings> | 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<TrendSearchRequest, BullishWeightKey | 'minTrendScore' | 'limit' | 'scanLimit'>,
): 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<TrendSearchRequest, BullishWeightKey | 'minTrendScore' | 'limit' | 'scanLimit'>,
): TrendSearchAppSettings {
return resolveTrendSearchAppSettings({
...current,
...extractTrendSearchAppSettings(req),
});
}