서버 가상매매 목록 지표없음 문제 개선

This commit is contained in:
Macbook
2026-06-07 19:14:13 +09:00
parent b294c2be5d
commit 2f3133d413
10 changed files with 299 additions and 62 deletions
@@ -36,6 +36,7 @@ export default function VirtualTradingScreen() {
snapshots,
snapshotLoadingByMarket,
liveStatusByMarket,
appLiveStrategyId,
handleStart,
handleStop,
handleRemoveTarget,
@@ -184,7 +185,11 @@ export default function VirtualTradingScreen() {
aria-label="투자 대상 목록"
>
{t => {
const strategyId = resolveVirtualTargetStrategyId(t, session.globalStrategyId);
const strategyId = resolveVirtualTargetStrategyId(
t,
session.globalStrategyId,
appLiveStrategyId,
);
const snapKey = `${t.market}:${strategyId ?? ''}`;
const snap = snapshots[snapKey] ?? snapshots[t.market];
const signalLoading = snapshotLoadingByMarket[t.market];
@@ -351,19 +351,42 @@ public class LiveStrategySettingsService {
return dto;
}
/** 앱 전역 실시간 체크 ON/OFF·전략 ID를 DTO에 반영 (종목별 행과 분리) */
/**
* 앱 전역 실시간 템플릿 반영.
* 전역 liveStrategyCheck+ID 가 있으면 관심종목·차트 모드 우선.
* 없으면 가상투자 per-market 행(isLiveCheck + strategyId) 유지.
*/
private LiveStrategySettingsDto mergeGlobalTemplate(
LiveStrategySettingsDto dto, GcAppSettings app, String market) {
dto.setMarket(market);
dto.setLiveCheck(Boolean.TRUE.equals(app.getLiveStrategyCheck()));
dto.setStrategyId(app.getLiveStrategyId());
boolean globalCheck = Boolean.TRUE.equals(app.getLiveStrategyCheck());
Long globalSid = app.getLiveStrategyId();
if (globalCheck && globalSid != null) {
dto.setLiveCheck(true);
dto.setStrategyId(globalSid);
applyGlobalExecutionFields(dto, app);
return dto;
}
if (dto.getStrategyId() != null && dto.isLiveCheck()) {
applyGlobalExecutionFields(dto, app);
return dto;
}
dto.setLiveCheck(globalCheck);
dto.setStrategyId(globalSid);
applyGlobalExecutionFields(dto, app);
return dto;
}
private void applyGlobalExecutionFields(LiveStrategySettingsDto dto, GcAppSettings app) {
if (app.getLiveExecutionType() != null) {
dto.setExecutionType(app.getLiveExecutionType());
}
if (app.getLivePositionMode() != null) {
dto.setPositionMode(app.getLivePositionMode());
}
return dto;
}
private LiveStrategySettingsDto defaultDtoFromApp(String market, GcAppSettings app) {
+113 -37
View File
@@ -44,7 +44,7 @@ import {
type VirtualTargetItem,
type VirtualCardViewMode,
} from '../utils/virtualTradingStorage';
import { useAppSettings, resolveAppDefaults } from '../hooks/useAppSettings';
import { useAppSettings, resolveAppDefaults, subscribeAppSettings } from '../hooks/useAppSettings';
import { coerceFiniteNumber } from '../utils/safeFormat';
import type { TradeNotificationGridPresetId } from '../utils/tradeNotificationGridPresets';
import {
@@ -62,6 +62,12 @@ import {
resolveVirtualTargetStrategyId,
} from '../utils/virtualTargetStrategy';
import { virtualTargetLimitMessage } from '../utils/virtualTargetLimits';
import { hydrateVirtualTargetsIfEmpty } from '../utils/virtualTargetsHydrate';
import {
mergeTargetsWithLiveStrategySettings,
mergeVirtualSessionWithAppLive,
resolveVirtualTargetStrategyIdWithApp,
} from '../utils/virtualTargetHydrate';
import '../styles/virtualTradingDashboard.css';
type RightTab = 'trade' | 'orderbook' | 'history';
@@ -102,7 +108,8 @@ const VirtualTradingPage: React.FC<Props> = ({
const [globalDisplayMode, setGlobalDisplayMode] = useState<VirtualCardDisplayMode>('signal');
const [focusMarket, setFocusMarket] = useState<string | null>(null);
const orderAnchorRef = useRef<HTMLDivElement>(null);
const { settings: appSettings } = useAppSettings();
const preferencesHydratedRef = useRef(false);
const { settings: appSettings, isLoaded: settingsLoaded } = useAppSettings();
const appChartDefaults = resolveAppDefaults(appSettings);
const chartRealtimeSource = (appChartDefaults.chartRealtimeSource
?? 'BACKEND_STOMP') as ChartRealtimeSource;
@@ -111,59 +118,127 @@ const VirtualTradingPage: React.FC<Props> = ({
const chartPaneSeparator = appChartDefaults.chartPaneSeparator;
const chartLiveReceiveHighlight = appChartDefaults.chartLiveReceiveHighlight;
const virtualTargetMaxCount = appChartDefaults.virtualTargetMaxCount;
const appLiveStrategyId = appChartDefaults.liveStrategyId ?? null;
const reloadFromUiPreferences = useCallback(() => {
const sessionFromDb = mergeVirtualSessionWithAppLive(loadVirtualSession(), appLiveStrategyId);
setSession(sessionFromDb);
setTargets(loadVirtualTargets());
}, [appLiveStrategyId]);
useEffect(() => {
void Promise.all([
loadStrategies()
.then(list => {
setStrategies(list);
const validIds = new Set(
list.map(s => s.id).filter((id): id is number => id != null && id > 0),
);
setSession(prev =>
prev.globalStrategyId != null && !validIds.has(prev.globalStrategyId)
? { ...prev, globalStrategyId: null }
: prev,
);
setTargets(prev =>
prev.map(t =>
t.strategyId != null && !validIds.has(t.strategyId)
? { ...t, strategyId: null }
: t,
),
);
})
.catch(() => setStrategies([])),
loadPaperSummary().then(setSummary),
loadPaperTrades().then(setTrades),
]).finally(() => setLoading(false));
}, []);
if (!settingsLoaded) return;
let cancelled = false;
preferencesHydratedRef.current = false;
setLoading(true);
void (async () => {
reloadFromUiPreferences();
let nextTargets = loadVirtualTargets();
if (nextTargets.length === 0) {
nextTargets = await hydrateVirtualTargetsIfEmpty();
if (!cancelled && nextTargets.length > 0) {
setTargets(nextTargets);
saveVirtualTargets(nextTargets);
}
}
const sessionFromDb = mergeVirtualSessionWithAppLive(loadVirtualSession(), appLiveStrategyId);
if (!cancelled) {
setSession(sessionFromDb);
}
if (nextTargets.length > 0) {
const withLive = await mergeTargetsWithLiveStrategySettings(
nextTargets,
sessionFromDb,
appLiveStrategyId,
);
if (!cancelled && withLive !== nextTargets) {
setTargets(withLive);
}
}
if (!cancelled) {
preferencesHydratedRef.current = true;
}
await Promise.all([
loadStrategies()
.then(list => {
if (cancelled) return;
setStrategies(list);
const validIds = new Set(
list.map(s => s.id).filter((id): id is number => id != null && id > 0),
);
setSession(prev => {
const next = prev.globalStrategyId != null && !validIds.has(prev.globalStrategyId)
? { ...prev, globalStrategyId: null }
: prev;
return mergeVirtualSessionWithAppLive(next, appLiveStrategyId);
});
setTargets(prev =>
prev.map(t =>
t.strategyId != null && !validIds.has(t.strategyId)
? { ...t, strategyId: null }
: t,
),
);
})
.catch(() => { if (!cancelled) setStrategies([]); }),
loadPaperSummary().then(sum => { if (!cancelled) setSummary(sum); }),
loadPaperTrades().then(tr => { if (!cancelled) setTrades(tr); }),
]);
if (!cancelled) setLoading(false);
})();
return () => { cancelled = true; };
}, [settingsLoaded, appLiveStrategyId, reloadFromUiPreferences]);
/** DB is_pinned → localStorage 투자대상 목록 동기화 (최초 1회) */
useEffect(() => {
if (!settingsLoaded) return;
return subscribeAppSettings(() => {
reloadFromUiPreferences();
});
}, [settingsLoaded, reloadFromUiPreferences]);
/** DB is_pinned·strategyId → ui_preferences 투자대상 동기화 */
useEffect(() => {
if (!settingsLoaded) return;
let cancelled = false;
const initial = loadVirtualTargets();
if (initial.length === 0) return;
const sessionFromDb = mergeVirtualSessionWithAppLive(loadVirtualSession(), appLiveStrategyId);
void (async () => {
const merged = await Promise.all(
initial.map(async t => {
try {
const s = await loadLiveStrategySettings(t.market);
const pinned = !!s.isPinned;
if (!!t.pinned === pinned) return t;
return { ...t, pinned };
let next = t;
if (!!t.pinned !== !!s.isPinned) {
next = { ...next, pinned: !!s.isPinned };
}
if (next.strategyId == null && s.strategyId != null && s.strategyId > 0 && s.isLiveCheck) {
next = { ...next, strategyId: s.strategyId };
}
return next;
} catch {
return t;
}
}),
);
if (!cancelled) setTargets(merged);
const withLive = await mergeTargetsWithLiveStrategySettings(
merged,
sessionFromDb,
appLiveStrategyId,
);
if (!cancelled) setTargets(withLive);
})();
return () => { cancelled = true; };
}, []);
}, [settingsLoaded, appLiveStrategyId]);
useEffect(() => { saveVirtualTargets(targets); }, [targets]);
useEffect(() => { saveVirtualSession(session); }, [session]);
useEffect(() => {
if (!preferencesHydratedRef.current) return;
saveVirtualTargets(targets);
}, [targets]);
useEffect(() => {
if (!preferencesHydratedRef.current) return;
saveVirtualSession(session);
}, [session]);
useEffect(() => { saveVirtualCardViewMode(viewMode); }, [viewMode]);
useEffect(() => { saveVirtualGridPreset(gridPreset); }, [gridPreset]);
@@ -194,9 +269,9 @@ const VirtualTradingPage: React.FC<Props> = ({
const targetRefs = useMemo(() =>
targets.map(t => ({
market: t.market,
strategyId: resolveVirtualTargetStrategyId(t, session.globalStrategyId),
strategyId: resolveVirtualTargetStrategyIdWithApp(t, session, appLiveStrategyId),
})),
[targets, session.globalStrategyId]);
[targets, session, appLiveStrategyId]);
const { snapshots, loadingByMarket: snapshotLoadingByMarket } = useVirtualIndicatorSnapshots(
targetRefs,
@@ -474,6 +549,7 @@ const VirtualTradingPage: React.FC<Props> = ({
globalDisplayMode={globalDisplayMode}
onFocusMarketChange={setFocusMarket}
gridPreset={gridPreset}
appLiveStrategyId={appLiveStrategyId}
/>
)}
rightTabs={rightTabs}
@@ -4,6 +4,7 @@ import {
resolveVirtualTargetNames,
resolveVirtualTargetStrategyDisplayName,
} from '../../utils/virtualTargetNames';
import { resolveVirtualTargetStrategyId } from '../../utils/virtualTargetStrategy';
import type { VirtualIndicatorSnapshot } from '../../hooks/useVirtualIndicatorSnapshots';
import { useLiveReceiveFlash } from '../../hooks/useLiveReceiveFlash';
import type { ChartRealtimeSource } from '../../hooks/useChartRealtimeData';
@@ -57,6 +58,8 @@ interface Props {
readOnlyStrategyLabel?: string;
/** 헤더 우측 레이아웃 (알림 목록 신호 슬롯: inline-quote) */
headVariant?: VirtualCardHeadVariant;
/** gc_app_settings.liveStrategyId — ui 미동기화 시 지표 평가용 */
appLiveStrategyId?: number | null;
}
const VirtualTargetCard: React.FC<Props> = ({
@@ -88,6 +91,7 @@ const VirtualTargetCard: React.FC<Props> = ({
englishName,
readOnlyStrategyLabel,
headVariant = 'default',
appLiveStrategyId = null,
}) => {
const inlineHeadQuote = headVariant === 'inline-quote';
const { koreanName: displayKo, englishName: displayEn } = resolveVirtualTargetNames(
@@ -101,6 +105,11 @@ const VirtualTargetCard: React.FC<Props> = ({
readOnlyStrategyLabel,
);
const rows = snapshot?.rows ?? [];
const effectiveStrategyId = resolveVirtualTargetStrategyId(
{ strategyId },
globalStrategyId,
appLiveStrategyId,
);
const metrics = useMemo(() => buildConditionMetrics(rows), [rows]);
const metCount = metrics.filter(m => m.status === 'match').length;
@@ -232,6 +241,7 @@ const VirtualTargetCard: React.FC<Props> = ({
viewMode={viewMode}
receiving={highlightReceiving}
loading={signalLoading}
hasStrategy={effectiveStrategyId != null}
/>
)}
@@ -44,6 +44,7 @@ interface Props {
globalDisplayMode: VirtualCardDisplayMode;
onFocusMarketChange?: (market: string | null) => void;
gridPreset: TradeNotificationGridPresetId;
appLiveStrategyId?: number | null;
}
const VirtualTargetGrid: React.FC<Props> = ({
@@ -62,6 +63,7 @@ const VirtualTargetGrid: React.FC<Props> = ({
globalDisplayMode,
onFocusMarketChange,
gridPreset,
appLiveStrategyId = null,
}) => {
const gridCols = getTradeNotificationGridPreset(gridPreset).cols;
@@ -146,7 +148,7 @@ const VirtualTargetGrid: React.FC<Props> = ({
running={session.running}
liveStatus={resolveVirtualLiveStatusFallback(
focusTarget.market,
resolveVirtualTargetStrategyId(focusTarget, session.globalStrategyId),
resolveVirtualTargetStrategyId(focusTarget, session.globalStrategyId, appLiveStrategyId),
liveStatusByMarket,
session.running,
)}
@@ -184,7 +186,7 @@ const VirtualTargetGrid: React.FC<Props> = ({
aria-label="가상매매 종목 그리드"
>
{t => {
const stratId = resolveVirtualTargetStrategyId(t, session.globalStrategyId);
const stratId = resolveVirtualTargetStrategyId(t, session.globalStrategyId, appLiveStrategyId);
const strat = strategies.find(s => s.id === stratId);
return (
<VirtualTargetCard
@@ -219,6 +221,7 @@ const VirtualTargetGrid: React.FC<Props> = ({
ticker={tickers?.get(t.market)}
koreanName={t.koreanName}
englishName={t.englishName}
appLiveStrategyId={appLiveStrategyId}
/>
);
}}
@@ -18,6 +18,8 @@ interface Props {
receiving?: boolean;
/** 업비트 캔들 warm-up · live-conditions 수집 중 */
loading?: boolean;
/** false — 전략 미지정 안내 */
hasStrategy?: boolean;
className?: string;
}
@@ -26,6 +28,7 @@ const VirtualTargetSignalPanel: React.FC<Props> = ({
viewMode = 'summary',
receiving = false,
loading = false,
hasStrategy = true,
className = '',
}) => {
const rows = snapshot?.rows ?? [];
@@ -49,7 +52,9 @@ const VirtualTargetSignalPanel: React.FC<Props> = ({
<p className={`vtd-muted vtd-card-empty${loading ? ' vtd-card-empty--loading' : ''}${className ? ` ${className}` : ''}`}>
{loading
? '지표 데이터 수집 중… (업비트 캔들 · 실시간 연결)'
: '전략 조건·지표 데이터 없음'}
: !hasStrategy
? '투자전략을 선택하세요 (상단 또는 종목별 전략)'
: '전략 조건·지표 데이터 없음'}
</p>
);
}
+48 -15
View File
@@ -52,6 +52,11 @@ import {
reloadAppSettingsCache,
} from './useAppSettings';
import { hydrateVirtualTargetsIfEmpty } from '../utils/virtualTargetsHydrate';
import {
mergeTargetsWithLiveStrategySettings,
mergeVirtualSessionWithAppLive,
resolveVirtualTargetStrategyIdWithApp,
} from '../utils/virtualTargetHydrate';
export interface UseVirtualTradingCoreOptions {
defaultMarket?: string;
@@ -81,6 +86,7 @@ export function useVirtualTradingCore(options: UseVirtualTradingCoreOptions = {}
const paperAutoTradeEnabled = paperAutoTradeEnabledProp ?? appChartDefaults.paperAutoTradeEnabled;
const paperAutoTradeBudgetPct = paperAutoTradeBudgetPctProp ?? appChartDefaults.paperAutoTradeBudgetPct;
const virtualTargetMaxCount = appChartDefaults.virtualTargetMaxCount;
const appLiveStrategyId = appChartDefaults.liveStrategyId ?? null;
const [targets, setTargets] = useState<VirtualTargetItem[]>(() => loadVirtualTargets());
const [session, setSession] = useState<VirtualSessionConfig>(() => loadVirtualSession());
@@ -95,8 +101,8 @@ export function useVirtualTradingCore(options: UseVirtualTradingCoreOptions = {}
const reloadFromUiPreferences = useCallback(() => {
setTargets(loadVirtualTargets());
setSession(loadVirtualSession());
}, []);
setSession(mergeVirtualSessionWithAppLive(loadVirtualSession(), appLiveStrategyId));
}, [appLiveStrategyId]);
useEffect(() => {
if (!settingsLoaded) return;
@@ -113,6 +119,20 @@ export function useVirtualTradingCore(options: UseVirtualTradingCoreOptions = {}
saveVirtualTargets(nextTargets);
}
}
const sessionFromDb = mergeVirtualSessionWithAppLive(loadVirtualSession(), appLiveStrategyId);
if (!cancelled) {
setSession(sessionFromDb);
}
if (nextTargets.length > 0) {
const withLive = await mergeTargetsWithLiveStrategySettings(
nextTargets,
sessionFromDb,
appLiveStrategyId,
);
if (!cancelled && withLive !== nextTargets) {
setTargets(withLive);
}
}
if (!cancelled) {
preferencesHydratedRef.current = true;
}
@@ -123,11 +143,12 @@ export function useVirtualTradingCore(options: UseVirtualTradingCoreOptions = {}
const validIds = new Set(
s.map(x => x.id).filter((id): id is number => id != null && id > 0),
);
setSession(prev =>
prev.globalStrategyId != null && !validIds.has(prev.globalStrategyId)
setSession(prev => {
const next = prev.globalStrategyId != null && !validIds.has(prev.globalStrategyId)
? { ...prev, globalStrategyId: null }
: prev,
);
: prev;
return mergeVirtualSessionWithAppLive(next, appLiveStrategyId);
});
setTargets(prev =>
prev.map(t =>
t.strategyId != null && !validIds.has(t.strategyId)
@@ -142,7 +163,7 @@ export function useVirtualTradingCore(options: UseVirtualTradingCoreOptions = {}
if (!cancelled) setLoading(false);
})();
return () => { cancelled = true; };
}, [settingsSessionKey, settingsLoaded, reloadFromUiPreferences]);
}, [settingsSessionKey, settingsLoaded, appLiveStrategyId, reloadFromUiPreferences]);
/** DB ui_preferences 반영(웹·모바일 동일 계정) */
useEffect(() => {
@@ -152,29 +173,40 @@ export function useVirtualTradingCore(options: UseVirtualTradingCoreOptions = {}
});
}, [settingsLoaded, reloadFromUiPreferences]);
/** DB is_pinned → uiPreferences.virtual.targets 동기화 */
/** DB is_pinned·strategyId → uiPreferences.virtual.targets 동기화 */
useEffect(() => {
if (!settingsLoaded) return;
let cancelled = false;
const initial = loadVirtualTargets();
if (initial.length === 0) return;
const sessionFromDb = mergeVirtualSessionWithAppLive(loadVirtualSession(), appLiveStrategyId);
void (async () => {
const merged = await Promise.all(
initial.map(async t => {
try {
const s = await loadLiveStrategySettings(t.market);
const pinned = !!s.isPinned;
if (!!t.pinned === pinned) return t;
return { ...t, pinned };
let next = t;
if (!!t.pinned !== !!s.isPinned) {
next = { ...next, pinned: !!s.isPinned };
}
if (next.strategyId == null && s.strategyId != null && s.strategyId > 0 && s.isLiveCheck) {
next = { ...next, strategyId: s.strategyId };
}
return next;
} catch {
return t;
}
}),
);
if (!cancelled) setTargets(merged);
const withLive = await mergeTargetsWithLiveStrategySettings(
merged,
sessionFromDb,
appLiveStrategyId,
);
if (!cancelled) setTargets(withLive);
})();
return () => { cancelled = true; };
}, [settingsLoaded]);
}, [settingsLoaded, appLiveStrategyId]);
useEffect(() => {
if (!preferencesHydratedRef.current) return;
@@ -203,9 +235,9 @@ export function useVirtualTradingCore(options: UseVirtualTradingCoreOptions = {}
const targetRefs = useMemo(
() => targets.map(t => ({
market: t.market,
strategyId: resolveVirtualTargetStrategyId(t, session.globalStrategyId),
strategyId: resolveVirtualTargetStrategyIdWithApp(t, session, appLiveStrategyId),
})),
[targets, session.globalStrategyId],
[targets, session, appLiveStrategyId],
);
const { snapshots, loadingByMarket: snapshotLoadingByMarket } = useVirtualIndicatorSnapshots(
@@ -400,6 +432,7 @@ export function useVirtualTradingCore(options: UseVirtualTradingCoreOptions = {}
snapshotLoadingByMarket,
liveStatusByMarket: mergedLiveStatus,
lastTickAtByMarket,
appLiveStrategyId,
strategyNames,
virtualTargetMaxCount,
handleStart,
+1 -1
View File
@@ -1472,7 +1472,7 @@ export async function pinCandleWatch(market: string, candleType: string): Promis
try {
await fetch(
`${API_BASE}/candles/watch?market=${encodeURIComponent(market)}&type=${encodeURIComponent(candleType)}`,
{ method: 'POST' },
{ method: 'POST', headers: authHeaders() },
);
} catch {
/* pin 실패해도 평가 API는 시도 */
@@ -0,0 +1,78 @@
/**
* 가상투자 — ui_preferences·앱 설정·서버 live-strategy 행 간 전략 ID 정합
*/
import { loadLiveStrategySettings, type LiveStrategySettingsDto } from './backendApi';
import type { VirtualSessionConfig, VirtualTargetItem } from './virtualTradingStorage';
import { resolveVirtualTargetStrategyId } from './virtualTargetStrategy';
/** 종목별 effective strategyId (ui → 세션 → 앱 전역 liveStrategyId) */
export function resolveVirtualTargetStrategyIdWithApp(
target: Pick<VirtualTargetItem, 'strategyId'>,
session: Pick<VirtualSessionConfig, 'globalStrategyId'>,
appLiveStrategyId: number | null | undefined,
): number | null {
return resolveVirtualTargetStrategyId(
target,
session.globalStrategyId,
appLiveStrategyId ?? null,
);
}
/** 세션 globalStrategyId — ui_preferences 비었을 때 앱 전역 liveStrategyId 로 복구 */
export function mergeVirtualSessionWithAppLive(
session: VirtualSessionConfig,
appLiveStrategyId: number | null | undefined,
): VirtualSessionConfig {
if (session.globalStrategyId != null) return session;
const sid = appLiveStrategyId;
if (sid == null || sid <= 0) return session;
return { ...session, globalStrategyId: sid };
}
function applyLiveRow(
target: VirtualTargetItem,
live: LiveStrategySettingsDto,
globalStrategyId: number | null,
appLiveStrategyId: number | null | undefined,
): VirtualTargetItem {
if (target.strategyId != null) return target;
const effective = resolveVirtualTargetStrategyIdWithApp(
target,
{ globalStrategyId },
appLiveStrategyId,
);
if (effective != null) return target;
const sid = live.strategyId;
if (sid == null || sid <= 0 || !live.isLiveCheck) return target;
return { ...target, strategyId: sid };
}
/** 서버 gc_live_strategy_settings — ui에 strategyId 없을 때 per-market 복구 */
export async function mergeTargetsWithLiveStrategySettings(
targets: VirtualTargetItem[],
session: VirtualSessionConfig,
appLiveStrategyId: number | null | undefined,
): Promise<VirtualTargetItem[]> {
if (targets.length === 0) return targets;
const globalId = session.globalStrategyId;
const merged = await Promise.all(
targets.map(async t => {
const effective = resolveVirtualTargetStrategyIdWithApp(
t,
session,
appLiveStrategyId,
);
if (effective != null) return t;
try {
const live = await loadLiveStrategySettings(t.market);
return applyLiveRow(t, live, globalId, appLiveStrategyId);
} catch {
return t;
}
}),
);
const changed = merged.some((t, i) => t.strategyId !== targets[i]?.strategyId);
return changed ? merged : targets;
}
+5 -1
View File
@@ -11,9 +11,13 @@ export const VIRTUAL_DEFAULT_STRATEGY_VALUE = '__default__';
export function resolveVirtualTargetStrategyId(
target: Pick<VirtualTargetItem, 'strategyId'>,
globalStrategyId: number | null,
/** gc_app_settings.liveStrategyId — ui_preferences.globalStrategyId 미동기화 시 폴백 */
appLiveStrategyId: number | null = null,
): number | null {
if (target.strategyId != null) return target.strategyId;
return globalStrategyId;
if (globalStrategyId != null) return globalStrategyId;
if (appLiveStrategyId != null && appLiveStrategyId > 0) return appLiveStrategyId;
return null;
}
/** 기본전략 변경 직전 — null(기본 따름) 종목을 이전 기본전략 ID 로 고정 */