가상매매 기본전략 설정
This commit is contained in:
@@ -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));
|
||||||
|
|||||||
@@ -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('가상투자 시작 설정 저장에 실패했습니다.');
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user