모의투자, 백테스팅 레이아웃 수정

This commit is contained in:
Macbook
2026-05-24 20:13:21 +09:00
parent 958b813f3b
commit af230a4233
23 changed files with 2394 additions and 782 deletions
@@ -0,0 +1,182 @@
import React, { useCallback, useEffect, useState } from 'react';
import {
loadStrategies,
loadLiveStrategySettings,
saveLiveStrategySettings,
type LiveStrategySettingsDto,
type PaperSummaryDto,
type StrategyDto,
} from '../../utils/backendApi';
import { fmtKrw } from '../TradeOrderPanel';
import PaperSplitPanel from './PaperSplitPanel';
const CANDLE_TYPES = ['1m', '3m', '5m', '15m', '30m', '1h', '4h', '1d'] as const;
interface Props {
market: string;
summary: PaperSummaryDto | null;
paperAutoTradeEnabled?: boolean;
onMarketSelect?: (market: string) => void;
}
const PaperLeftStrategyTab: React.FC<Props> = ({
market,
summary,
paperAutoTradeEnabled = false,
onMarketSelect,
}) => {
const [strategies, setStrategies] = useState<StrategyDto[]>([]);
const [settings, setSettings] = useState<LiveStrategySettingsDto | null>(null);
const [saving, setSaving] = useState(false);
useEffect(() => {
loadStrategies().then(setStrategies).catch(() => setStrategies([]));
}, []);
useEffect(() => {
let cancelled = false;
loadLiveStrategySettings(market)
.then(s => { if (!cancelled) setSettings(s); })
.catch(() => {
if (!cancelled) {
setSettings({
market,
strategyId: null,
isLiveCheck: false,
executionType: 'CANDLE_CLOSE',
positionMode: 'LONG_ONLY',
candleType: '1m',
});
}
});
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]);
const selected = strategies.find(s => s.id === settings?.strategyId);
const top = (
<>
<div className="ptd-asset-inline">
<div className="ptd-asset-total ptd-asset-total--sm">
{summary ? fmtKrw(summary.totalAsset) : '—'} <span>KRW</span>
</div>
<div className={`ptd-asset-ret ptd-asset-ret--sm ${(summary?.totalReturnPct ?? 0) >= 0 ? 'up' : 'down'}`}>
{summary ? `${(summary.totalReturnPct ?? 0) >= 0 ? '+' : ''}${summary.totalReturnPct.toFixed(2)}%` : '—'}
</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">: {selected.name}</p>}
<label className="ptd-left-field">
<span> </span>
<select
className="ptd-left-select"
value={settings?.candleType ?? '1m'}
disabled={saving}
onChange={e => void persist({ candleType: e.target.value })}
>
{CANDLE_TYPES.map(c => <option key={c} value={c}>{c}</option>)}
</select>
</label>
<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 — 설정 탭에서 변경'}
</p>
</>
);
const bottom = (
<table className="ptd-table ptd-table--compact">
<thead>
<tr><th></th><th></th><th></th></tr>
</thead>
<tbody>
{(summary?.positions ?? []).length === 0 ? (
<tr><td colSpan={3} className="ptd-muted"> </td></tr>
) : summary!.positions.map(p => {
const code = p.symbol.replace(/^KRW-/, '');
const up = (p.profitLoss ?? 0) >= 0;
return (
<tr
key={p.symbol}
className={market === p.symbol ? 'ptd-row--sel' : ''}
onClick={() => onMarketSelect?.(p.symbol)}
>
<td>{code}</td>
<td>{p.quantity.toFixed(4)}</td>
<td className={up ? 'up' : 'down'}>
{p.profitLoss != null ? `${up ? '+' : ''}${fmtKrw(p.profitLoss)}` : '—'}
</td>
</tr>
);
})}
</tbody>
</table>
);
return (
<PaperSplitPanel
className="ptd-split-panel--left"
topTitle="전략 · 자산"
bottomTitle="보유 종목"
top={top}
bottom={bottom}
/>
);
};
export default PaperLeftStrategyTab;