시간봉 알림 수정
This commit is contained in:
@@ -51,7 +51,9 @@ import { coerceFiniteNumber } from '../utils/safeFormat';
|
|||||||
import {
|
import {
|
||||||
syncVirtualTargetsToBackend,
|
syncVirtualTargetsToBackend,
|
||||||
stopVirtualLiveOnBackend,
|
stopVirtualLiveOnBackend,
|
||||||
|
warnStrategyTimeframeMismatch,
|
||||||
} from '../utils/virtualLiveStrategySync';
|
} from '../utils/virtualLiveStrategySync';
|
||||||
|
import { pinStrategyEvaluationTimeframes } from '../utils/strategyTimeframePin';
|
||||||
import { persistVirtualTargetPinned } from '../utils/virtualTargetMutations';
|
import { persistVirtualTargetPinned } from '../utils/virtualTargetMutations';
|
||||||
import { virtualTargetLimitMessage } from '../utils/virtualTargetLimits';
|
import { virtualTargetLimitMessage } from '../utils/virtualTargetLimits';
|
||||||
import '../styles/virtualTradingDashboard.css';
|
import '../styles/virtualTradingDashboard.css';
|
||||||
@@ -269,13 +271,16 @@ const VirtualTradingPage: React.FC<Props> = ({
|
|||||||
window.alert('투자전략을 선택하세요.');
|
window.alert('투자전략을 선택하세요.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const globalStrat = strategies.find(s => s.id === session.globalStrategyId);
|
||||||
|
const ok = await warnStrategyTimeframeMismatch(globalStrat, session.globalStrategyId);
|
||||||
|
if (!ok) return;
|
||||||
try {
|
try {
|
||||||
await syncVirtualTargetsToBackend(targets, session, true);
|
await syncVirtualTargetsToBackend(targets, session, true);
|
||||||
setSession(s => ({ ...s, running: true }));
|
setSession(s => ({ ...s, running: true }));
|
||||||
} catch {
|
} catch {
|
||||||
window.alert('가상투자 시작 설정 저장에 실패했습니다.');
|
window.alert('가상투자 시작 설정 저장에 실패했습니다.');
|
||||||
}
|
}
|
||||||
}, [targets, session]);
|
}, [targets, session, strategies]);
|
||||||
|
|
||||||
const handleStop = useCallback(async () => {
|
const handleStop = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
@@ -298,24 +303,21 @@ const VirtualTradingPage: React.FC<Props> = ({
|
|||||||
t.market === market ? { ...t, strategyId } : t,
|
t.market === market ? { ...t, strategyId } : t,
|
||||||
));
|
));
|
||||||
if (!session.running || !strategyId) return;
|
if (!session.running || !strategyId) return;
|
||||||
const target = targets.find(t => t.market === market);
|
|
||||||
const candleType = normalizeStartCandleType(
|
|
||||||
resolveTargetCandleType(target ?? { candleType: undefined }, snapshots[market]?.timeframe),
|
|
||||||
);
|
|
||||||
try {
|
try {
|
||||||
await saveLiveStrategySettings({
|
await saveLiveStrategySettings({
|
||||||
market,
|
market,
|
||||||
strategyId,
|
strategyId,
|
||||||
candleType,
|
|
||||||
isLiveCheck: true,
|
isLiveCheck: true,
|
||||||
executionType: session.executionType,
|
executionType: session.executionType,
|
||||||
positionMode: session.positionMode,
|
positionMode: session.positionMode,
|
||||||
skipWatchlistSync: true,
|
skipWatchlistSync: true,
|
||||||
|
skipGlobalTemplate: true,
|
||||||
});
|
});
|
||||||
|
await pinStrategyEvaluationTimeframes(market, strategyId);
|
||||||
} catch {
|
} catch {
|
||||||
window.alert('종목 전략 변경 저장에 실패했습니다.');
|
window.alert('종목 전략 변경 저장에 실패했습니다.');
|
||||||
}
|
}
|
||||||
}, [session, targets, snapshots]);
|
}, [session, targets]);
|
||||||
|
|
||||||
const handleTargetCandleTypeChange = useCallback(async (market: string, candleType: string) => {
|
const handleTargetCandleTypeChange = useCallback(async (market: string, candleType: string) => {
|
||||||
const normalized = normalizeStartCandleType(candleType);
|
const normalized = normalizeStartCandleType(candleType);
|
||||||
@@ -330,15 +332,14 @@ const VirtualTradingPage: React.FC<Props> = ({
|
|||||||
await saveLiveStrategySettings({
|
await saveLiveStrategySettings({
|
||||||
market,
|
market,
|
||||||
strategyId,
|
strategyId,
|
||||||
candleType: normalized,
|
|
||||||
isLiveCheck: true,
|
isLiveCheck: true,
|
||||||
executionType: session.executionType,
|
executionType: session.executionType,
|
||||||
positionMode: session.positionMode,
|
positionMode: session.positionMode,
|
||||||
skipWatchlistSync: true,
|
skipWatchlistSync: true,
|
||||||
|
skipGlobalTemplate: true,
|
||||||
});
|
});
|
||||||
await pinCandleWatch(market, normalized);
|
if (strategyId) {
|
||||||
if (normalized !== '1m') {
|
await pinStrategyEvaluationTimeframes(market, strategyId);
|
||||||
await pinCandleWatch(market, '1m');
|
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
window.alert('종목 평가 분봉 변경 저장에 실패했습니다.');
|
window.alert('종목 평가 분봉 변경 저장에 실패했습니다.');
|
||||||
|
|||||||
@@ -1123,6 +1123,12 @@ export async function fetchLiveConditionStatus(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 전략 DSL에 포함된 평가 시간봉 목록 (실시간 체크·STOMP 구독용) */
|
||||||
|
export async function loadStrategyTimeframes(strategyId: number): Promise<string[]> {
|
||||||
|
const list = await request<string[]>(`/strategies/${strategyId}/timeframes`);
|
||||||
|
return list?.length ? list : ['1m'];
|
||||||
|
}
|
||||||
|
|
||||||
/** 차트 실시간 파이프라인 pin (STOMP warm-up) */
|
/** 차트 실시간 파이프라인 pin (STOMP warm-up) */
|
||||||
export async function pinCandleWatch(market: string, candleType: string): Promise<void> {
|
export async function pinCandleWatch(market: string, candleType: string): Promise<void> {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { loadStrategyTimeframes, pinCandleWatch } from './backendApi';
|
||||||
|
import { normalizeStartCandleType } from './strategyStartNodes';
|
||||||
|
|
||||||
|
/** 전략 DSL 평가 분봉 전체 pin — 1m 집계·상위 봉 warm-up */
|
||||||
|
export async function pinStrategyEvaluationTimeframes(
|
||||||
|
market: string,
|
||||||
|
strategyId: number,
|
||||||
|
): Promise<string[]> {
|
||||||
|
const raw = await loadStrategyTimeframes(strategyId);
|
||||||
|
const timeframes = [...new Set(raw.map(tf => normalizeStartCandleType(tf)))];
|
||||||
|
if (timeframes.length === 0) timeframes.push('1m');
|
||||||
|
|
||||||
|
for (const tf of timeframes) {
|
||||||
|
await pinCandleWatch(market, tf);
|
||||||
|
}
|
||||||
|
if (!timeframes.includes('1m')) {
|
||||||
|
await pinCandleWatch(market, '1m');
|
||||||
|
}
|
||||||
|
return timeframes;
|
||||||
|
}
|
||||||
@@ -1,7 +1,53 @@
|
|||||||
import { saveLiveStrategySettings } from './backendApi';
|
import { loadStrategyTimeframes, saveLiveStrategySettings } from './backendApi';
|
||||||
|
import { collectDslBranches } from './strategyConditionSerde';
|
||||||
|
import { pinStrategyEvaluationTimeframes } from './strategyTimeframePin';
|
||||||
import { normalizeStartCandleType } from './strategyStartNodes';
|
import { normalizeStartCandleType } from './strategyStartNodes';
|
||||||
|
import type { StrategyDto } from './backendApi';
|
||||||
|
import type { LogicNode } from './strategyTypes';
|
||||||
import type { VirtualSessionConfig, VirtualTargetItem } from './virtualTradingStorage';
|
import type { VirtualSessionConfig, VirtualTargetItem } from './virtualTradingStorage';
|
||||||
import { resolveTargetCandleType } from './virtualTradingStorage';
|
|
||||||
|
function collectUiTimeframes(strategy: StrategyDto | null | undefined): string[] {
|
||||||
|
if (!strategy) return [];
|
||||||
|
const set = new Set<string>();
|
||||||
|
const buy = strategy.buyCondition as LogicNode | null | undefined;
|
||||||
|
const sell = strategy.sellCondition as LogicNode | null | undefined;
|
||||||
|
for (const b of [
|
||||||
|
...collectDslBranches(buy ?? null),
|
||||||
|
...collectDslBranches(sell ?? null),
|
||||||
|
]) {
|
||||||
|
set.add(normalizeStartCandleType(b.candleType));
|
||||||
|
}
|
||||||
|
return [...set];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UI(편집기)와 서버 DSL의 평가 분봉이 다르면 안내.
|
||||||
|
* @returns false — 계속 진행 불가(재저장 필요)
|
||||||
|
*/
|
||||||
|
export async function warnStrategyTimeframeMismatch(
|
||||||
|
strategy: StrategyDto | null | undefined,
|
||||||
|
strategyId: number,
|
||||||
|
): Promise<boolean> {
|
||||||
|
if (!strategy) return true;
|
||||||
|
const uiTfs = collectUiTimeframes(strategy);
|
||||||
|
if (uiTfs.length === 0 || (uiTfs.length === 1 && uiTfs[0] === '1m')) return true;
|
||||||
|
|
||||||
|
let serverTfs: string[];
|
||||||
|
try {
|
||||||
|
serverTfs = await loadStrategyTimeframes(strategyId);
|
||||||
|
} catch {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const serverSet = new Set(serverTfs.map(tf => normalizeStartCandleType(tf)));
|
||||||
|
const missingOnServer = uiTfs.filter(tf => !serverSet.has(tf));
|
||||||
|
if (missingOnServer.length === 0) return true;
|
||||||
|
|
||||||
|
window.alert(
|
||||||
|
`전략 화면에는 ${uiTfs.join(', ')} 봉으로 보이지만, 서버에는 ${[...serverSet].join(', ') || '1m'} 만 저장되어 있습니다.\n\n`
|
||||||
|
+ '전략편집기에서 「저장」을 눌러 다시 저장한 뒤 가상매매를 시작하세요.',
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/** 가상투자 대상 종목을 백엔드 live strategy 설정과 동기화 */
|
/** 가상투자 대상 종목을 백엔드 live strategy 설정과 동기화 */
|
||||||
export async function syncVirtualTargetsToBackend(
|
export async function syncVirtualTargetsToBackend(
|
||||||
@@ -18,23 +64,35 @@ export async function syncVirtualTargetsToBackend(
|
|||||||
skipWatchlistSync: true,
|
skipWatchlistSync: true,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
if (isLiveCheck && session.globalStrategyId != null) {
|
||||||
|
await saveLiveStrategySettings({
|
||||||
|
market: targets[0].market,
|
||||||
|
strategyId: session.globalStrategyId,
|
||||||
|
isLiveCheck: true,
|
||||||
|
executionType: session.executionType,
|
||||||
|
positionMode: session.positionMode,
|
||||||
|
skipWatchlistSync: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const pinJobs = targets.map(async t => {
|
||||||
|
const strategyId = t.strategyId ?? session.globalStrategyId;
|
||||||
|
if (strategyId == null) return;
|
||||||
|
await pinStrategyEvaluationTimeframes(t.market, strategyId);
|
||||||
|
});
|
||||||
|
await Promise.all(pinJobs);
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
targets.map(t =>
|
targets.map(t =>
|
||||||
saveLiveStrategySettings({
|
saveLiveStrategySettings({
|
||||||
market: t.market,
|
market: t.market,
|
||||||
strategyId: t.strategyId ?? session.globalStrategyId,
|
strategyId: t.strategyId ?? session.globalStrategyId,
|
||||||
candleType: normalizeStartCandleType(resolveTargetCandleType(t)),
|
|
||||||
isPinned: !!t.pinned,
|
isPinned: !!t.pinned,
|
||||||
|
skipGlobalTemplate: true,
|
||||||
...shared,
|
...shared,
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
await saveLiveStrategySettings({
|
|
||||||
market: targets[0].market,
|
|
||||||
strategyId: session.globalStrategyId,
|
|
||||||
...shared,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 가상투자 중지 — 대상 종목 체크 해제 + 전역 liveStrategyCheck OFF */
|
/** 가상투자 중지 — 대상 종목 체크 해제 + 전역 liveStrategyCheck OFF */
|
||||||
|
|||||||
Reference in New Issue
Block a user