가상투자 설정 수정
This commit is contained in:
@@ -3,7 +3,7 @@ import type { StrategyDto } from '../../utils/backendApi';
|
||||
import type { VirtualSessionConfig, VirtualCardViewMode } from '../../utils/virtualTradingStorage';
|
||||
import type { VirtualCardDisplayMode } from './VirtualTargetCard';
|
||||
|
||||
interface Props {
|
||||
interface SessionProps {
|
||||
session: Pick<VirtualSessionConfig, 'globalStrategyId' | 'executionType' | 'positionMode' | 'running'>;
|
||||
strategies: StrategyDto[];
|
||||
onStrategyChange: (strategyId: number | null) => void;
|
||||
@@ -11,15 +11,21 @@ interface Props {
|
||||
onPositionModeChange: (mode: VirtualSessionConfig['positionMode']) => void;
|
||||
onStart: () => void;
|
||||
onStop: () => void;
|
||||
viewMode?: VirtualCardViewMode;
|
||||
onViewModeChange?: (mode: VirtualCardViewMode) => void;
|
||||
globalDisplayMode?: VirtualCardDisplayMode;
|
||||
onGlobalDisplayModeChange?: (mode: VirtualCardDisplayMode) => void;
|
||||
/** 우측 뷰 토글: full=신호·차트+요약·상세, display-only=요약·상세만, hidden=숨김 */
|
||||
viewControls?: 'full' | 'display-only' | 'hidden';
|
||||
paperAutoTradeEnabled?: boolean;
|
||||
onPaperAutoTradeEnabled?: (enabled: boolean) => void;
|
||||
paperTradingEnabled?: boolean;
|
||||
}
|
||||
|
||||
const VirtualGridUnifiedHeader: React.FC<Props> = ({
|
||||
interface ViewProps {
|
||||
session: Pick<VirtualSessionConfig, 'running'>;
|
||||
viewMode: VirtualCardViewMode;
|
||||
onViewModeChange: (mode: VirtualCardViewMode) => void;
|
||||
globalDisplayMode?: VirtualCardDisplayMode;
|
||||
onGlobalDisplayModeChange?: (mode: VirtualCardDisplayMode) => void;
|
||||
showSignalChartToggle?: boolean;
|
||||
}
|
||||
|
||||
export const VirtualSessionHeaderControls: React.FC<SessionProps> = ({
|
||||
session,
|
||||
strategies,
|
||||
onStrategyChange,
|
||||
@@ -27,155 +33,187 @@ const VirtualGridUnifiedHeader: React.FC<Props> = ({
|
||||
onPositionModeChange,
|
||||
onStart,
|
||||
onStop,
|
||||
viewMode = 'summary',
|
||||
onViewModeChange,
|
||||
globalDisplayMode = 'signal',
|
||||
onGlobalDisplayModeChange,
|
||||
viewControls = 'full',
|
||||
paperAutoTradeEnabled = false,
|
||||
onPaperAutoTradeEnabled,
|
||||
paperTradingEnabled = true,
|
||||
}) => (
|
||||
<div className="vtd-unified-head">
|
||||
<span className="vtd-unified-head-title">종목별 매매시그널 일치 현황</span>
|
||||
<div className="vtd-header-session">
|
||||
<div className="vtd-session-field">
|
||||
<span className="vtd-session-label">투자전략</span>
|
||||
<select
|
||||
className="vtd-session-select"
|
||||
value={session.globalStrategyId ?? ''}
|
||||
onChange={e => {
|
||||
const v = e.target.value;
|
||||
onStrategyChange(v ? Number(v) : null);
|
||||
}}
|
||||
>
|
||||
<option value="">— 선택 —</option>
|
||||
{strategies.map(s => (
|
||||
<option key={s.id} value={s.id}>{s.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="vtd-unified-head-center">
|
||||
<div className="vtd-session-field">
|
||||
<span className="vtd-session-label">투자전략</span>
|
||||
<select
|
||||
className="vtd-session-select"
|
||||
value={session.globalStrategyId ?? ''}
|
||||
onChange={e => {
|
||||
const v = e.target.value;
|
||||
onStrategyChange(v ? Number(v) : null);
|
||||
}}
|
||||
>
|
||||
<option value="">— 선택 —</option>
|
||||
{strategies.map(s => (
|
||||
<option key={s.id} value={s.id}>{s.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="vtd-session-field">
|
||||
<span className="vtd-session-label">실행방식</span>
|
||||
<div className="vtd-view-toggle" role="group" aria-label="실행방식">
|
||||
<button
|
||||
type="button"
|
||||
className={`vtd-view-toggle-btn${session.executionType === 'CANDLE_CLOSE' ? ' vtd-view-toggle-btn--on' : ''}`}
|
||||
onClick={() => onExecutionTypeChange('CANDLE_CLOSE')}
|
||||
>
|
||||
봉 마감 직후
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`vtd-view-toggle-btn${session.executionType === 'REALTIME_TICK' ? ' vtd-view-toggle-btn--on' : ''}`}
|
||||
onClick={() => onExecutionTypeChange('REALTIME_TICK')}
|
||||
>
|
||||
실시간 틱
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="vtd-session-field">
|
||||
<span className="vtd-session-label">시그널 모드</span>
|
||||
<div className="vtd-view-toggle" role="group" aria-label="시그널 모드">
|
||||
<button
|
||||
type="button"
|
||||
className={`vtd-view-toggle-btn${session.positionMode === 'LONG_ONLY' ? ' vtd-view-toggle-btn--on' : ''}`}
|
||||
onClick={() => onPositionModeChange('LONG_ONLY')}
|
||||
>
|
||||
보유 자산 기준
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`vtd-view-toggle-btn${session.positionMode === 'SIGNAL_ONLY' ? ' vtd-view-toggle-btn--on' : ''}`}
|
||||
onClick={() => onPositionModeChange('SIGNAL_ONLY')}
|
||||
>
|
||||
순수 지표 기준
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="vtd-session-actions">
|
||||
<div className="vtd-session-field">
|
||||
<span className="vtd-session-label">실행방식</span>
|
||||
<div className="vtd-view-toggle" role="group" aria-label="실행방식">
|
||||
<button
|
||||
type="button"
|
||||
className="bps-btn vtd-session-btn vtd-session-btn--start"
|
||||
disabled={session.running}
|
||||
onClick={onStart}
|
||||
className={`vtd-view-toggle-btn${session.executionType === 'CANDLE_CLOSE' ? ' vtd-view-toggle-btn--on' : ''}`}
|
||||
onClick={() => onExecutionTypeChange('CANDLE_CLOSE')}
|
||||
>
|
||||
시작
|
||||
봉 마감 직후
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="bps-btn bps-btn--danger vtd-session-btn"
|
||||
disabled={!session.running}
|
||||
onClick={onStop}
|
||||
className={`vtd-view-toggle-btn${session.executionType === 'REALTIME_TICK' ? ' vtd-view-toggle-btn--on' : ''}`}
|
||||
onClick={() => onExecutionTypeChange('REALTIME_TICK')}
|
||||
>
|
||||
종료
|
||||
실시간 틱
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{viewControls !== 'hidden' && onViewModeChange && (
|
||||
<div className="vtd-unified-head-right">
|
||||
{viewControls === 'full' && onGlobalDisplayModeChange && (
|
||||
<>
|
||||
<div className="vtd-grid-head-group" role="group" aria-label="전체 종목 신호·차트">
|
||||
<div className="vtd-view-toggle">
|
||||
<button
|
||||
type="button"
|
||||
className={`vtd-view-toggle-btn${globalDisplayMode === 'signal' ? ' vtd-view-toggle-btn--on' : ''}`}
|
||||
onClick={() => onGlobalDisplayModeChange('signal')}
|
||||
>
|
||||
신호
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`vtd-view-toggle-btn${globalDisplayMode === 'chart' ? ' vtd-view-toggle-btn--on' : ''}`}
|
||||
onClick={() => onGlobalDisplayModeChange('chart')}
|
||||
>
|
||||
차트
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<span className="vtd-grid-head-divider" aria-hidden="true" />
|
||||
</>
|
||||
)}
|
||||
<div className="vtd-grid-head-group" role="group" aria-label="카드 표시 방식">
|
||||
<div className="vtd-view-toggle">
|
||||
<button
|
||||
type="button"
|
||||
className={`vtd-view-toggle-btn${viewMode === 'summary' ? ' vtd-view-toggle-btn--on' : ''}`}
|
||||
onClick={() => onViewModeChange('summary')}
|
||||
>
|
||||
요약표시
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`vtd-view-toggle-btn${viewMode === 'detail' ? ' vtd-view-toggle-btn--on' : ''}`}
|
||||
onClick={() => onViewModeChange('detail')}
|
||||
>
|
||||
상세표시
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{viewMode === 'detail' && (
|
||||
<>
|
||||
<span className="vtd-grid-head-divider" aria-hidden="true" />
|
||||
<div className="vtd-grid-head-legend">
|
||||
<span className="vtd-legend vtd-legend--match">● 충족</span>
|
||||
<span className="vtd-legend vtd-legend--pending">● 대기</span>
|
||||
<span className="vtd-legend vtd-legend--nomatch">● 미충족</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{session.running && (
|
||||
<>
|
||||
<span className="vtd-grid-head-divider" aria-hidden="true" />
|
||||
<span className="vtd-grid-live">실시간 갱신 3초</span>
|
||||
</>
|
||||
)}
|
||||
<div className="vtd-session-field">
|
||||
<span className="vtd-session-label">시그널 모드</span>
|
||||
<div className="vtd-view-toggle" role="group" aria-label="시그널 모드">
|
||||
<button
|
||||
type="button"
|
||||
className={`vtd-view-toggle-btn${session.positionMode === 'LONG_ONLY' ? ' vtd-view-toggle-btn--on' : ''}`}
|
||||
onClick={() => onPositionModeChange('LONG_ONLY')}
|
||||
>
|
||||
보유 자산 기준
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`vtd-view-toggle-btn${session.positionMode === 'SIGNAL_ONLY' ? ' vtd-view-toggle-btn--on' : ''}`}
|
||||
onClick={() => onPositionModeChange('SIGNAL_ONLY')}
|
||||
>
|
||||
순수 지표 기준
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="vtd-session-field">
|
||||
<span className="vtd-session-label">자동매매</span>
|
||||
<div
|
||||
className="vtd-view-toggle"
|
||||
role="group"
|
||||
aria-label="자동매매"
|
||||
title={
|
||||
!paperTradingEnabled
|
||||
? '설정 › 가상투자에서 가상투자를 활성화하세요'
|
||||
: paperAutoTradeEnabled
|
||||
? 'Match 충족 시 자동 매수·매도'
|
||||
: '수동 주문만 가능'
|
||||
}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className={`vtd-view-toggle-btn${paperAutoTradeEnabled ? ' vtd-view-toggle-btn--on' : ''}`}
|
||||
disabled={!paperTradingEnabled || !onPaperAutoTradeEnabled}
|
||||
onClick={() => onPaperAutoTradeEnabled?.(true)}
|
||||
>
|
||||
ON
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`vtd-view-toggle-btn${!paperAutoTradeEnabled ? ' vtd-view-toggle-btn--on' : ''}`}
|
||||
disabled={!paperTradingEnabled || !onPaperAutoTradeEnabled}
|
||||
onClick={() => onPaperAutoTradeEnabled?.(false)}
|
||||
>
|
||||
OFF
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="vtd-session-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="bps-btn vtd-session-btn vtd-session-btn--start"
|
||||
disabled={session.running}
|
||||
onClick={onStart}
|
||||
>
|
||||
시작
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="bps-btn bps-btn--danger vtd-session-btn"
|
||||
disabled={!session.running}
|
||||
onClick={onStop}
|
||||
>
|
||||
종료
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default VirtualGridUnifiedHeader;
|
||||
export const VirtualViewHeaderControls: React.FC<ViewProps> = ({
|
||||
session,
|
||||
viewMode,
|
||||
onViewModeChange,
|
||||
globalDisplayMode = 'signal',
|
||||
onGlobalDisplayModeChange,
|
||||
showSignalChartToggle = true,
|
||||
}) => (
|
||||
<div className="vtd-header-view">
|
||||
{showSignalChartToggle && onGlobalDisplayModeChange && (
|
||||
<>
|
||||
<div className="vtd-grid-head-group" role="group" aria-label="전체 종목 신호·차트">
|
||||
<div className="vtd-view-toggle">
|
||||
<button
|
||||
type="button"
|
||||
className={`vtd-view-toggle-btn${globalDisplayMode === 'signal' ? ' vtd-view-toggle-btn--on' : ''}`}
|
||||
onClick={() => onGlobalDisplayModeChange('signal')}
|
||||
>
|
||||
신호
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`vtd-view-toggle-btn${globalDisplayMode === 'chart' ? ' vtd-view-toggle-btn--on' : ''}`}
|
||||
onClick={() => onGlobalDisplayModeChange('chart')}
|
||||
>
|
||||
차트
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<span className="vtd-grid-head-divider" aria-hidden="true" />
|
||||
</>
|
||||
)}
|
||||
<div className="vtd-grid-head-group" role="group" aria-label="카드 표시 방식">
|
||||
<div className="vtd-view-toggle">
|
||||
<button
|
||||
type="button"
|
||||
className={`vtd-view-toggle-btn${viewMode === 'summary' ? ' vtd-view-toggle-btn--on' : ''}`}
|
||||
onClick={() => onViewModeChange('summary')}
|
||||
>
|
||||
요약표시
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`vtd-view-toggle-btn${viewMode === 'detail' ? ' vtd-view-toggle-btn--on' : ''}`}
|
||||
onClick={() => onViewModeChange('detail')}
|
||||
>
|
||||
상세표시
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{viewMode === 'detail' && (
|
||||
<>
|
||||
<span className="vtd-grid-head-divider" aria-hidden="true" />
|
||||
<div className="vtd-grid-head-legend">
|
||||
<span className="vtd-legend vtd-legend--match">● 충족</span>
|
||||
<span className="vtd-legend vtd-legend--pending">● 대기</span>
|
||||
<span className="vtd-legend vtd-legend--nomatch">● 미충족</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{session.running && (
|
||||
<>
|
||||
<span className="vtd-grid-head-divider" aria-hidden="true" />
|
||||
<span className="vtd-grid-live">실시간 갱신 3초</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import VirtualTargetCard, { type VirtualCardDisplayMode } from './VirtualTargetCard';
|
||||
import VirtualGridUnifiedHeader from './VirtualGridUnifiedHeader';
|
||||
import VirtualTargetFocusView from './VirtualTargetFocusView';
|
||||
import type { StrategyDto } from '../../utils/backendApi';
|
||||
import type { Theme } from '../../types';
|
||||
import type { ChartRealtimeSource } from '../../hooks/useChartRealtimeData';
|
||||
import type { VirtualTargetItem, VirtualCardViewMode, VirtualSessionConfig } from '../../utils/virtualTradingStorage';
|
||||
import { loadVirtualCardViewMode, saveVirtualCardViewMode } from '../../utils/virtualTradingStorage';
|
||||
import type { VirtualLiveStatus } from '../../hooks/useVirtualTargetLiveStatus';
|
||||
import type { VirtualIndicatorSnapshot } from '../../hooks/useVirtualIndicatorSnapshots';
|
||||
import type { TickerData } from '../../hooks/useMarketTicker';
|
||||
@@ -16,11 +14,6 @@ interface Props {
|
||||
strategies: StrategyDto[];
|
||||
snapshots: Record<string, VirtualIndicatorSnapshot>;
|
||||
session: Pick<VirtualSessionConfig, 'globalStrategyId' | 'executionType' | 'positionMode' | 'running'>;
|
||||
onStrategyChange: (strategyId: number | null) => void;
|
||||
onExecutionTypeChange: (type: VirtualSessionConfig['executionType']) => void;
|
||||
onPositionModeChange: (mode: VirtualSessionConfig['positionMode']) => void;
|
||||
onStart: () => void;
|
||||
onStop: () => void;
|
||||
onTargetStrategyChange: (market: string, strategyId: number | null) => void;
|
||||
liveStatusByMarket?: Record<string, VirtualLiveStatus>;
|
||||
lastTickAtByMarket?: Record<string, number>;
|
||||
@@ -30,11 +23,13 @@ interface Props {
|
||||
chartRealtimeSource?: ChartRealtimeSource;
|
||||
chartSeriesPriceLabels?: boolean;
|
||||
tickers?: Map<string, TickerData>;
|
||||
viewMode: VirtualCardViewMode;
|
||||
globalDisplayMode: VirtualCardDisplayMode;
|
||||
onFocusMarketChange?: (market: string | null) => void;
|
||||
}
|
||||
|
||||
const VirtualTargetGrid: React.FC<Props> = ({
|
||||
targets, strategies, snapshots, session,
|
||||
onStrategyChange, onExecutionTypeChange, onPositionModeChange, onStart, onStop,
|
||||
onTargetStrategyChange,
|
||||
liveStatusByMarket = {}, lastTickAtByMarket = {}, selectedMarket,
|
||||
onSelectMarket,
|
||||
@@ -42,9 +37,10 @@ const VirtualTargetGrid: React.FC<Props> = ({
|
||||
chartRealtimeSource = 'BACKEND_STOMP',
|
||||
chartSeriesPriceLabels = true,
|
||||
tickers,
|
||||
viewMode,
|
||||
globalDisplayMode,
|
||||
onFocusMarketChange,
|
||||
}) => {
|
||||
const [viewMode, setViewMode] = useState<VirtualCardViewMode>(() => loadVirtualCardViewMode());
|
||||
const [globalDisplayMode, setGlobalDisplayMode] = useState<VirtualCardDisplayMode>('signal');
|
||||
const [cardDisplayOverrides, setCardDisplayOverrides] = useState<Record<string, VirtualCardDisplayMode>>({});
|
||||
const [focusMarket, setFocusMarket] = useState<string | null>(null);
|
||||
|
||||
@@ -53,14 +49,14 @@ const VirtualTargetGrid: React.FC<Props> = ({
|
||||
useEffect(() => {
|
||||
if (focusMarket && !activeMarkets.has(focusMarket)) {
|
||||
setFocusMarket(null);
|
||||
onFocusMarketChange?.(null);
|
||||
}
|
||||
}, [activeMarkets, focusMarket]);
|
||||
}, [activeMarkets, focusMarket, onFocusMarketChange]);
|
||||
|
||||
useEffect(() => {
|
||||
saveVirtualCardViewMode(viewMode);
|
||||
}, [viewMode]);
|
||||
onFocusMarketChange?.(focusMarket);
|
||||
}, [focusMarket, onFocusMarketChange]);
|
||||
|
||||
/** 제거된 종목 override 정리 */
|
||||
useEffect(() => {
|
||||
setCardDisplayOverrides(prev => {
|
||||
const next: Record<string, VirtualCardDisplayMode> = {};
|
||||
@@ -73,10 +69,9 @@ const VirtualTargetGrid: React.FC<Props> = ({
|
||||
});
|
||||
}, [activeMarkets]);
|
||||
|
||||
const handleGlobalDisplayMode = useCallback((mode: VirtualCardDisplayMode) => {
|
||||
setGlobalDisplayMode(mode);
|
||||
useEffect(() => {
|
||||
setCardDisplayOverrides({});
|
||||
}, []);
|
||||
}, [globalDisplayMode]);
|
||||
|
||||
const handleCardDisplayMode = useCallback((market: string, mode: VirtualCardDisplayMode) => {
|
||||
setCardDisplayOverrides(prev => ({ ...prev, [market]: mode }));
|
||||
@@ -92,27 +87,9 @@ const VirtualTargetGrid: React.FC<Props> = ({
|
||||
[focusMarket, targets],
|
||||
);
|
||||
|
||||
const renderUnifiedHeader = (viewControls: 'full' | 'display-only' | 'hidden') => (
|
||||
<VirtualGridUnifiedHeader
|
||||
session={session}
|
||||
strategies={strategies}
|
||||
onStrategyChange={onStrategyChange}
|
||||
onExecutionTypeChange={onExecutionTypeChange}
|
||||
onPositionModeChange={onPositionModeChange}
|
||||
onStart={onStart}
|
||||
onStop={onStop}
|
||||
viewMode={viewMode}
|
||||
onViewModeChange={setViewMode}
|
||||
globalDisplayMode={globalDisplayMode}
|
||||
onGlobalDisplayModeChange={handleGlobalDisplayMode}
|
||||
viewControls={viewControls}
|
||||
/>
|
||||
);
|
||||
|
||||
if (targets.length === 0) {
|
||||
return (
|
||||
<div className="vtd-grid-wrap">
|
||||
{renderUnifiedHeader('hidden')}
|
||||
<div className="vtd-grid-empty">
|
||||
<p className="vtd-muted">좌측에서 투자대상 종목을 추가하면 카드가 표시됩니다.</p>
|
||||
</div>
|
||||
@@ -122,7 +99,6 @@ const VirtualTargetGrid: React.FC<Props> = ({
|
||||
|
||||
return (
|
||||
<div className={`vtd-grid-wrap${focusMarket ? ' vtd-grid-wrap--focus' : ''}`}>
|
||||
{renderUnifiedHeader(focusMarket ? 'display-only' : 'full')}
|
||||
{focusMarket && focusTarget ? (
|
||||
<VirtualTargetFocusView
|
||||
market={focusTarget.market}
|
||||
|
||||
Reference in New Issue
Block a user