가상투자 설정 수정
This commit is contained in:
@@ -4,9 +4,11 @@
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
loadPaperSummary,
|
||||
loadPaperTrades,
|
||||
loadStrategies,
|
||||
saveLiveStrategySettings,
|
||||
type PaperSummaryDto,
|
||||
type PaperTradeDto,
|
||||
type StrategyDto,
|
||||
} from '../utils/backendApi';
|
||||
import type { Theme, TradeOrderFillRequest } from '../types';
|
||||
@@ -14,10 +16,17 @@ import type { TickerData } from '../hooks/useMarketTicker';
|
||||
import TradeOrderPanel from './TradeOrderPanel';
|
||||
import PaperCompactOrderbook from './paper/PaperCompactOrderbook';
|
||||
import PaperSplitPanel from './paper/PaperSplitPanel';
|
||||
import PaperTradeHistoryList from './paper/PaperTradeHistoryList';
|
||||
import BuilderPageShell from './layout/BuilderPageShell';
|
||||
import VirtualLeftTargetPanel from './virtual/VirtualLeftTargetPanel';
|
||||
import VirtualTargetGrid from './virtual/VirtualTargetGrid';
|
||||
import {
|
||||
VirtualSessionHeaderControls,
|
||||
VirtualViewHeaderControls,
|
||||
} from './virtual/VirtualGridUnifiedHeader';
|
||||
import type { VirtualCardDisplayMode } from './virtual/VirtualTargetCard';
|
||||
import { useVirtualIndicatorSnapshots } from '../hooks/useVirtualIndicatorSnapshots';
|
||||
import { useVirtualAutoTrade } from '../hooks/useVirtualAutoTrade';
|
||||
import { useVirtualTargetLiveStatus } from '../hooks/useVirtualTargetLiveStatus';
|
||||
import type { ChartRealtimeSource } from '../hooks/useChartRealtimeData';
|
||||
import {
|
||||
@@ -25,14 +34,21 @@ import {
|
||||
loadVirtualTargets,
|
||||
saveVirtualSession,
|
||||
saveVirtualTargets,
|
||||
loadVirtualCardViewMode,
|
||||
saveVirtualCardViewMode,
|
||||
type VirtualSessionConfig,
|
||||
type VirtualTargetItem,
|
||||
type VirtualCardViewMode,
|
||||
} from '../utils/virtualTradingStorage';
|
||||
import { useAppSettings, resolveAppDefaults } from '../hooks/useAppSettings';
|
||||
import { coerceFiniteNumber } from '../utils/safeFormat';
|
||||
import {
|
||||
syncVirtualTargetsToBackend,
|
||||
stopVirtualLiveOnBackend,
|
||||
} from '../utils/virtualLiveStrategySync';
|
||||
import '../styles/virtualTradingDashboard.css';
|
||||
|
||||
type RightTab = 'trade' | 'orderbook';
|
||||
type RightTab = 'trade' | 'orderbook' | 'history';
|
||||
|
||||
interface Props {
|
||||
theme?: Theme;
|
||||
@@ -40,30 +56,34 @@ interface Props {
|
||||
defaultMarket?: string;
|
||||
paperTradingEnabled?: boolean;
|
||||
paperAutoTradeEnabled?: boolean;
|
||||
paperAutoTradeBudgetPct?: number;
|
||||
onPaperAutoTradeEnabled?: (enabled: boolean) => void;
|
||||
onPaperOrderFilled?: () => void;
|
||||
}
|
||||
|
||||
function coinCode(symbol: string): string {
|
||||
return symbol.replace(/^KRW-/, '');
|
||||
}
|
||||
|
||||
const VirtualTradingPage: React.FC<Props> = ({
|
||||
theme = 'dark',
|
||||
tickers,
|
||||
defaultMarket = 'KRW-BTC',
|
||||
paperTradingEnabled = true,
|
||||
paperAutoTradeEnabled = false,
|
||||
paperAutoTradeBudgetPct = 95,
|
||||
onPaperAutoTradeEnabled,
|
||||
onPaperOrderFilled,
|
||||
}) => {
|
||||
const [targets, setTargets] = useState<VirtualTargetItem[]>(() => loadVirtualTargets());
|
||||
const [session, setSession] = useState<VirtualSessionConfig>(() => loadVirtualSession());
|
||||
const [strategies, setStrategies] = useState<StrategyDto[]>([]);
|
||||
const [summary, setSummary] = useState<PaperSummaryDto | null>(null);
|
||||
const [trades, setTrades] = useState<PaperTradeDto[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [selectedMarket, setSelectedMarket] = useState(defaultMarket);
|
||||
const [rightTab, setRightTab] = useState<RightTab>('trade');
|
||||
const [fillBuy, setFillBuy] = useState<TradeOrderFillRequest | null>(null);
|
||||
const [fillSell, setFillSell] = useState<TradeOrderFillRequest | null>(null);
|
||||
const [viewMode, setViewMode] = useState<VirtualCardViewMode>(() => loadVirtualCardViewMode());
|
||||
const [globalDisplayMode, setGlobalDisplayMode] = useState<VirtualCardDisplayMode>('signal');
|
||||
const [focusMarket, setFocusMarket] = useState<string | null>(null);
|
||||
const orderAnchorRef = useRef<HTMLDivElement>(null);
|
||||
const { settings: appSettings } = useAppSettings();
|
||||
const appChartDefaults = resolveAppDefaults(appSettings);
|
||||
@@ -72,12 +92,35 @@ const VirtualTradingPage: React.FC<Props> = ({
|
||||
const chartSeriesPriceLabels = appChartDefaults.chartSeriesPriceLabels;
|
||||
|
||||
useEffect(() => {
|
||||
loadStrategies().then(setStrategies).catch(() => setStrategies([]));
|
||||
loadPaperSummary().then(setSummary).finally(() => setLoading(false));
|
||||
void Promise.all([
|
||||
loadStrategies().then(setStrategies).catch(() => setStrategies([])),
|
||||
loadPaperSummary().then(setSummary),
|
||||
loadPaperTrades().then(setTrades),
|
||||
]).finally(() => setLoading(false));
|
||||
}, []);
|
||||
|
||||
useEffect(() => { saveVirtualTargets(targets); }, [targets]);
|
||||
useEffect(() => { saveVirtualSession(session); }, [session]);
|
||||
useEffect(() => { saveVirtualCardViewMode(viewMode); }, [viewMode]);
|
||||
|
||||
const strategyNames = useMemo(
|
||||
() => Object.fromEntries(strategies.map(s => [s.id, s.name])),
|
||||
[strategies],
|
||||
);
|
||||
|
||||
const refreshPaperData = useCallback(async (opts?: { focusHistory?: boolean }) => {
|
||||
try {
|
||||
const [sum, tr] = await Promise.all([loadPaperSummary(), loadPaperTrades()]);
|
||||
setSummary(sum);
|
||||
setTrades(tr);
|
||||
} catch { /* ignore */ }
|
||||
onPaperOrderFilled?.();
|
||||
if (opts?.focusHistory) setRightTab('history');
|
||||
}, [onPaperOrderFilled]);
|
||||
|
||||
const handleGlobalDisplayModeChange = useCallback((mode: VirtualCardDisplayMode) => {
|
||||
setGlobalDisplayMode(mode);
|
||||
}, []);
|
||||
|
||||
const targetRefs = useMemo(() =>
|
||||
targets.map(t => ({
|
||||
@@ -92,6 +135,17 @@ const VirtualTradingPage: React.FC<Props> = ({
|
||||
session.running,
|
||||
);
|
||||
|
||||
useVirtualAutoTrade({
|
||||
enabled: paperTradingEnabled && paperAutoTradeEnabled,
|
||||
session,
|
||||
targets,
|
||||
snapshots,
|
||||
tickers,
|
||||
paperAutoTradeBudgetPct,
|
||||
positions: summary?.positions,
|
||||
onFilled: () => void refreshPaperData({ focusHistory: true }),
|
||||
});
|
||||
|
||||
const mergedLiveStatus = useMemo(() => {
|
||||
if (!session.running) return liveStatusByMarket;
|
||||
const merged = { ...liveStatusByMarket };
|
||||
@@ -133,8 +187,12 @@ const VirtualTradingPage: React.FC<Props> = ({
|
||||
}, [selectedMarket, handleSelectMarket]);
|
||||
|
||||
const handleGlobalStrategyChange = useCallback((strategyId: number | null) => {
|
||||
setSession(s => ({ ...s, globalStrategyId: strategyId }));
|
||||
}, []);
|
||||
const next = { ...session, globalStrategyId: strategyId };
|
||||
setSession(next);
|
||||
if (session.running && strategyId) {
|
||||
void syncVirtualTargetsToBackend(targets, next, true);
|
||||
}
|
||||
}, [session, targets]);
|
||||
|
||||
const handleStart = useCallback(async () => {
|
||||
if (targets.length === 0) {
|
||||
@@ -145,19 +203,8 @@ const VirtualTradingPage: React.FC<Props> = ({
|
||||
window.alert('투자전략을 선택하세요.');
|
||||
return;
|
||||
}
|
||||
const template = {
|
||||
isLiveCheck: true,
|
||||
executionType: session.executionType,
|
||||
positionMode: session.positionMode,
|
||||
};
|
||||
try {
|
||||
await Promise.all(targets.map(t =>
|
||||
saveLiveStrategySettings({
|
||||
market: t.market,
|
||||
strategyId: t.strategyId ?? session.globalStrategyId,
|
||||
...template,
|
||||
}),
|
||||
));
|
||||
await syncVirtualTargetsToBackend(targets, session, true);
|
||||
setSession(s => ({ ...s, running: true }));
|
||||
} catch {
|
||||
window.alert('가상투자 시작 설정 저장에 실패했습니다.');
|
||||
@@ -165,20 +212,8 @@ const VirtualTradingPage: React.FC<Props> = ({
|
||||
}, [targets, session]);
|
||||
|
||||
const handleStop = useCallback(async () => {
|
||||
if (targets.length === 0) {
|
||||
setSession(s => ({ ...s, running: false }));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await Promise.all(targets.map(t =>
|
||||
saveLiveStrategySettings({
|
||||
market: t.market,
|
||||
strategyId: t.strategyId ?? session.globalStrategyId,
|
||||
isLiveCheck: false,
|
||||
executionType: session.executionType,
|
||||
positionMode: session.positionMode,
|
||||
}),
|
||||
));
|
||||
await stopVirtualLiveOnBackend(targets, session);
|
||||
} catch { /* ignore */ }
|
||||
setSession(s => ({ ...s, running: false }));
|
||||
}, [targets, session]);
|
||||
@@ -202,24 +237,41 @@ const VirtualTradingPage: React.FC<Props> = ({
|
||||
isLiveCheck: true,
|
||||
executionType: session.executionType,
|
||||
positionMode: session.positionMode,
|
||||
skipWatchlistSync: true,
|
||||
});
|
||||
} catch {
|
||||
window.alert('종목 전략 변경 저장에 실패했습니다.');
|
||||
}
|
||||
}, [session]);
|
||||
|
||||
const resyncRunningSession = useCallback(async (nextSession: VirtualSessionConfig) => {
|
||||
if (!session.running || targets.length === 0) return;
|
||||
try {
|
||||
await syncVirtualTargetsToBackend(targets, nextSession, true);
|
||||
} catch {
|
||||
window.alert('가상투자 설정 동기화에 실패했습니다.');
|
||||
}
|
||||
}, [session.running, targets]);
|
||||
|
||||
const handleExecutionTypeChange = useCallback((executionType: VirtualSessionConfig['executionType']) => {
|
||||
setSession(s => ({ ...s, executionType }));
|
||||
}, []);
|
||||
const next = { ...session, executionType };
|
||||
setSession(next);
|
||||
void resyncRunningSession(next);
|
||||
}, [session, resyncRunningSession]);
|
||||
|
||||
const handlePositionModeChange = useCallback((positionMode: VirtualSessionConfig['positionMode']) => {
|
||||
setSession(s => ({ ...s, positionMode }));
|
||||
}, []);
|
||||
const next = { ...session, positionMode };
|
||||
setSession(next);
|
||||
void resyncRunningSession(next);
|
||||
}, [session, resyncRunningSession]);
|
||||
|
||||
const rightTabs = (
|
||||
<div className="bps-right-tabs">
|
||||
<button type="button" className={`bps-right-tab${rightTab === 'trade' ? ' bps-right-tab--on' : ''}`} onClick={() => setRightTab('trade')}>매매</button>
|
||||
<button type="button" className={`bps-right-tab${rightTab === 'orderbook' ? ' bps-right-tab--on' : ''}`} onClick={() => setRightTab('orderbook')}>호가</button>
|
||||
<button type="button" className={`bps-right-tab${rightTab === 'history' ? ' bps-right-tab--on' : ''}`} onClick={() => setRightTab('history')}>
|
||||
거래{trades.length > 0 ? ` (${trades.length})` : ''}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -228,9 +280,34 @@ const VirtualTradingPage: React.FC<Props> = ({
|
||||
theme={theme}
|
||||
title="가상투자"
|
||||
subtitle="Virtual Trading"
|
||||
pageClassName="bps-page--vtd"
|
||||
loading={loading}
|
||||
loadingText="가상투자 대시보드 로딩…"
|
||||
collapsiblePanels
|
||||
headerCenter={(
|
||||
<VirtualSessionHeaderControls
|
||||
session={session}
|
||||
strategies={strategies}
|
||||
onStrategyChange={handleGlobalStrategyChange}
|
||||
onExecutionTypeChange={handleExecutionTypeChange}
|
||||
onPositionModeChange={handlePositionModeChange}
|
||||
onStart={() => void handleStart()}
|
||||
onStop={() => void handleStop()}
|
||||
paperAutoTradeEnabled={paperAutoTradeEnabled}
|
||||
onPaperAutoTradeEnabled={onPaperAutoTradeEnabled}
|
||||
paperTradingEnabled={paperTradingEnabled}
|
||||
/>
|
||||
)}
|
||||
headerActions={(
|
||||
<VirtualViewHeaderControls
|
||||
session={session}
|
||||
viewMode={viewMode}
|
||||
onViewModeChange={setViewMode}
|
||||
globalDisplayMode={globalDisplayMode}
|
||||
onGlobalDisplayModeChange={handleGlobalDisplayModeChange}
|
||||
showSignalChartToggle={!focusMarket}
|
||||
/>
|
||||
)}
|
||||
leftStorageKey="vtd-left-width"
|
||||
leftDefaultWidth={380}
|
||||
leftCollapsedStorageKey="vtd-left-open"
|
||||
@@ -255,11 +332,6 @@ const VirtualTradingPage: React.FC<Props> = ({
|
||||
strategies={strategies}
|
||||
snapshots={snapshots}
|
||||
session={session}
|
||||
onStrategyChange={handleGlobalStrategyChange}
|
||||
onExecutionTypeChange={handleExecutionTypeChange}
|
||||
onPositionModeChange={handlePositionModeChange}
|
||||
onStart={() => void handleStart()}
|
||||
onStop={() => void handleStop()}
|
||||
onTargetStrategyChange={(market, strategyId) => void handleTargetStrategyChange(market, strategyId)}
|
||||
liveStatusByMarket={mergedLiveStatus}
|
||||
lastTickAtByMarket={lastTickAtByMarket}
|
||||
@@ -269,6 +341,9 @@ const VirtualTradingPage: React.FC<Props> = ({
|
||||
chartRealtimeSource={chartRealtimeSource}
|
||||
chartSeriesPriceLabels={chartSeriesPriceLabels}
|
||||
tickers={tickers}
|
||||
viewMode={viewMode}
|
||||
globalDisplayMode={globalDisplayMode}
|
||||
onFocusMarketChange={setFocusMarket}
|
||||
/>
|
||||
)}
|
||||
rightTabs={rightTabs}
|
||||
@@ -292,7 +367,7 @@ const VirtualTradingPage: React.FC<Props> = ({
|
||||
showSymbolField
|
||||
paperTradingEnabled={paperTradingEnabled}
|
||||
paperAutoTradeEnabled={paperAutoTradeEnabled}
|
||||
onPaperOrderFilled={onPaperOrderFilled}
|
||||
onPaperOrderFilled={() => void refreshPaperData({ focusHistory: true })}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -308,12 +383,12 @@ const VirtualTradingPage: React.FC<Props> = ({
|
||||
showSymbolField={false}
|
||||
paperTradingEnabled={paperTradingEnabled}
|
||||
paperAutoTradeEnabled={paperAutoTradeEnabled}
|
||||
onPaperOrderFilled={onPaperOrderFilled}
|
||||
onPaperOrderFilled={() => void refreshPaperData({ focusHistory: true })}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
) : rightTab === 'orderbook' ? (
|
||||
<PaperSplitPanel
|
||||
className="ptd-split-panel--right"
|
||||
topTitle="매도 호가"
|
||||
@@ -339,6 +414,14 @@ const VirtualTradingPage: React.FC<Props> = ({
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<PaperTradeHistoryList
|
||||
trades={trades}
|
||||
strategyNames={strategyNames}
|
||||
onSelectMarket={handleSelectMarket}
|
||||
emptyText="수동·자동 매매 체결 내역이 없습니다."
|
||||
className="ptd-trade-history--fill"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user