모의투자, 백테스팅 레이아웃 수정
This commit is contained in:
@@ -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;
|
||||
Reference in New Issue
Block a user