서버 가상매매 목록 지표없음 문제 개선
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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,20 +118,60 @@ 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([
|
||||
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 =>
|
||||
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)
|
||||
@@ -133,37 +180,65 @@ const VirtualTradingPage: React.FC<Props> = ({
|
||||
),
|
||||
);
|
||||
})
|
||||
.catch(() => setStrategies([])),
|
||||
loadPaperSummary().then(setSummary),
|
||||
loadPaperTrades().then(setTrades),
|
||||
]).finally(() => setLoading(false));
|
||||
}, []);
|
||||
.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,6 +52,8 @@ const VirtualTargetSignalPanel: React.FC<Props> = ({
|
||||
<p className={`vtd-muted vtd-card-empty${loading ? ' vtd-card-empty--loading' : ''}${className ? ` ${className}` : ''}`}>
|
||||
{loading
|
||||
? '지표 데이터 수집 중… (업비트 캔들 · 실시간 연결)'
|
||||
: !hasStrategy
|
||||
? '투자전략을 선택하세요 (상단 또는 종목별 전략)'
|
||||
: '전략 조건·지표 데이터 없음'}
|
||||
</p>
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 로 고정 */
|
||||
|
||||
Reference in New Issue
Block a user