가상투자 화면 추가
This commit is contained in:
@@ -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>
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
|
||||
Reference in New Issue
Block a user