193 lines
7.0 KiB
TypeScript
193 lines
7.0 KiB
TypeScript
import React, { useMemo } from 'react';
|
|
import { useAppSettings } from '../../hooks/useAppSettings';
|
|
import { resetPaperAccount, type PaperAllocationDto } from '../../utils/backendApi';
|
|
import PaperSplitPanel from './PaperSplitPanel';
|
|
import PaperSymbolStageSettings from './PaperSymbolStageSettings';
|
|
|
|
interface Props {
|
|
selectedMarket?: string;
|
|
allocations?: PaperAllocationDto[];
|
|
onSettingsSaved?: () => void;
|
|
onAccountReset?: () => void;
|
|
onOpenSettings?: () => void;
|
|
}
|
|
|
|
const SettingsFieldRow: React.FC<{ label: string; children: React.ReactNode }> = ({ label, children }) => (
|
|
<div className="vtd-target-strategy-field">
|
|
<span className="vtd-target-strategy-label">{label}</span>
|
|
{children}
|
|
</div>
|
|
);
|
|
|
|
const PaperLeftSettingsTab: React.FC<Props> = ({
|
|
selectedMarket,
|
|
allocations = [],
|
|
onSettingsSaved,
|
|
onAccountReset,
|
|
onOpenSettings,
|
|
}) => {
|
|
const { defaults, save, isLoaded } = useAppSettings();
|
|
const selectedAlloc = useMemo(
|
|
() => allocations.find(a => a.symbol === selectedMarket),
|
|
[allocations, selectedMarket],
|
|
);
|
|
|
|
if (!isLoaded) {
|
|
return <p className="ptd-muted">설정 로딩…</p>;
|
|
}
|
|
|
|
const patch = (p: Parameters<typeof save>[0]) => {
|
|
save(p);
|
|
onSettingsSaved?.();
|
|
};
|
|
|
|
const handleReset = async () => {
|
|
if (!window.confirm('모의투자 계좌를 초기화합니다. 계속할까요?')) return;
|
|
const res = await resetPaperAccount();
|
|
if (res) onAccountReset?.();
|
|
};
|
|
|
|
const top = (
|
|
<div className="ptd-settings-panel">
|
|
{onOpenSettings && (
|
|
<button type="button" className="bps-btn bps-btn--ghost ptd-settings-link" onClick={onOpenSettings}>
|
|
설정 → 모의투자에서 전체 관리
|
|
</button>
|
|
)}
|
|
<div className="vtd-target-item ptd-item-card ptd-settings-card">
|
|
<div className="ptd-item-card-metrics">
|
|
<SettingsFieldRow label="모의투자 활성화">
|
|
<label className="vtd-trade-meta-value ptd-settings-value-box ptd-settings-value-box--check">
|
|
<input
|
|
type="checkbox"
|
|
checked={defaults.paperTradingEnabled}
|
|
onChange={e => patch({ paperTradingEnabled: e.target.checked })}
|
|
/>
|
|
</label>
|
|
</SettingsFieldRow>
|
|
<SettingsFieldRow label="자동매매">
|
|
<label className="vtd-trade-meta-value ptd-settings-value-box ptd-settings-value-box--check">
|
|
<input
|
|
type="checkbox"
|
|
checked={defaults.paperAutoTradeEnabled}
|
|
onChange={e => patch({ paperAutoTradeEnabled: e.target.checked })}
|
|
/>
|
|
</label>
|
|
</SettingsFieldRow>
|
|
<SettingsFieldRow label="매수 예산 (%)">
|
|
<input
|
|
type="number"
|
|
className="vtd-trade-meta-value ptd-settings-input"
|
|
min={1}
|
|
max={100}
|
|
value={defaults.paperAutoTradeBudgetPct}
|
|
disabled={!defaults.paperAutoTradeEnabled}
|
|
onChange={e => patch({ paperAutoTradeBudgetPct: Number(e.target.value) })}
|
|
/>
|
|
</SettingsFieldRow>
|
|
<SettingsFieldRow label="미체결 주문 처리">
|
|
<select
|
|
className="vtd-trade-meta-value ptd-settings-input ptd-settings-select"
|
|
value={defaults.paperOrderFillMode}
|
|
onChange={e => patch({ paperOrderFillMode: e.target.value as 'LIMIT_ONLY' | 'NEXT_TICK' | 'AUTO_CANCEL' })}
|
|
>
|
|
<option value="LIMIT_ONLY">A. 지정가 유지 (조건 충족 시 체결)</option>
|
|
<option value="NEXT_TICK">B. 다음 틱 강제 체결 (시장가 동일)</option>
|
|
<option value="AUTO_CANCEL">C. 허용폭 초과 시 자동 취소</option>
|
|
</select>
|
|
</SettingsFieldRow>
|
|
{defaults.paperOrderFillMode === 'AUTO_CANCEL' && (
|
|
<SettingsFieldRow label="허용 이탈폭 (%)">
|
|
<input
|
|
type="number"
|
|
className="vtd-trade-meta-value ptd-settings-input"
|
|
min={0.1}
|
|
max={50}
|
|
step={0.1}
|
|
value={defaults.paperOrderAutoCancelPct}
|
|
onChange={e => patch({ paperOrderAutoCancelPct: Number(e.target.value) })}
|
|
/>
|
|
</SettingsFieldRow>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
const bottom = (
|
|
<div className="ptd-settings-panel ptd-settings-panel--account ptd-settings-panel--stages">
|
|
{selectedMarket ? (
|
|
<PaperSymbolStageSettings
|
|
symbol={selectedMarket}
|
|
allocation={selectedAlloc}
|
|
onSaved={onSettingsSaved}
|
|
/>
|
|
) : (
|
|
<p className="ptd-muted ptd-item-card-empty">중앙 투자금 관리에서 종목을 선택하면 분할 매매 단계를 설정할 수 있습니다.</p>
|
|
)}
|
|
<div className="ptd-settings-panel-divider" aria-hidden />
|
|
<div className="vtd-target-item ptd-item-card ptd-settings-card">
|
|
<div className="ptd-item-card-metrics">
|
|
<SettingsFieldRow label="초기 자본 (KRW)">
|
|
<input
|
|
type="number"
|
|
className="vtd-trade-meta-value ptd-settings-input"
|
|
min={100000}
|
|
step={100000}
|
|
value={defaults.paperInitialCapital}
|
|
onChange={e => patch({ paperInitialCapital: Number(e.target.value) })}
|
|
/>
|
|
</SettingsFieldRow>
|
|
<SettingsFieldRow label="수수료율 (%)">
|
|
<input
|
|
type="number"
|
|
className="vtd-trade-meta-value ptd-settings-input"
|
|
min={0}
|
|
max={1}
|
|
step={0.01}
|
|
value={defaults.paperFeeRatePct}
|
|
onChange={e => patch({ paperFeeRatePct: Number(e.target.value) })}
|
|
/>
|
|
</SettingsFieldRow>
|
|
<SettingsFieldRow label="슬리피지 (%)">
|
|
<input
|
|
type="number"
|
|
className="vtd-trade-meta-value ptd-settings-input"
|
|
min={0}
|
|
max={5}
|
|
step={0.01}
|
|
value={defaults.paperSlippagePct}
|
|
onChange={e => patch({ paperSlippagePct: Number(e.target.value) })}
|
|
/>
|
|
</SettingsFieldRow>
|
|
<SettingsFieldRow label="최소 주문 (KRW)">
|
|
<input
|
|
type="number"
|
|
className="vtd-trade-meta-value ptd-settings-input"
|
|
min={1000}
|
|
step={1000}
|
|
value={defaults.paperMinOrderKrw}
|
|
onChange={e => patch({ paperMinOrderKrw: Number(e.target.value) })}
|
|
/>
|
|
</SettingsFieldRow>
|
|
</div>
|
|
</div>
|
|
<button type="button" className="bps-btn bps-btn--danger ptd-settings-reset" onClick={() => void handleReset()}>
|
|
계좌 초기화
|
|
</button>
|
|
</div>
|
|
);
|
|
|
|
return (
|
|
<PaperSplitPanel
|
|
className="ptd-split-panel--left"
|
|
topTitle="운영 · 자동매매"
|
|
bottomTitle="종목별 분할 매매 · 계좌"
|
|
top={top}
|
|
bottom={bottom}
|
|
/>
|
|
);
|
|
};
|
|
|
|
export default PaperLeftSettingsTab;
|