가상투자 화면 추가

This commit is contained in:
Macbook
2026-05-25 13:14:36 +09:00
parent 1fe3e404fa
commit 339e2355eb
23 changed files with 2230 additions and 457 deletions
+79 -87
View File
@@ -10,6 +10,7 @@ import {
type StrategyDto,
} from '../utils/backendApi';
import type { Theme, TradeOrderFillRequest } from '../types';
import type { TickerData } from '../hooks/useMarketTicker';
import TradeOrderPanel from './TradeOrderPanel';
import PaperCompactOrderbook from './paper/PaperCompactOrderbook';
import PaperSplitPanel from './paper/PaperSplitPanel';
@@ -18,6 +19,7 @@ import VirtualLeftTargetPanel from './virtual/VirtualLeftTargetPanel';
import VirtualTargetGrid from './virtual/VirtualTargetGrid';
import { useVirtualIndicatorSnapshots } from '../hooks/useVirtualIndicatorSnapshots';
import { useVirtualTargetLiveStatus } from '../hooks/useVirtualTargetLiveStatus';
import type { ChartRealtimeSource } from '../hooks/useChartRealtimeData';
import {
loadVirtualSession,
loadVirtualTargets,
@@ -26,13 +28,14 @@ import {
type VirtualSessionConfig,
type VirtualTargetItem,
} from '../utils/virtualTradingStorage';
import { useAppSettings, resolveAppDefaults } from '../hooks/useAppSettings';
import '../styles/virtualTradingDashboard.css';
type RightTab = 'trade' | 'orderbook';
interface Props {
theme?: Theme;
tickers?: Map<string, { tradePrice: number | null }>;
tickers?: Map<string, TickerData>;
defaultMarket?: string;
paperTradingEnabled?: boolean;
paperAutoTradeEnabled?: boolean;
@@ -61,6 +64,11 @@ const VirtualTradingPage: React.FC<Props> = ({
const [fillBuy, setFillBuy] = useState<TradeOrderFillRequest | null>(null);
const [fillSell, setFillSell] = useState<TradeOrderFillRequest | null>(null);
const orderAnchorRef = useRef<HTMLDivElement>(null);
const { settings: appSettings } = useAppSettings();
const appChartDefaults = resolveAppDefaults(appSettings);
const chartRealtimeSource = (appChartDefaults.chartRealtimeSource
?? 'BACKEND_STOMP') as ChartRealtimeSource;
const chartSeriesPriceLabels = appChartDefaults.chartSeriesPriceLabels;
useEffect(() => {
loadStrategies().then(setStrategies).catch(() => setStrategies([]));
@@ -78,7 +86,10 @@ const VirtualTradingPage: React.FC<Props> = ({
[targets, session.globalStrategyId]);
const snapshots = useVirtualIndicatorSnapshots(targetRefs, strategies, session.running);
const liveStatusByMarket = useVirtualTargetLiveStatus(targetRefs, session.running);
const { statusByMarket: liveStatusByMarket, lastTickAtByMarket } = useVirtualTargetLiveStatus(
targetRefs,
session.running,
);
const mergedLiveStatus = useMemo(() => {
if (!session.running) return liveStatusByMarket;
@@ -164,63 +175,31 @@ const VirtualTradingPage: React.FC<Props> = ({
setRightTab('trade');
}, [selectedMarket]);
const headerControls = (
<div className="vtd-header-controls">
<label className="vtd-header-field">
<span></span>
<select
className="vtd-header-select"
value={session.globalStrategyId ?? ''}
onChange={e => {
const v = e.target.value;
handleGlobalStrategyChange(v ? Number(v) : null);
}}
>
<option value=""> </option>
{strategies.map(s => (
<option key={s.id} value={s.id}>{s.name}</option>
))}
</select>
</label>
<label className="vtd-header-field">
<span></span>
<select
className="vtd-header-select"
value={session.executionType}
onChange={e => setSession(s => ({
...s,
executionType: e.target.value === 'REALTIME_TICK' ? 'REALTIME_TICK' : 'CANDLE_CLOSE',
}))}
>
<option value="CANDLE_CLOSE"> </option>
<option value="REALTIME_TICK"> </option>
</select>
</label>
<label className="vtd-header-field">
<span> </span>
<select
className="vtd-header-select"
value={session.positionMode}
onChange={e => setSession(s => ({
...s,
positionMode: e.target.value === 'SIGNAL_ONLY' ? 'SIGNAL_ONLY' : 'LONG_ONLY',
}))}
>
<option value="LONG_ONLY"> </option>
<option value="SIGNAL_ONLY"> </option>
</select>
</label>
{!session.running ? (
<button type="button" className="bps-btn bps-btn--primary" onClick={() => void handleStart()}>
</button>
) : (
<button type="button" className="bps-btn bps-btn--danger" onClick={() => void handleStop()}>
</button>
)}
</div>
);
const handleTargetStrategyChange = useCallback(async (market: string, strategyId: number | null) => {
setTargets(prev => prev.map(t =>
t.market === market ? { ...t, strategyId } : t,
));
if (!session.running || !strategyId) return;
try {
await saveLiveStrategySettings({
market,
strategyId,
isLiveCheck: true,
executionType: session.executionType,
positionMode: session.positionMode,
});
} catch {
window.alert('종목 전략 변경 저장에 실패했습니다.');
}
}, [session]);
const handleExecutionTypeChange = useCallback((executionType: VirtualSessionConfig['executionType']) => {
setSession(s => ({ ...s, executionType }));
}, []);
const handlePositionModeChange = useCallback((positionMode: VirtualSessionConfig['positionMode']) => {
setSession(s => ({ ...s, positionMode }));
}, []);
const rightTabs = (
<div className="bps-right-tabs">
@@ -240,15 +219,16 @@ const VirtualTradingPage: React.FC<Props> = ({
leftDefaultWidth={380}
rightStorageKey="vtd-right-width"
rightDefaultWidth={380}
headerActions={headerControls}
left={(
<VirtualLeftTargetPanel
targets={targets}
globalStrategyId={session.globalStrategyId}
strategies={strategies}
selectedMarket={selectedMarket}
tickers={tickers}
onTargetsChange={handleTargetsChange}
onSelectMarket={setSelectedMarket}
onTargetStrategyChange={(market, strategyId) => void handleTargetStrategyChange(market, strategyId)}
/>
)}
center={(
@@ -256,10 +236,18 @@ const VirtualTradingPage: React.FC<Props> = ({
targets={targets}
strategies={strategies}
snapshots={snapshots}
running={session.running}
globalStrategyId={session.globalStrategyId}
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}
theme={theme}
chartRealtimeSource={chartRealtimeSource}
chartSeriesPriceLabels={chartSeriesPriceLabels}
/>
)}
rightTabs={rightTabs}
@@ -271,33 +259,37 @@ const VirtualTradingPage: React.FC<Props> = ({
topTitle="매수"
bottomTitle="매도"
top={(
<TradeOrderPanel
side="buy"
market={selectedMarket}
tradePrice={tradePrice}
availableKrw={summary?.cashBalance ?? 0}
fillRequest={fillBuy}
searchAnchorRef={orderAnchorRef}
onMarketSelect={setSelectedMarket}
showSymbolField
paperTradingEnabled={paperTradingEnabled}
paperAutoTradeEnabled={paperAutoTradeEnabled}
onPaperOrderFilled={onPaperOrderFilled}
/>
<div className="ptd-order-card">
<TradeOrderPanel
side="buy"
market={selectedMarket}
tradePrice={tradePrice}
availableKrw={summary?.cashBalance ?? 0}
fillRequest={fillBuy}
searchAnchorRef={orderAnchorRef}
onMarketSelect={setSelectedMarket}
showSymbolField
paperTradingEnabled={paperTradingEnabled}
paperAutoTradeEnabled={paperAutoTradeEnabled}
onPaperOrderFilled={onPaperOrderFilled}
/>
</div>
)}
bottom={(
<TradeOrderPanel
side="sell"
market={selectedMarket}
tradePrice={tradePrice}
availableCoinQty={posQty}
fillRequest={fillSell}
onMarketSelect={setSelectedMarket}
showSymbolField={false}
paperTradingEnabled={paperTradingEnabled}
paperAutoTradeEnabled={paperAutoTradeEnabled}
onPaperOrderFilled={onPaperOrderFilled}
/>
<div className="ptd-order-card">
<TradeOrderPanel
side="sell"
market={selectedMarket}
tradePrice={tradePrice}
availableCoinQty={posQty}
fillRequest={fillSell}
onMarketSelect={setSelectedMarket}
showSymbolField={false}
paperTradingEnabled={paperTradingEnabled}
paperAutoTradeEnabled={paperAutoTradeEnabled}
onPaperOrderFilled={onPaperOrderFilled}
/>
</div>
)}
/>
) : (