추세검색 설정 추가
This commit is contained in:
@@ -418,6 +418,8 @@ export interface AppSettingsDto {
|
||||
paperMinOrderKrw?: number;
|
||||
paperAutoTradeEnabled?: boolean;
|
||||
paperAutoTradeBudgetPct?: number;
|
||||
/** 가상매매 투자대상 목록 최대 종목 수 */
|
||||
virtualTargetMaxCount?: number;
|
||||
/** PAPER | LIVE | BOTH */
|
||||
tradingMode?: string;
|
||||
liveAutoTradeEnabled?: boolean;
|
||||
@@ -431,6 +433,8 @@ export interface AppSettingsDto {
|
||||
fcmPushEnabled?: boolean;
|
||||
/** 차트·UI 표시 시간대 (IANA, 예: Asia/Seoul) */
|
||||
displayTimezone?: string;
|
||||
/** 추세검색 기본 설정 */
|
||||
trendSearchSettings?: import('./trendSearchAppSettings').TrendSearchAppSettings | null;
|
||||
}
|
||||
|
||||
export interface LiveSummaryDto {
|
||||
@@ -1025,6 +1029,8 @@ export interface LiveStrategySettingsDto {
|
||||
skipWatchlistSync?: boolean;
|
||||
/** true면 gc_app_settings 전역 템플릿 갱신 생략 */
|
||||
skipGlobalTemplate?: boolean;
|
||||
/** 가상투자 투자대상 고정 — ON 이면 목록에서 삭제 불가 */
|
||||
isPinned?: boolean;
|
||||
}
|
||||
|
||||
/** 실시간 전략 체크 설정 로드 */
|
||||
|
||||
@@ -9,14 +9,14 @@ export type TopMenuId = MenuPage;
|
||||
|
||||
export type SettingsCategoryId =
|
||||
| 'general' | 'chart' | 'indicators' | 'backtest' | 'strategy'
|
||||
| 'paper' | 'alert' | 'network' | 'admin';
|
||||
| 'paper' | 'trend-search' | 'alert' | 'network' | 'admin';
|
||||
|
||||
export const TOP_MENU_IDS: TopMenuId[] = [
|
||||
'dashboard', 'chart', 'virtual', 'trend-search', 'strategy-editor', 'backtest', 'settings',
|
||||
];
|
||||
|
||||
export const SETTINGS_MENU_IDS: SettingsCategoryId[] = [
|
||||
'general', 'chart', 'indicators', 'backtest', 'strategy', 'paper', 'alert', 'network', 'admin',
|
||||
'general', 'chart', 'indicators', 'backtest', 'strategy', 'paper', 'trend-search', 'alert', 'network', 'admin',
|
||||
];
|
||||
|
||||
export const ALL_MENU_IDS = [
|
||||
@@ -42,7 +42,8 @@ export const MENU_LABELS: Record<string, string> = {
|
||||
settings_indicators: '설정 · 보조지표',
|
||||
settings_backtest: '설정 · 백테스팅',
|
||||
settings_strategy: '설정 · 전략',
|
||||
settings_paper: '설정 · 가상투자',
|
||||
settings_paper: '설정 · 가상매매',
|
||||
'settings_trend-search': '설정 · 추세검색',
|
||||
settings_alert: '설정 · 알림',
|
||||
settings_network: '설정 · 네트워크',
|
||||
settings_admin: '설정 · 관리자',
|
||||
|
||||
@@ -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}분`;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { sortTrendSearchByStrength } from './trendSearchMetrics';
|
||||
import { resolveVirtualTargetNames } from './virtualTargetNames';
|
||||
import type { TrendSearchResultDto } from './backendApi';
|
||||
import type { TrendSearchAppSettings } from './trendSearchAppSettings';
|
||||
import { addVirtualTarget } from './virtualTargetMutations';
|
||||
import { loadVirtualTargets } from './virtualTradingStorage';
|
||||
import { isVirtualTargetAddAllowed } from './virtualTargetLimits';
|
||||
import { getVirtualTargetMaxCount } from '../hooks/useAppSettings';
|
||||
|
||||
/** 추세검색 결과 상위 순위 → 가상매매 투자대상 자동추가 */
|
||||
export async function autoAddTrendSearchTargets(
|
||||
results: TrendSearchResultDto[],
|
||||
settings: Pick<TrendSearchAppSettings, 'autoAddTargetsEnabled' | 'autoAddTopRank' | 'autoAddMinScore'>,
|
||||
timeframe: string,
|
||||
): Promise<void> {
|
||||
if (!settings.autoAddTargetsEnabled || results.length === 0) return;
|
||||
|
||||
const sorted = sortTrendSearchByStrength(results);
|
||||
const topN = Math.max(1, Math.min(50, settings.autoAddTopRank));
|
||||
const minScore = Math.max(0, Math.min(100, settings.autoAddMinScore));
|
||||
const candidates = sorted
|
||||
.slice(0, topN)
|
||||
.filter(r => r.matchRate >= minScore);
|
||||
|
||||
let targets = loadVirtualTargets();
|
||||
const maxCount = getVirtualTargetMaxCount();
|
||||
|
||||
for (const row of candidates) {
|
||||
if (!isVirtualTargetAddAllowed(targets.length, maxCount)) break;
|
||||
if (targets.some(t => t.market === row.market)) continue;
|
||||
|
||||
const { koreanName, englishName } = resolveVirtualTargetNames(row.market, row.koreanName);
|
||||
targets = await addVirtualTarget(
|
||||
row.market,
|
||||
{ koreanName, englishName, candleType: timeframe },
|
||||
{ showLimitAlert: false },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -55,6 +55,18 @@ export function bullishWeightsFromRequest(
|
||||
return BULLISH_WEIGHT_ITEMS.map(item => req[item.key] ?? DEFAULT_BULLISH_WEIGHTS[item.key]);
|
||||
}
|
||||
|
||||
export const WEIGHT_STEP = 5;
|
||||
|
||||
export function snapToWeightStep(value: number): number {
|
||||
return Math.max(0, Math.min(100, Math.round(value / WEIGHT_STEP) * WEIGHT_STEP));
|
||||
}
|
||||
|
||||
/** range 슬라이더 트랙 클릭 위치 → 5점 단위 배점 */
|
||||
export function weightFromSliderPointer(clientX: number, rect: DOMRect): number {
|
||||
const ratio = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
|
||||
return snapToWeightStep(ratio * 100);
|
||||
}
|
||||
|
||||
export function sumWeights(weights: number[]): number {
|
||||
return weights.reduce((a, b) => a + b, 0);
|
||||
}
|
||||
@@ -71,43 +83,48 @@ export function adjustBullishWeight(
|
||||
const idx = keys.indexOf(changedKey);
|
||||
if (idx < 0) return current;
|
||||
|
||||
const clamped = Math.max(0, Math.min(100, Math.round(newValue)));
|
||||
const oldValue = current[changedKey];
|
||||
const clamped = snapToWeightStep(newValue);
|
||||
const oldValue = snapToWeightStep(current[changedKey]);
|
||||
if (clamped === oldValue) return current;
|
||||
|
||||
const result: Record<BullishWeightKey, number> = { ...current };
|
||||
for (const k of keys) result[k] = snapToWeightStep(result[k]);
|
||||
result[changedKey] = clamped;
|
||||
|
||||
const delta = clamped - oldValue;
|
||||
const otherKeys = keys.filter(k => k !== changedKey);
|
||||
|
||||
if (delta > 0) {
|
||||
let remaining = delta;
|
||||
for (let i = 0; i < otherKeys.length; i++) {
|
||||
const k = otherKeys[i];
|
||||
const take = i === otherKeys.length - 1
|
||||
? remaining
|
||||
: Math.floor(delta / otherKeys.length);
|
||||
result[k] = Math.max(0, result[k] - take);
|
||||
remaining -= take;
|
||||
const distribute = (amount: number, mode: 'take' | 'give') => {
|
||||
let remaining = amount;
|
||||
let idx = 0;
|
||||
let guard = 0;
|
||||
while (remaining > 0 && guard < 200) {
|
||||
const k = otherKeys[idx % otherKeys.length];
|
||||
if (mode === 'take') {
|
||||
const take = Math.min(WEIGHT_STEP, remaining, result[k]);
|
||||
if (take > 0) {
|
||||
result[k] -= take;
|
||||
remaining -= take;
|
||||
}
|
||||
} else {
|
||||
const give = Math.min(WEIGHT_STEP, remaining, 100 - result[k]);
|
||||
if (give > 0) {
|
||||
result[k] += give;
|
||||
remaining -= give;
|
||||
}
|
||||
}
|
||||
idx += 1;
|
||||
guard += 1;
|
||||
}
|
||||
} else {
|
||||
const add = Math.abs(delta);
|
||||
let remaining = add;
|
||||
for (let i = 0; i < otherKeys.length; i++) {
|
||||
const k = otherKeys[i];
|
||||
const give = i === otherKeys.length - 1
|
||||
? remaining
|
||||
: Math.floor(add / otherKeys.length);
|
||||
result[k] += give;
|
||||
remaining -= give;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (delta > 0) distribute(delta, 'take');
|
||||
else if (delta < 0) distribute(-delta, 'give');
|
||||
|
||||
const total = keys.reduce((s, k) => s + result[k], 0);
|
||||
if (total !== 100) {
|
||||
const fixKey = otherKeys[0] ?? changedKey;
|
||||
result[fixKey] = Math.max(0, result[fixKey] + (100 - total));
|
||||
result[fixKey] = snapToWeightStep(Math.max(0, Math.min(100, result[fixKey] + (100 - total))));
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
@@ -24,6 +24,7 @@ export async function syncVirtualTargetsToBackend(
|
||||
market: t.market,
|
||||
strategyId: t.strategyId ?? session.globalStrategyId,
|
||||
candleType: normalizeStartCandleType(resolveTargetCandleType(t)),
|
||||
isPinned: !!t.pinned,
|
||||
...shared,
|
||||
}),
|
||||
),
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
/** 가상매매 투자대상 목록 최대 개수 (설정 · gc_app_settings.virtual_target_max_count) */
|
||||
|
||||
export const DEFAULT_VIRTUAL_TARGET_MAX = 20;
|
||||
export const MIN_VIRTUAL_TARGET_MAX = 1;
|
||||
export const MAX_VIRTUAL_TARGET_MAX = 100;
|
||||
|
||||
export function clampVirtualTargetMax(n: number): number {
|
||||
if (!Number.isFinite(n)) return DEFAULT_VIRTUAL_TARGET_MAX;
|
||||
return Math.min(MAX_VIRTUAL_TARGET_MAX, Math.max(MIN_VIRTUAL_TARGET_MAX, Math.floor(n)));
|
||||
}
|
||||
|
||||
export function virtualTargetLimitMessage(max: number): string {
|
||||
return `투자대상은 최대 ${max}종목까지 등록할 수 있습니다. 설정 › 가상매매에서 한도를 확인하세요.`;
|
||||
}
|
||||
|
||||
export function isVirtualTargetAddAllowed(
|
||||
currentCount: number,
|
||||
maxCount: number = DEFAULT_VIRTUAL_TARGET_MAX,
|
||||
): boolean {
|
||||
return currentCount < clampVirtualTargetMax(maxCount);
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
import { saveLiveStrategySettings } from './backendApi';
|
||||
import { getVirtualTargetMaxCount } from '../hooks/useAppSettings';
|
||||
import {
|
||||
isVirtualTargetAddAllowed,
|
||||
virtualTargetLimitMessage,
|
||||
} from '../utils/virtualTargetLimits';
|
||||
import { normalizeStartCandleType } from './strategyStartNodes';
|
||||
import { syncVirtualTargetsToBackend } from './virtualLiveStrategySync';
|
||||
import {
|
||||
loadVirtualSession,
|
||||
loadVirtualTargets,
|
||||
notifyVirtualSessionChanged,
|
||||
saveVirtualTargets,
|
||||
type VirtualTargetItem,
|
||||
} from './virtualTradingStorage';
|
||||
|
||||
export interface VirtualTargetMeta {
|
||||
koreanName?: string;
|
||||
englishName?: string;
|
||||
/** 추세검색 캔들 주기 → 투자대상 평가 분봉 기본값 */
|
||||
candleType?: string;
|
||||
}
|
||||
|
||||
export interface AddVirtualTargetOptions {
|
||||
/** false면 한도 초과 시 알림 없이 스킵 (자동추가용) */
|
||||
showLimitAlert?: boolean;
|
||||
}
|
||||
|
||||
/** 가상투자 투자대상 목록에 종목 추가 (localStorage + DB) */
|
||||
export async function addVirtualTarget(
|
||||
market: string,
|
||||
meta: VirtualTargetMeta = {},
|
||||
opts: AddVirtualTargetOptions = {},
|
||||
): Promise<VirtualTargetItem[]> {
|
||||
const showLimitAlert = opts.showLimitAlert !== false;
|
||||
const targets = loadVirtualTargets();
|
||||
if (targets.some(t => t.market === market)) return targets;
|
||||
|
||||
const maxCount = getVirtualTargetMaxCount();
|
||||
if (!isVirtualTargetAddAllowed(targets.length, maxCount)) {
|
||||
if (showLimitAlert) window.alert(virtualTargetLimitMessage(maxCount));
|
||||
return targets;
|
||||
}
|
||||
|
||||
const session = loadVirtualSession();
|
||||
const en = meta.englishName ?? market.replace(/^KRW-/, '');
|
||||
const item: VirtualTargetItem = {
|
||||
market,
|
||||
strategyId: session.globalStrategyId,
|
||||
koreanName: meta.koreanName,
|
||||
englishName: en,
|
||||
candleType: meta.candleType ? normalizeStartCandleType(meta.candleType) : undefined,
|
||||
};
|
||||
|
||||
const next = [...targets, item];
|
||||
saveVirtualTargets(next);
|
||||
|
||||
await saveLiveStrategySettings({
|
||||
market,
|
||||
strategyId: session.globalStrategyId,
|
||||
isLiveCheck: session.running,
|
||||
isPinned: false,
|
||||
executionType: session.executionType,
|
||||
positionMode: session.positionMode,
|
||||
candleType: item.candleType ?? '1m',
|
||||
skipWatchlistSync: true,
|
||||
skipGlobalTemplate: true,
|
||||
});
|
||||
|
||||
if (session.running && session.globalStrategyId) {
|
||||
await syncVirtualTargetsToBackend(next, session, true);
|
||||
}
|
||||
|
||||
notifyVirtualSessionChanged();
|
||||
return next;
|
||||
}
|
||||
|
||||
/** 가상투자 투자대상 목록에서 종목 제거 (localStorage + DB 비활성화) */
|
||||
/** 투자대상 고정 상태 DB 저장 */
|
||||
export async function persistVirtualTargetPinned(market: string, pinned: boolean): Promise<void> {
|
||||
const session = loadVirtualSession();
|
||||
const item = loadVirtualTargets().find(t => t.market === market);
|
||||
await saveLiveStrategySettings({
|
||||
market,
|
||||
strategyId: item?.strategyId ?? session.globalStrategyId,
|
||||
isLiveCheck: session.running,
|
||||
isPinned: pinned,
|
||||
executionType: session.executionType,
|
||||
positionMode: session.positionMode,
|
||||
skipWatchlistSync: true,
|
||||
skipGlobalTemplate: true,
|
||||
});
|
||||
}
|
||||
|
||||
export async function removeVirtualTarget(market: string): Promise<VirtualTargetItem[]> {
|
||||
const targets = loadVirtualTargets();
|
||||
const item = targets.find(t => t.market === market);
|
||||
if (!item) return targets;
|
||||
if (item.pinned) return targets;
|
||||
|
||||
const session = loadVirtualSession();
|
||||
const next = targets.filter(t => t.market !== market);
|
||||
saveVirtualTargets(next);
|
||||
|
||||
await saveLiveStrategySettings({
|
||||
market,
|
||||
strategyId: session.globalStrategyId,
|
||||
isLiveCheck: false,
|
||||
isPinned: false,
|
||||
executionType: session.executionType,
|
||||
positionMode: session.positionMode,
|
||||
skipWatchlistSync: true,
|
||||
skipGlobalTemplate: true,
|
||||
});
|
||||
|
||||
if (session.running) {
|
||||
await syncVirtualTargetsToBackend(next, session, true);
|
||||
}
|
||||
|
||||
notifyVirtualSessionChanged();
|
||||
return next;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { getKoreanName } from './marketNameCache';
|
||||
|
||||
/** 백엔드/캐시에 마켓 코드가 한글명으로 들어온 경우 보정 */
|
||||
export function isInvalidStoredKoreanName(market: string, name?: string | null): boolean {
|
||||
if (!name || !name.trim()) return true;
|
||||
const trimmed = name.trim();
|
||||
if (trimmed === market) return true;
|
||||
if (trimmed.startsWith('KRW-')) return true;
|
||||
const sym = market.replace(/^KRW-/, '');
|
||||
if (trimmed === sym) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/** 투자대상 표시·저장용 한글명·영문 심볼 */
|
||||
export function resolveVirtualTargetNames(
|
||||
market: string,
|
||||
koreanName?: string | null,
|
||||
englishName?: string | null,
|
||||
): { koreanName: string; englishName: string } {
|
||||
const en = (englishName?.trim() || market.replace(/^KRW-/, '')).replace(/^KRW-/, '');
|
||||
const ko = !isInvalidStoredKoreanName(market, koreanName)
|
||||
? koreanName!.trim()
|
||||
: getKoreanName(market);
|
||||
return { koreanName: ko, englishName: en };
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { normalizeStartCandleType } from './strategyStartNodes';
|
||||
import { resolveVirtualTargetNames } from './virtualTargetNames';
|
||||
|
||||
/** 가상투자 대상 종목·세션 설정 localStorage */
|
||||
|
||||
@@ -9,6 +10,8 @@ export interface VirtualTargetItem {
|
||||
candleType?: string;
|
||||
koreanName?: string;
|
||||
englishName?: string;
|
||||
/** 투자대상 고정 — ON 이면 목록·추세검색에서 삭제 불가 (gc_live_strategy_settings.is_pinned) */
|
||||
pinned?: boolean;
|
||||
}
|
||||
|
||||
export interface VirtualSessionConfig {
|
||||
@@ -31,7 +34,15 @@ export function loadVirtualTargets(): VirtualTargetItem[] {
|
||||
const raw = localStorage.getItem(TARGETS_KEY);
|
||||
if (!raw) return [];
|
||||
const parsed = JSON.parse(raw) as VirtualTargetItem[];
|
||||
return Array.isArray(parsed) ? parsed : [];
|
||||
if (!Array.isArray(parsed)) return [];
|
||||
return parsed.map(t => {
|
||||
const { koreanName, englishName } = resolveVirtualTargetNames(
|
||||
t.market,
|
||||
t.koreanName,
|
||||
t.englishName,
|
||||
);
|
||||
return { ...t, koreanName, englishName };
|
||||
});
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user