가상투자 설정 수정

This commit is contained in:
Macbook
2026-05-25 22:30:54 +09:00
parent cae1c3a624
commit 1e950c7db4
24 changed files with 1015 additions and 362 deletions
@@ -1,12 +1,12 @@
import React, { useCallback, useEffect, useState } from 'react';
import React, { useEffect, useState } from 'react';
import {
loadStrategies,
loadLiveStrategySettings,
saveLiveStrategySettings,
type LiveStrategySettingsDto,
type PaperSummaryDto,
type StrategyDto,
} from '../../utils/backendApi';
import { loadVirtualSession } from '../../utils/virtualTradingStorage';
import { fmtKrw } from '../TradeOrderPanel';
import PaperSplitPanel from './PaperSplitPanel';
@@ -25,12 +25,22 @@ const PaperLeftStrategyTab: React.FC<Props> = ({
}) => {
const [strategies, setStrategies] = useState<StrategyDto[]>([]);
const [settings, setSettings] = useState<LiveStrategySettingsDto | null>(null);
const [saving, setSaving] = useState(false);
const [virtualRunning, setVirtualRunning] = useState(() => loadVirtualSession().running);
useEffect(() => {
loadStrategies().then(setStrategies).catch(() => setStrategies([]));
}, []);
useEffect(() => {
const refreshVirtual = () => setVirtualRunning(loadVirtualSession().running);
window.addEventListener('gc_virtual_session_changed', refreshVirtual);
window.addEventListener('storage', refreshVirtual);
return () => {
window.removeEventListener('gc_virtual_session_changed', refreshVirtual);
window.removeEventListener('storage', refreshVirtual);
};
}, []);
useEffect(() => {
let cancelled = false;
loadLiveStrategySettings(market)
@@ -47,26 +57,10 @@ const PaperLeftStrategyTab: React.FC<Props> = ({
}
});
return () => { cancelled = true; };
}, [market]);
const persist = useCallback(async (patch: Partial<LiveStrategySettingsDto>) => {
if (!settings) return;
const next = { ...settings, ...patch, market };
const prev = settings;
setSettings(next);
setSaving(true);
try {
const saved = await saveLiveStrategySettings(next);
if (saved) setSettings(saved);
else setSettings(prev);
} catch {
setSettings(prev);
} finally {
setSaving(false);
}
}, [settings, market]);
}, [market, virtualRunning]);
const selected = strategies.find(s => s.id === settings?.strategyId);
const execLabel = settings?.executionType === 'REALTIME_TICK' ? '실시간 틱 (3초)' : '봉 마감 직후';
const top = (
<>
@@ -79,55 +73,20 @@ const PaperLeftStrategyTab: React.FC<Props> = ({
</div>
</div>
<div className="ptd-left-section">
<label className="ptd-left-row">
<span> </span>
<input
type="checkbox"
checked={settings?.isLiveCheck ?? false}
disabled={saving}
onChange={e => void persist({ isLiveCheck: e.target.checked })}
/>
</label>
<label className="ptd-left-field">
<span> </span>
<select
className="ptd-left-select"
value={settings?.strategyId ?? ''}
disabled={saving}
onChange={e => {
const v = e.target.value;
void persist({ strategyId: v ? Number(v) : null });
}}
>
<option value=""> </option>
{strategies.map(s => (
<option key={s.id} value={s.id}>{s.name}</option>
))}
</select>
</label>
{selected && (
<p className="ptd-left-hint">
· <strong></strong> .
{virtualRunning ? ' (세션 실행 중)' : ''}
</p>
{settings?.isLiveCheck && selected && (
<p className="ptd-left-hint">
: {selected.name}
{settings?.strategyCandleTypes?.length
? ` · ${settings.strategyCandleTypes.join(', ')}`
: ''}
{market.replace(/^KRW-/, '')}: {selected.name} · {execLabel}
</p>
)}
<label className="ptd-left-field">
<span> </span>
<select
className="ptd-left-select"
value={settings?.executionType ?? 'CANDLE_CLOSE'}
disabled={saving}
onChange={e => void persist({ executionType: e.target.value as LiveStrategySettingsDto['executionType'] })}
>
<option value="CANDLE_CLOSE"> </option>
<option value="REALTIME_TICK"> (3)</option>
</select>
</label>
</div>
<p className={`ptd-left-status ${paperAutoTradeEnabled ? 'ptd-left-status--on' : ''}`}>
{paperAutoTradeEnabled ? '자동매매 ON' : '자동매매 OFF — 설정 탭에서 변경'}
{paperAutoTradeEnabled
? '자동매매 ON — 가상투자 Match 시 가상 계좌 자동 체결'
: '자동매매 OFF — 가상투자 타이틀바에서 변경'}
</p>
</>
);
@@ -0,0 +1,87 @@
import React from 'react';
import type { PaperTradeDto } from '../../utils/backendApi';
import { fmtKrw } from '../TradeOrderPanel';
function coinCode(symbol: string): string {
return symbol.replace(/^KRW-/, '');
}
function fmtTradeTime(iso: string | null | undefined): string {
if (!iso) return '—';
const d = new Date(iso);
if (Number.isNaN(d.getTime())) return iso.slice(0, 19).replace('T', ' ');
const pad = (n: number) => String(n).padStart(2, '0');
return `${pad(d.getMonth() + 1)}/${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
}
function sourceLabel(source: string | null | undefined): string {
if (source === 'STRATEGY') return '자동';
return '수동';
}
interface Props {
trades: PaperTradeDto[];
strategyNames?: Record<number, string>;
emptyText?: string;
className?: string;
onSelectMarket?: (market: string) => void;
}
const PaperTradeHistoryList: React.FC<Props> = ({
trades,
strategyNames = {},
emptyText = '거래 내역이 없습니다.',
className = '',
onSelectMarket,
}) => (
<div className={`ptd-trade-history${className ? ` ${className}` : ''}`}>
{trades.length === 0 ? (
<p className="ptd-muted ptd-trade-history-empty">{emptyText}</p>
) : (
<div className="ptd-trade-history-scroll">
<table className="ptd-table ptd-table--compact ptd-table--trades">
<thead>
<tr>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
{trades.map(t => {
const strategyName = t.strategyId != null
? (strategyNames[t.strategyId] ?? `#${t.strategyId}`)
: null;
return (
<tr
key={t.id}
className={onSelectMarket ? 'ptd-row--click' : undefined}
onClick={onSelectMarket ? () => onSelectMarket(t.symbol) : undefined}
title={strategyName ? `전략: ${strategyName}` : undefined}
>
<td className="ptd-time">{fmtTradeTime(t.createdAt)}</td>
<td>{coinCode(t.symbol)}</td>
<td className={t.side === 'BUY' ? 'up' : 'down'}>
{t.side === 'BUY' ? '매수' : '매도'}
</td>
<td className={t.source === 'STRATEGY' ? 'ptd-source--auto' : 'ptd-source--manual'}>
{sourceLabel(t.source)}
</td>
<td>{fmtKrw(t.price)}</td>
<td>{t.quantity}</td>
<td>{fmtKrw(t.netAmount)}</td>
</tr>
);
})}
</tbody>
</table>
</div>
)}
</div>
);
export default PaperTradeHistoryList;