가상투자 설정 수정

This commit is contained in:
Macbook
2026-05-25 22:30:54 +09:00
parent cae1c3a624
commit 1e950c7db4
24 changed files with 1015 additions and 362 deletions
+106 -32
View File
@@ -84,6 +84,9 @@ import {
type LiveStrategySettingsDto,
type BacktestSignal,
} from './utils/backendApi';
import { useVirtualLiveStrategy } from './hooks/useVirtualLiveStrategy';
import { VIRTUAL_SESSION_CHANGED_EVENT } from './utils/virtualTradingStorage';
import { notifyPaperTradesChanged } from './utils/paperTradeEvents';
import { useIsMobile } from './hooks/useMediaQuery';
import MobileChartDock from './components/MobileChartDock';
import LoginModal from './components/LoginModal';
@@ -583,6 +586,11 @@ function App() {
const [paperCoinQty, setPaperCoinQty] = useState(0);
const [paperRefreshKey, setPaperRefreshKey] = useState(0);
const handlePaperOrderFilled = useCallback(() => {
setPaperRefreshKey(k => k + 1);
notifyPaperTradesChanged();
}, []);
const refreshPaperAccount = useCallback(async (forMarket?: string) => {
const summary = await loadPaperSummary();
if (!summary) return;
@@ -698,55 +706,104 @@ function App() {
syncBacktestMarkersRef.current = syncBacktestMarkers;
// ── 실시간 전략 체크 (전역 설정 + DB 관심종목 = 체크 대상) ─────────────────
// ── 실시간 전략 체크 (가상투자 세션 우선, 없으면 전역+관심종목) ─────────────
const [showLivePanel, setShowLivePanel] = useState(false);
const [liveStrategies, setLiveStrategies] = useState<{id:number;name:string}[]>([]);
const [liveStrategyCandleTypes, setLiveStrategyCandleTypes] = useState<string[] | undefined>();
const [activeLiveSettings, setActiveLiveSettings] = useState<LiveStrategySettingsDto[]>([]);
const { session: virtualSession, isVirtualActive, monitoredMarkets: virtualMonitoredMarkets } = useVirtualLiveStrategy();
const refreshActiveLiveSettings = useCallback(() => {
loadActiveLiveStrategySettings()
.then(setActiveLiveSettings)
.catch(() => setActiveLiveSettings([]));
}, []);
useEffect(() => {
refreshActiveLiveSettings();
}, [refreshActiveLiveSettings, isVirtualActive, virtualMonitoredMarkets.join(','), virtualSession.executionType, virtualSession.positionMode]);
useEffect(() => {
const onVirtualChanged = () => refreshActiveLiveSettings();
window.addEventListener(VIRTUAL_SESSION_CHANGED_EVENT, onVirtualChanged);
return () => window.removeEventListener(VIRTUAL_SESSION_CHANGED_EVENT, onVirtualChanged);
}, [refreshActiveLiveSettings]);
useEffect(() => {
if (!appSettingsLoaded) return;
if (isVirtualActive) {
const active = activeLiveSettings.find(s => s.market === symbol);
setLiveStrategyCandleTypes(active?.strategyCandleTypes);
return;
}
loadLiveStrategySettings(symbol)
.then(s => setLiveStrategyCandleTypes(s.strategyCandleTypes))
.catch(() => { /* optional */ });
}, [symbol, appSettingsLoaded, appDefaults.liveStrategyId]);
}, [symbol, appSettingsLoaded, appDefaults.liveStrategyId, isVirtualActive, activeLiveSettings]);
const liveStrategySettings: LiveStrategySettingsDto = useMemo(() => ({
market: symbol,
strategyId: appDefaults.liveStrategyId ?? null,
isLiveCheck: appDefaults.liveStrategyCheck ?? false,
executionType: appDefaults.liveExecutionType ?? 'CANDLE_CLOSE',
positionMode: appDefaults.livePositionMode ?? 'LONG_ONLY',
strategyCandleTypes: liveStrategyCandleTypes,
}), [symbol, appDefaults, liveStrategyCandleTypes]);
const liveStrategySettings: LiveStrategySettingsDto = useMemo(() => {
if (isVirtualActive) {
const active = activeLiveSettings.find(s => s.market === symbol);
return {
market: symbol,
strategyId: active?.strategyId ?? virtualSession.globalStrategyId,
isLiveCheck: true,
executionType: active?.executionType ?? virtualSession.executionType,
positionMode: active?.positionMode ?? virtualSession.positionMode,
strategyCandleTypes: active?.strategyCandleTypes ?? liveStrategyCandleTypes,
};
}
return {
market: symbol,
strategyId: appDefaults.liveStrategyId ?? null,
isLiveCheck: appDefaults.liveStrategyCheck ?? false,
executionType: appDefaults.liveExecutionType ?? 'CANDLE_CLOSE',
positionMode: appDefaults.livePositionMode ?? 'LONG_ONLY',
strategyCandleTypes: liveStrategyCandleTypes,
};
}, [isVirtualActive, activeLiveSettings, symbol, virtualSession, appDefaults, liveStrategyCandleTypes]);
/** 실시간 체크 ON + 전략 선택 시 STOMP 구독 대상 (관심종목 + 현재 차트 종목) */
/** STOMP 구독 대상 — 가상투자 실행 중이면 투자대상 종목, 아니면 관심종목 */
const monitoredMarkets = useMemo(() => {
if (isVirtualActive) return virtualMonitoredMarkets;
if (!appDefaults.liveStrategyCheck || !appDefaults.liveStrategyId) return [];
const set = new Set(watchlist);
if (symbol) set.add(symbol);
return [...set];
}, [watchlist, symbol, appDefaults.liveStrategyCheck, appDefaults.liveStrategyId]);
}, [isVirtualActive, virtualMonitoredMarkets, watchlist, symbol, appDefaults.liveStrategyCheck, appDefaults.liveStrategyId]);
const [marketSubscriptions, setMarketSubscriptions] = useState<{ market: string; candleType: string }[]>([]);
/** STOMP 구독 목록 — API 로드 전에도 관심종목 × 1m 폴백 구독 */
const liveStompSubscriptions = useMemo(() => {
if (!appDefaults.liveStrategyCheck || !appDefaults.liveStrategyId) return [];
if (monitoredMarkets.length === 0) return [];
const filtered = activeLiveSettings.filter(s => monitoredMarkets.includes(s.market));
if (filtered.length > 0) return expandLiveStrategySubscriptions(filtered);
if (marketSubscriptions.length > 0) return marketSubscriptions;
return monitoredMarkets.map(m => ({ market: m, candleType: '1m' }));
}, [appDefaults.liveStrategyCheck, appDefaults.liveStrategyId, marketSubscriptions, monitoredMarkets]);
}, [monitoredMarkets, activeLiveSettings, marketSubscriptions]);
useEffect(() => {
if (monitoredMarkets.length === 0) {
setMarketSubscriptions([]);
return;
}
if (isVirtualActive) {
setMarketSubscriptions(expandLiveStrategySubscriptions(
activeLiveSettings.filter(s => monitoredMarkets.includes(s.market)),
));
return;
}
if (!appDefaults.liveStrategyCheck) {
setMarketSubscriptions([]);
return;
}
loadActiveLiveStrategySettings()
.then(list => setMarketSubscriptions(expandLiveStrategySubscriptions(list)))
.catch(() => { /* liveStompSubscriptions 가 monitoredMarkets 폴백 사용 */ });
}, [appDefaults.liveStrategyCheck, appDefaults.liveStrategyId, monitoredMarkets.join(',')]);
.catch(() => { /* liveStompSubscriptions 폴백 */ });
}, [isVirtualActive, appDefaults.liveStrategyCheck, appDefaults.liveStrategyId, monitoredMarkets.join(','), activeLiveSettings]);
const handleLiveSettingsChange = useCallback((saved: LiveStrategySettingsDto) => {
if (isVirtualActive) return;
saveAppDef({
liveStrategyCheck: saved.isLiveCheck,
liveStrategyId: saved.strategyId ?? undefined,
@@ -757,11 +814,9 @@ function App() {
setLiveStrategyCandleTypes(saved.strategyCandleTypes);
}
if (appDefaults.liveStrategyCheck && saved.isLiveCheck) {
loadActiveLiveStrategySettings()
.then(list => setMarketSubscriptions(expandLiveStrategySubscriptions(list)))
.catch(() => { /* 폴백 유지 */ });
refreshActiveLiveSettings();
}
}, [saveAppDef, appDefaults.liveStrategyCheck]);
}, [saveAppDef, appDefaults.liveStrategyCheck, isVirtualActive, refreshActiveLiveSettings]);
const prevFavoritesRef = useRef<string[]>(getFavorites());
@@ -808,10 +863,21 @@ function App() {
return () => { cancelled = true; };
}, [dbLoaded]);
const liveStrategyName = useMemo(
() => liveStrategies.find(s => s.id === appDefaults.liveStrategyId)?.name ?? null,
[liveStrategies, appDefaults.liveStrategyId],
);
const liveStrategyName = useMemo(() => {
const sid = liveStrategySettings.strategyId;
return sid != null ? liveStrategies.find(s => s.id === sid)?.name ?? null : null;
}, [liveStrategies, liveStrategySettings.strategyId]);
const liveStrategyNameByMarket = useMemo(() => {
const map: Record<string, string> = {};
for (const s of activeLiveSettings) {
if (s.strategyId != null) {
const name = liveStrategies.find(st => st.id === s.strategyId)?.name;
if (name) map[s.market] = name;
}
}
return map;
}, [activeLiveSettings, liveStrategies]);
const goToMarketChart = useCallback((market: string) => {
if (layoutDef.count > 1) {
@@ -1477,9 +1543,13 @@ function App() {
activeMarket={symbol}
enabled={monitoredMarkets.length > 0}
popupEnabled={appDefaults.tradeAlertPopup ?? true}
strategyId={appDefaults.liveStrategyId ?? undefined}
executionType={appDefaults.liveExecutionType ?? 'CANDLE_CLOSE'}
strategyId={liveStrategySettings.strategyId ?? undefined}
executionType={liveStrategySettings.executionType ?? 'CANDLE_CLOSE'}
strategyName={liveStrategyName}
strategyNameByMarket={liveStrategyNameByMarket}
executionTypeByMarket={Object.fromEntries(
activeLiveSettings.map(s => [s.market, s.executionType ?? 'CANDLE_CLOSE']),
)}
chartMarkersActive={menuPage === 'chart'}
onMarkersChange={markers => {
managerRef.current?.setLiveStrategyMarkers(markers, appDefaults.btShowPrice);
@@ -1537,7 +1607,7 @@ function App() {
defaultMarket={symbol}
paperTradingEnabled={paperTradingEnabled}
paperAutoTradeEnabled={paperAutoTradeEnabled}
onPaperOrderFilled={() => setPaperRefreshKey(k => k + 1)}
onPaperOrderFilled={handlePaperOrderFilled}
onGoChart={m => {
goToMarketChart(m);
setMenuPage('chart');
@@ -1552,7 +1622,9 @@ function App() {
defaultMarket={symbol}
paperTradingEnabled={paperTradingEnabled}
paperAutoTradeEnabled={paperAutoTradeEnabled}
onPaperOrderFilled={() => setPaperRefreshKey(k => k + 1)}
paperAutoTradeBudgetPct={appDefaults.paperAutoTradeBudgetPct}
onPaperAutoTradeEnabled={v => saveAppDef({ paperAutoTradeEnabled: v })}
onPaperOrderFilled={handlePaperOrderFilled}
/>
)}
@@ -1596,6 +1668,7 @@ function App() {
onLiveSettingsChange={handleLiveSettingsChange}
watchlistCount={watchlist.length}
monitoredMarkets={monitoredMarkets}
virtualDriven={isVirtualActive}
getIndicatorParams={getIndicatorParams}
saveIndicatorParams={saveIndicatorParams}
getIndicatorVisual={getIndicatorVisual}
@@ -2080,8 +2153,9 @@ function App() {
market={symbol}
strategies={liveStrategies}
settings={liveStrategySettings}
watchlistCount={watchlist.length}
watchlistCount={isVirtualActive ? monitoredMarkets.length : watchlist.length}
monitoredMarkets={monitoredMarkets}
virtualDriven={isVirtualActive}
onClearMarkers={() => { clearLiveMarkers(); managerRef.current?.clearLiveStrategyMarkers(); }}
onSettingsChange={handleLiveSettingsChange}
onClose={() => setShowLivePanel(false)}
@@ -2303,7 +2377,7 @@ function App() {
paperAutoTradeEnabled={paperAutoTradeEnabled}
onPaperOrderFilled={() => {
refreshPaperAccount(symbol);
setPaperRefreshKey(k => k + 1);
handlePaperOrderFilled();
}}
/>
@@ -2407,7 +2481,7 @@ function App() {
tradeAlertPopupGridCols={appDefaults.tradeAlertPopupGridCols}
onOrderDone={() => {
refreshPaperAccount(symbol);
setPaperRefreshKey(k => k + 1);
handlePaperOrderFilled();
}}
/>