Files
goldenChart/app/src/screens/virtual/VirtualTradingScreen.tsx
T
2026-05-28 16:24:14 +09:00

219 lines
7.8 KiB
TypeScript

import React, { useState } from 'react';
import { useAuth } from '../../contexts/AuthContext';
import { useAppSettings } from '../../hooks/useAppSettings';
import { useVirtualTradingCore } from '@frontend/hooks/useVirtualTradingCore';
import { resolveVirtualTargetStrategyId } from '@frontend/utils/virtualTargetStrategy';
import { useNavigation } from '../../contexts/NavigationContext';
import BottomSheet from '../../components/BottomSheet';
import SegmentedControl from '../../components/SegmentedControl';
import VirtualTargetListRow from './VirtualTargetListRow';
import VirtualFocusScreen from './VirtualFocusScreen';
import VirtualTradeScreen from './VirtualTradeScreen';
import VirtualHistoryScreen from './VirtualHistoryScreen';
import { MarketSearchPanel } from '@frontend/components/MarketSearchPanel';
export default function VirtualTradingScreen() {
const {
virtualNav,
goVirtualList,
goVirtualDetail,
goVirtualTrade,
goVirtualHistory,
} = useNavigation();
const { sessionKey: settingsSessionKey, authUser, guestMode } = useAuth();
const { isLoaded: settingsLoaded } = useAppSettings(settingsSessionKey);
const core = useVirtualTradingCore({ settingsSessionKey });
const {
targets,
session,
strategies,
summary,
trades,
loading,
snapshots,
liveStatusByMarket,
handleStart,
handleStop,
handleRemoveTarget,
handleTogglePin,
handleGlobalStrategyChange,
handleExecutionTypeChange,
handlePositionModeChange,
addTarget,
reloadTargetsFromStorage,
} = core;
const [addSheetOpen, setAddSheetOpen] = useState(false);
const pnlPct = summary?.totalReturnPct ?? 0;
const liveConnected = Object.values(liveStatusByMarket).some(s => s === 'live' || s === 'connecting');
const market = virtualNav.market;
const target = market ? targets.find(t => t.market === market) : undefined;
if (virtualNav.view === 'detail' && market && target) {
const sid = resolveVirtualTargetStrategyId(target, session.globalStrategyId);
const snap = snapshots[`${market}:${sid ?? ''}`] ?? snapshots[market];
return (
<VirtualFocusScreen
target={target}
session={session}
strategies={strategies}
snapshot={snap}
liveConnected={liveConnected}
onBack={goVirtualList}
onTrade={side => goVirtualTrade(market, side)}
onHistory={() => goVirtualHistory(market)}
/>
);
}
if (virtualNav.view === 'trade' && market && virtualNav.tradeSide) {
return (
<VirtualTradeScreen
market={market}
side={virtualNav.tradeSide}
summary={summary}
onBack={goVirtualList}
onOrderDone={core.refreshPaperData}
/>
);
}
if (virtualNav.view === 'history' && market) {
return (
<VirtualHistoryScreen
market={market}
trades={trades}
onBack={goVirtualList}
/>
);
}
return (
<div className="screen virtual-screen">
<header className="screen-header">
<h1 className="screen-title">가상매매</h1>
<button type="button" className="chip" onClick={() => reloadTargetsFromStorage()} title="동기화"></button>
<button
type="button"
className={`btn-primary session-btn${session.running ? ' running' : ''}`}
onClick={() => void (session.running ? handleStop() : handleStart())}
>
{session.running ? '■ 중지' : '▶ 시작'}
</button>
</header>
<div className="virtual-summary card" style={{ margin: '0 16px 12px' }}>
<div className="virtual-summary-row">
<div>
<div className="text-muted" style={{ fontSize: 12 }}> 자산</div>
<div style={{ fontSize: 22, fontWeight: 700 }}>{(summary?.totalAsset ?? 0).toLocaleString()}</div>
</div>
<div style={{ textAlign: 'right' }}>
<div className="text-muted" style={{ fontSize: 12 }}>수익률</div>
<div className={`${pnlPct >= 0 ? 'text-green' : 'text-red'}`} style={{ fontSize: 22, fontWeight: 700 }}>
{pnlPct >= 0 ? '+' : ''}{pnlPct.toFixed(2)}%
</div>
</div>
</div>
{!liveConnected && session.running && (
<div className="connection-banner" style={{ marginTop: 8, borderRadius: 8 }}>실시간 연결 끊김</div>
)}
</div>
<div className="chip-row">
<select
className="chip"
value={session.globalStrategyId ?? ''}
onChange={e => handleGlobalStrategyChange(e.target.value ? Number(e.target.value) : null)}
style={{ maxWidth: 160 }}
>
<option value="">전략 선택</option>
{strategies.map(s => (
<option key={s.id} value={s.id}>{s.name}</option>
))}
</select>
<SegmentedControl
size="sm"
options={[
{ value: 'CANDLE_CLOSE' as const, label: '봉마감' },
{ value: 'REALTIME_TICK' as const, label: '실시간' },
]}
value={session.executionType}
onChange={handleExecutionTypeChange}
/>
</div>
<div className="chip-row">
<SegmentedControl
size="sm"
options={[
{ value: 'LONG_ONLY' as const, label: '롱온리' },
{ value: 'SIGNAL_ONLY' as const, label: '시그널' },
]}
value={session.positionMode}
onChange={handlePositionModeChange}
/>
</div>
{!settingsLoaded || loading ? (
<div className="loading-center">{!settingsLoaded ? '설정 동기화 중…' : '로딩 중…'}</div>
) : targets.length === 0 ? (
<div className="empty-state">
<h3>투자 대상 없음</h3>
{guestMode ? (
<p>게스트 모드입니다. 웹과 같은 목록을 보려면 <strong>동일 계정으로 로그인</strong>하세요.</p>
) : (
<p>({authUser?.username}) 동일 DB입니다. 동기화 또는 + 종목 추가</p>
)}
</div>
) : (
<div className="stack-list" style={{ padding: '0 16px' }}>
{targets.map(t => {
const strategyId = resolveVirtualTargetStrategyId(t, session.globalStrategyId);
const snapKey = `${t.market}:${strategyId ?? ''}`;
const snap = snapshots[snapKey] ?? snapshots[t.market];
return (
<VirtualTargetListRow
key={t.market}
target={t}
globalStrategyId={session.globalStrategyId}
strategies={strategies}
snapshot={snap}
liveStatus={liveStatusByMarket[t.market]}
onDetail={() => goVirtualDetail(t.market)}
onTrade={side => goVirtualTrade(t.market, side)}
onTogglePin={() => void handleTogglePin(t.market)}
onRemove={() => handleRemoveTarget(t.market)}
/>
);
})}
</div>
)}
<button type="button" className="fab" aria-label="종목 추가" onClick={() => setAddSheetOpen(true)}>+</button>
<BottomSheet open={addSheetOpen} title="종목 검색" onClose={() => setAddSheetOpen(false)} height="full">
<div style={{ height: 'min(70vh, 520px)', overflow: 'hidden' }}>
<MarketSearchPanel
variant="embedded"
currentMarket={targets[0]?.market ?? 'KRW-BTC'}
onSelect={m => {
addTarget(m);
setAddSheetOpen(false);
}}
onClose={() => setAddSheetOpen(false)}
/>
</div>
</BottomSheet>
<style>{`
.session-btn.running { background: var(--gc-red); }
.virtual-summary-row { display: flex; justify-content: space-between; align-items: flex-start; }
`}</style>
</div>
);
}