가상매매 기본전략 설정

This commit is contained in:
Macbook
2026-06-07 19:21:54 +09:00
parent 2f3133d413
commit 76d4de56bd
5 changed files with 102 additions and 32 deletions
+19 -11
View File
@@ -58,6 +58,8 @@ import {
import { pinStrategyEvaluationTimeframes } from '../utils/strategyTimeframePin'; import { pinStrategyEvaluationTimeframes } from '../utils/strategyTimeframePin';
import { persistVirtualTargetPinned } from '../utils/virtualTargetMutations'; import { persistVirtualTargetPinned } from '../utils/virtualTargetMutations';
import { import {
applyDefaultGlobalStrategyToSession,
ensureDefaultGlobalStrategyId,
freezeTargetsBeforeGlobalStrategyChange, freezeTargetsBeforeGlobalStrategyChange,
resolveVirtualTargetStrategyId, resolveVirtualTargetStrategyId,
} from '../utils/virtualTargetStrategy'; } from '../utils/virtualTargetStrategy';
@@ -166,12 +168,13 @@ const VirtualTradingPage: React.FC<Props> = ({
const validIds = new Set( const validIds = new Set(
list.map(s => s.id).filter((id): id is number => id != null && id > 0), list.map(s => s.id).filter((id): id is number => id != null && id > 0),
); );
setSession(prev => { setSession(prev =>
const next = prev.globalStrategyId != null && !validIds.has(prev.globalStrategyId) applyDefaultGlobalStrategyToSession(
? { ...prev, globalStrategyId: null } mergeVirtualSessionWithAppLive(prev, appLiveStrategyId),
: prev; list,
return mergeVirtualSessionWithAppLive(next, appLiveStrategyId); appLiveStrategyId,
}); ),
);
setTargets(prev => setTargets(prev =>
prev.map(t => prev.map(t =>
t.strategyId != null && !validIds.has(t.strategyId) t.strategyId != null && !validIds.has(t.strategyId)
@@ -380,20 +383,25 @@ const VirtualTradingPage: React.FC<Props> = ({
window.alert('투자대상 종목을 먼저 추가하세요.'); window.alert('투자대상 종목을 먼저 추가하세요.');
return; return;
} }
if (!session.globalStrategyId) { const globalStrategyId = ensureDefaultGlobalStrategyId(session.globalStrategyId, strategies);
if (!globalStrategyId) {
window.alert('투자전략을 선택하세요.'); window.alert('투자전략을 선택하세요.');
return; return;
} }
const globalStrat = strategies.find(s => s.id === session.globalStrategyId); const sessionForStart = { ...session, globalStrategyId };
if (session.globalStrategyId !== globalStrategyId) {
setSession(sessionForStart);
}
const globalStrat = strategies.find(s => s.id === globalStrategyId);
const synced = await syncStrategyTimeframesFromLayoutIfNeeded( const synced = await syncStrategyTimeframesFromLayoutIfNeeded(
session.globalStrategyId, globalStrategyId,
globalStrat, globalStrat,
); );
if (!synced) return; if (!synced) return;
const ok = await warnStrategyTimeframeMismatch(session.globalStrategyId, globalStrat); const ok = await warnStrategyTimeframeMismatch(globalStrategyId, globalStrat);
if (!ok) return; if (!ok) return;
try { try {
await syncVirtualTargetsToBackend(targets, session, true); await syncVirtualTargetsToBackend(targets, sessionForStart, true);
setSession(s => ({ ...s, running: true })); setSession(s => ({ ...s, running: true }));
} catch { } catch {
window.alert('가상투자 시작 설정 저장에 실패했습니다.'); window.alert('가상투자 시작 설정 저장에 실패했습니다.');
@@ -12,6 +12,7 @@ import {
import { import {
defaultStrategyOptionLabel, defaultStrategyOptionLabel,
parseTargetStrategySelectValue, parseTargetStrategySelectValue,
resolveStrategyIdForNewTarget,
targetStrategySelectValue, targetStrategySelectValue,
} from '../../utils/virtualTargetStrategy'; } from '../../utils/virtualTargetStrategy';
import { VirtualScroll } from '../common/VirtualScroll'; import { VirtualScroll } from '../common/VirtualScroll';
@@ -78,7 +79,7 @@ const VirtualLeftTargetPanel: React.FC<Props> = ({
...targets, ...targets,
{ {
market, market,
strategyId: globalStrategyId, strategyId: resolveStrategyIdForNewTarget(globalStrategyId, strategies),
koreanName: names.koreanName, koreanName: names.koreanName,
englishName: names.englishName, englishName: names.englishName,
}, },
@@ -86,7 +87,7 @@ const VirtualLeftTargetPanel: React.FC<Props> = ({
onSelectMarket(market); onSelectMarket(market);
setQuery(''); setQuery('');
setShowDropdown(false); setShowDropdown(false);
}, [addedSet, targets, globalStrategyId, onTargetsChange, onSelectMarket, virtualTargetMaxCount]); }, [addedSet, targets, globalStrategyId, strategies, onTargetsChange, onSelectMarket, virtualTargetMaxCount]);
const handleRemove = useCallback((market: string) => { const handleRemove = useCallback((market: string) => {
onTargetsChange(targets.filter(t => t.market !== market)); onTargetsChange(targets.filter(t => t.market !== market));
+22 -13
View File
@@ -37,8 +37,11 @@ import {
import { pinStrategyEvaluationTimeframes } from '../utils/strategyTimeframePin'; import { pinStrategyEvaluationTimeframes } from '../utils/strategyTimeframePin';
import { persistVirtualTargetPinned } from '../utils/virtualTargetMutations'; import { persistVirtualTargetPinned } from '../utils/virtualTargetMutations';
import { import {
applyDefaultGlobalStrategyToSession,
ensureDefaultGlobalStrategyId,
freezeTargetsBeforeGlobalStrategyChange, freezeTargetsBeforeGlobalStrategyChange,
resolveVirtualTargetStrategyId, resolveVirtualTargetStrategyId,
resolveStrategyIdForNewTarget,
} from '../utils/virtualTargetStrategy'; } from '../utils/virtualTargetStrategy';
import { import {
isVirtualTargetAddAllowed, isVirtualTargetAddAllowed,
@@ -143,12 +146,13 @@ export function useVirtualTradingCore(options: UseVirtualTradingCoreOptions = {}
const validIds = new Set( const validIds = new Set(
s.map(x => x.id).filter((id): id is number => id != null && id > 0), s.map(x => x.id).filter((id): id is number => id != null && id > 0),
); );
setSession(prev => { setSession(prev =>
const next = prev.globalStrategyId != null && !validIds.has(prev.globalStrategyId) applyDefaultGlobalStrategyToSession(
? { ...prev, globalStrategyId: null } mergeVirtualSessionWithAppLive(prev, appLiveStrategyId),
: prev; s,
return mergeVirtualSessionWithAppLive(next, appLiveStrategyId); appLiveStrategyId,
}); ),
);
setTargets(prev => setTargets(prev =>
prev.map(t => prev.map(t =>
t.strategyId != null && !validIds.has(t.strategyId) t.strategyId != null && !validIds.has(t.strategyId)
@@ -285,13 +289,13 @@ export function useVirtualTradingCore(options: UseVirtualTradingCoreOptions = {}
const names = resolveVirtualTargetNames(market, meta?.koreanName, meta?.englishName); const names = resolveVirtualTargetNames(market, meta?.koreanName, meta?.englishName);
setTargets(prev => [...prev, { setTargets(prev => [...prev, {
market, market,
strategyId: session.globalStrategyId, strategyId: resolveStrategyIdForNewTarget(session.globalStrategyId, strategies),
koreanName: names.koreanName, koreanName: names.koreanName,
englishName: names.englishName, englishName: names.englishName,
}]); }]);
setSelectedMarket(market); setSelectedMarket(market);
return true; return true;
}, [targets, session.globalStrategyId, virtualTargetMaxCount]); }, [targets, session.globalStrategyId, strategies, virtualTargetMaxCount]);
const handleRemoveTarget = useCallback((market: string) => { const handleRemoveTarget = useCallback((market: string) => {
setTargets(prev => prev.filter(x => x.market !== market)); setTargets(prev => prev.filter(x => x.market !== market));
@@ -320,20 +324,25 @@ export function useVirtualTradingCore(options: UseVirtualTradingCoreOptions = {}
window.alert('투자대상 종목을 먼저 추가하세요.'); window.alert('투자대상 종목을 먼저 추가하세요.');
return; return;
} }
if (!session.globalStrategyId) { const globalStrategyId = ensureDefaultGlobalStrategyId(session.globalStrategyId, strategies);
if (!globalStrategyId) {
window.alert('투자전략을 선택하세요.'); window.alert('투자전략을 선택하세요.');
return; return;
} }
const globalStrat = strategies.find(s => s.id === session.globalStrategyId); const sessionForStart = { ...session, globalStrategyId };
if (session.globalStrategyId !== globalStrategyId) {
setSession(sessionForStart);
}
const globalStrat = strategies.find(s => s.id === globalStrategyId);
const synced = await syncStrategyTimeframesFromLayoutIfNeeded( const synced = await syncStrategyTimeframesFromLayoutIfNeeded(
session.globalStrategyId, globalStrategyId,
globalStrat, globalStrat,
); );
if (!synced) return; if (!synced) return;
const ok = await warnStrategyTimeframeMismatch(session.globalStrategyId, globalStrat); const ok = await warnStrategyTimeframeMismatch(globalStrategyId, globalStrat);
if (!ok) return; if (!ok) return;
try { try {
await syncVirtualTargetsToBackend(targets, session, true); await syncVirtualTargetsToBackend(targets, sessionForStart, true);
setSession(s => ({ ...s, running: true })); setSession(s => ({ ...s, running: true }));
} catch { } catch {
window.alert('가상투자 시작 설정 저장에 실패했습니다.'); window.alert('가상투자 시작 설정 저장에 실패했습니다.');
+10 -5
View File
@@ -1,11 +1,14 @@
import { saveLiveStrategySettings } from './backendApi'; import { loadStrategies, saveLiveStrategySettings } from './backendApi';
import { getVirtualTargetMaxCount } from '../hooks/useAppSettings'; import { getVirtualTargetMaxCount } from '../hooks/useAppSettings';
import { import {
isVirtualTargetAddAllowed, isVirtualTargetAddAllowed,
virtualTargetLimitMessage, virtualTargetLimitMessage,
} from '../utils/virtualTargetLimits'; } from '../utils/virtualTargetLimits';
import { syncVirtualTargetsToBackend } from './virtualLiveStrategySync'; import { syncVirtualTargetsToBackend } from './virtualLiveStrategySync';
import { resolveVirtualTargetStrategyId } from './virtualTargetStrategy'; import {
ensureDefaultGlobalStrategyId,
resolveVirtualTargetStrategyId,
} from './virtualTargetStrategy';
import { import {
loadVirtualSession, loadVirtualSession,
loadVirtualTargets, loadVirtualTargets,
@@ -41,11 +44,13 @@ export async function addVirtualTarget(
} }
const session = loadVirtualSession(); const session = loadVirtualSession();
const strategies = await loadStrategies().catch(() => []);
const strategyId = ensureDefaultGlobalStrategyId(session.globalStrategyId, strategies);
const en = meta.englishName ?? market.replace(/^KRW-/, ''); const en = meta.englishName ?? market.replace(/^KRW-/, '');
const item: VirtualTargetItem = { const item: VirtualTargetItem = {
market, market,
/** 신규 추가 시점의 상단 기본전략(템플릿)을 종목에 고정 */ /** 신규 추가 시점의 상단 기본전략(템플릿)을 종목에 고정 */
strategyId: session.globalStrategyId, strategyId,
koreanName: meta.koreanName, koreanName: meta.koreanName,
englishName: en, englishName: en,
}; };
@@ -55,7 +60,7 @@ export async function addVirtualTarget(
await saveLiveStrategySettings({ await saveLiveStrategySettings({
market, market,
strategyId: session.globalStrategyId, strategyId,
isLiveCheck: session.running, isLiveCheck: session.running,
isPinned: false, isPinned: false,
executionType: session.executionType, executionType: session.executionType,
@@ -64,7 +69,7 @@ export async function addVirtualTarget(
skipGlobalTemplate: true, skipGlobalTemplate: true,
}); });
if (session.running && session.globalStrategyId) { if (session.running && strategyId) {
await syncVirtualTargetsToBackend(next, session, true); await syncVirtualTargetsToBackend(next, session, true);
} }
+48 -1
View File
@@ -1,4 +1,4 @@
import type { VirtualTargetItem } from './virtualTradingStorage'; import type { VirtualTargetItem, VirtualSessionConfig } from './virtualTradingStorage';
/** 종목 전략 셀렉트 — 기본(전역) 전략 옵션 값 */ /** 종목 전략 셀렉트 — 기본(전역) 전략 옵션 값 */
export const VIRTUAL_DEFAULT_STRATEGY_VALUE = '__default__'; export const VIRTUAL_DEFAULT_STRATEGY_VALUE = '__default__';
@@ -49,6 +49,53 @@ export function parseTargetStrategySelectValue(value: string): number | null {
return Number.isFinite(n) ? n : null; return Number.isFinite(n) ? n : null;
} }
/** 전략 목록 첫 항목 ID (가상매매 기본 전략) */
export function pickDefaultGlobalStrategyId(
strategies: Array<{ id?: number | null }>,
): number | null {
for (const s of strategies) {
const id = s.id;
if (id != null && id > 0) return id;
}
return null;
}
/** 저장된 globalStrategyId 가 유효하면 유지, 아니면 목록 첫 항목 */
export function ensureDefaultGlobalStrategyId(
globalStrategyId: number | null,
strategies: Array<{ id?: number | null }>,
): number | null {
const validIds = new Set(
strategies.map(s => s.id).filter((id): id is number => id != null && id > 0),
);
if (globalStrategyId != null && validIds.has(globalStrategyId)) {
return globalStrategyId;
}
return pickDefaultGlobalStrategyId(strategies);
}
/** 신규 종목 추가·세션 초기화 — 상단 기본전략 (없으면 목록 첫 항목) */
export function resolveStrategyIdForNewTarget(
globalStrategyId: number | null,
strategies: Array<{ id?: number | null }>,
): number | null {
return ensureDefaultGlobalStrategyId(globalStrategyId, strategies);
}
/** 전략 목록 로드 후 — 상단 기본전략을 첫 항목으로 자동 설정 */
export function applyDefaultGlobalStrategyToSession(
session: VirtualSessionConfig,
strategies: Array<{ id?: number | null }>,
appLiveStrategyId: number | null = null,
): VirtualSessionConfig {
let gid = ensureDefaultGlobalStrategyId(session.globalStrategyId, strategies);
if (gid == null && appLiveStrategyId != null && appLiveStrategyId > 0) {
gid = appLiveStrategyId;
}
if (gid === session.globalStrategyId) return session;
return { ...session, globalStrategyId: gid };
}
/** 종목 셀렉트 — 기본 전략(전역) 옵션 라벨 (신규 추가 시 적용되는 템플릿) */ /** 종목 셀렉트 — 기본 전략(전역) 옵션 라벨 (신규 추가 시 적용되는 템플릿) */
export function defaultStrategyOptionLabel( export function defaultStrategyOptionLabel(
globalStrategyId: number | null, globalStrategyId: number | null,