추세검색 설정 추가

This commit is contained in:
Macbook
2026-05-27 01:36:06 +09:00
parent c1bcf88c6c
commit 2e08c6b16f
42 changed files with 1507 additions and 226 deletions
@@ -0,0 +1,128 @@
/**
* 추세검색 앱 설정 (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';
export interface TrendSearchAppSettings {
weightMaAlignment: number;
weightMaSlope: number;
weightAdxTrend: number;
weightMacdMomentum: number;
weightPricePosition: number;
minTrendScore: number;
/** 결과 목록 최대 개수 */
limit: number;
/** 스캔 대상 종목 수 (거래대금 상위) */
scanLimit: number;
/** 자동 갱신 간격(초) */
autoRefreshSeconds: number;
/** 검색 후 상위 N위까지 투자대상 자동추가 */
autoAddTargetsEnabled: boolean;
autoAddTopRank: number;
/** 자동추가 시 추세점수(matchRate) 하한 */
autoAddMinScore: number;
}
export const DEFAULT_TREND_SEARCH_APP_SETTINGS: TrendSearchAppSettings = {
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,
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);
return {
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),
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}`;
}