모의투자 관리 기능 추가
This commit is contained in:
@@ -166,6 +166,8 @@ interface SettingsPageProps {
|
||||
onVerificationIssueNotify?: (v: boolean) => void;
|
||||
trendSearchSettings?: TrendSearchAppSettings;
|
||||
onTrendSearchSettingsChange?: (s: TrendSearchAppSettings) => void;
|
||||
/** 설정 화면 진입 시 선택할 카테고리 (예: 모의투자 화면에서 링크) */
|
||||
initialCategory?: SettingsCategoryId;
|
||||
}
|
||||
|
||||
// ── 카테고리 정의 ────────────────────────────────────────────────────────────
|
||||
@@ -233,6 +235,19 @@ const IcPaper = () => (
|
||||
<circle cx="16" cy="6" r="2" fill="currentColor" stroke="none"/>
|
||||
</svg>
|
||||
);
|
||||
const IcVirtual = () => (
|
||||
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
||||
<rect x="2" y="6" width="8" height="10" rx="1.5"/>
|
||||
<rect x="12" y="4" width="8" height="12" rx="1.5"/>
|
||||
<path d="M10 11h2"/>
|
||||
</svg>
|
||||
);
|
||||
const IcLive = () => (
|
||||
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
||||
<circle cx="11" cy="11" r="9"/>
|
||||
<path d="M8 11h6M11 8v6"/>
|
||||
</svg>
|
||||
);
|
||||
const IcTrendSearch = () => (
|
||||
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
||||
<polyline points="4,17 9,11 13,14 18,6"/>
|
||||
@@ -264,7 +279,9 @@ const ALL_CATEGORIES: Category[] = [
|
||||
{ id: 'indicators', label: '보조지표 설정', icon: <IcIndicators />, desc: '지표 추가 Main 탭 보조지표 기본 파라미터·색상·기준선' },
|
||||
{ id: 'backtest', label: '백테스팅', icon: <IcBacktest />, desc: '백테스팅 실행 옵션, 자본·비용·리스크 관리 설정' },
|
||||
{ id: 'strategy', label: '전략 설정', icon: <IcStrategy />, desc: '투자 전략 기본 파라미터 및 조건 설정' },
|
||||
{ id: 'paper', label: '가상매매', icon: <IcPaper />, desc: '가상 자본, 수수료, 투자대상 한도, 자동매매 ON/OFF 등 가상매매 설정' },
|
||||
{ id: 'paper', label: '모의투자', icon: <IcPaper />, desc: '모의투자 활성화, 자동매매, 초기 자본·수수료·슬리피지, 계좌 초기화' },
|
||||
{ id: 'virtual', label: '가상매매', icon: <IcVirtual />, desc: '가상매매 화면 투자대상 목록 한도 등 가상매매 전용 설정' },
|
||||
{ id: 'live', label: '실거래', icon: <IcLive />, desc: '매매 운영 모드, 업비트 API 키, 실거래 자동매매' },
|
||||
{ id: 'trend-search', label: '추세검색', icon: <IcTrendSearch />, desc: '상승추세 배점, 합격 점수, 결과 수, 자동갱신·투자대상 자동추가' },
|
||||
{ id: 'alert', label: '알림 설정', icon: <IcAlert />, desc: '가격 알림, 신호 알림, 알림 방식 설정' },
|
||||
{ id: 'network', label: '네트워크', icon: <IcNetwork />, desc: 'API 서버 주소, WebSocket, 연결 상태' },
|
||||
@@ -319,7 +336,7 @@ const StgCard: React.FC<{
|
||||
|
||||
// ── 각 카테고리 패널 ─────────────────────────────────────────────────────────
|
||||
|
||||
interface PaperPanelProps {
|
||||
interface PaperTradingSettingsPanelProps {
|
||||
paperTradingEnabled?: boolean;
|
||||
onPaperTradingEnabled?: (v: boolean) => void;
|
||||
paperInitialCapital?: number;
|
||||
@@ -334,21 +351,10 @@ interface PaperPanelProps {
|
||||
onPaperAutoTradeEnabled?: (v: boolean) => void;
|
||||
paperAutoTradeBudgetPct?: number;
|
||||
onPaperAutoTradeBudgetPct?: (v: number) => void;
|
||||
virtualTargetMaxCount?: number;
|
||||
onVirtualTargetMaxCount?: (v: number) => void;
|
||||
onPaperAccountReset?: () => void;
|
||||
tradingMode?: string;
|
||||
onTradingMode?: (v: string) => void;
|
||||
liveAutoTradeEnabled?: boolean;
|
||||
onLiveAutoTradeEnabled?: (v: boolean) => void;
|
||||
hasUpbitKeys?: boolean;
|
||||
upbitAccessKeyMasked?: string | null;
|
||||
onUpbitKeys?: (access: string, secret: string) => void;
|
||||
liveAutoTradeBudgetPct?: number;
|
||||
onLiveAutoTradeBudgetPct?: (v: number) => void;
|
||||
}
|
||||
|
||||
const PaperPanel: React.FC<PaperPanelProps> = ({
|
||||
const PaperTradingSettingsPanel: React.FC<PaperTradingSettingsPanelProps> = ({
|
||||
paperTradingEnabled = true,
|
||||
onPaperTradingEnabled,
|
||||
paperInitialCapital = 10_000_000,
|
||||
@@ -363,63 +369,29 @@ const PaperPanel: React.FC<PaperPanelProps> = ({
|
||||
onPaperAutoTradeEnabled,
|
||||
paperAutoTradeBudgetPct = 95,
|
||||
onPaperAutoTradeBudgetPct,
|
||||
virtualTargetMaxCount = 20,
|
||||
onVirtualTargetMaxCount,
|
||||
onPaperAccountReset,
|
||||
tradingMode = 'PAPER',
|
||||
onTradingMode,
|
||||
liveAutoTradeEnabled = false,
|
||||
onLiveAutoTradeEnabled,
|
||||
hasUpbitKeys = false,
|
||||
upbitAccessKeyMasked = null,
|
||||
onUpbitKeys,
|
||||
liveAutoTradeBudgetPct = 95,
|
||||
onLiveAutoTradeBudgetPct,
|
||||
}) => {
|
||||
const [accessKey, setAccessKey] = useState('');
|
||||
const [secretKey, setSecretKey] = useState('');
|
||||
|
||||
return (
|
||||
}) => (
|
||||
<>
|
||||
<SettingSection title="매매 운영 모드">
|
||||
<SettingRow label="실행 대상" desc="가상투자만, 실거래(업비트 API)만, 또는 둘 다 병행할 수 있습니다.">
|
||||
<select className="stg-select" value={tradingMode} onChange={e => onTradingMode?.(e.target.value)}>
|
||||
<option value="PAPER">가상투자만</option>
|
||||
<option value="LIVE">실거래만</option>
|
||||
<option value="BOTH">모의 + 실거래 병행</option>
|
||||
</select>
|
||||
</SettingRow>
|
||||
</SettingSection>
|
||||
<SettingSection title="가상매매">
|
||||
<SettingRow label="가상매매 활성화" desc="켜면 가상매매·실시간 차트 매매 패널·전략 Match 자동매매가 가상 계좌로 체결됩니다.">
|
||||
<SettingSection title="모의투자">
|
||||
<SettingRow
|
||||
label="모의투자 활성화"
|
||||
desc="켜면 모의투자 화면·실시간 차트 매매 패널·전략 Match 자동매매가 모의 계좌로 체결됩니다."
|
||||
>
|
||||
<label className="stg-toggle">
|
||||
<input type="checkbox" checked={paperTradingEnabled} onChange={e => onPaperTradingEnabled?.(e.target.checked)} />
|
||||
<span className="stg-toggle-slider" />
|
||||
</label>
|
||||
</SettingRow>
|
||||
<SettingRow
|
||||
label="투자대상 목록 최대 개수"
|
||||
desc="가상매매 화면·추세검색에서 등록할 수 있는 투자대상 종목 수 상한입니다."
|
||||
>
|
||||
<input
|
||||
type="number"
|
||||
className="stg-input stg-input--num"
|
||||
value={virtualTargetMaxCount}
|
||||
min={1}
|
||||
max={100}
|
||||
step={1}
|
||||
onChange={e => onVirtualTargetMaxCount?.(Number(e.target.value))}
|
||||
/>
|
||||
</SettingRow>
|
||||
</SettingSection>
|
||||
<SettingSection title="자동매매">
|
||||
<SettingRow
|
||||
label="자동매매 ON/OFF"
|
||||
desc="가상투자 화면 타이틀바에서 변경합니다. Match 충족 시 자동 매수·매도 여부를 제어합니다."
|
||||
label="자동매매"
|
||||
desc="Match 충족 시 모의 계좌로 자동 매수·매도합니다. 모의투자·가상매매 화면 타이틀바에서도 변경할 수 있습니다."
|
||||
>
|
||||
<span className={`stg-badge ${paperAutoTradeEnabled ? 'stg-badge--on' : 'stg-badge--off'}`}>
|
||||
{paperAutoTradeEnabled ? 'ON (타이틀바)' : 'OFF (타이틀바)'}
|
||||
</span>
|
||||
<label className="stg-toggle">
|
||||
<input type="checkbox" checked={paperAutoTradeEnabled} onChange={e => onPaperAutoTradeEnabled?.(e.target.checked)} />
|
||||
<span className="stg-toggle-slider" />
|
||||
</label>
|
||||
</SettingRow>
|
||||
<SettingRow
|
||||
label="자동 매수 예산 (%)"
|
||||
@@ -436,9 +408,6 @@ const PaperPanel: React.FC<PaperPanelProps> = ({
|
||||
onChange={e => onPaperAutoTradeBudgetPct?.(Number(e.target.value))}
|
||||
/>
|
||||
</SettingRow>
|
||||
{!paperAutoTradeEnabled && (
|
||||
<p className="stg-hint">자동매매 ON/OFF는 가상투자 화면 타이틀바에서 변경하세요. OFF 상태에서는 Match·시그널로 주문되지 않습니다.</p>
|
||||
)}
|
||||
</SettingSection>
|
||||
<SettingSection title="계좌·비용">
|
||||
<SettingRow label="초기 자본 (KRW)" desc="계좌 초기화 시 적용되는 시작 현금입니다.">
|
||||
@@ -485,69 +454,135 @@ const PaperPanel: React.FC<PaperPanelProps> = ({
|
||||
</SettingRow>
|
||||
</SettingSection>
|
||||
<SettingSection title="계좌 관리">
|
||||
<SettingRow label="계좌 초기화" desc="보유 종목·체결 이력을 삭제하고 초기 자본으로 현금을 되돌립니다.">
|
||||
<SettingRow
|
||||
label="계좌 초기화"
|
||||
desc="보유 종목·체결·원장·투자금 한도·미체결 주문을 삭제하고 초기 자본으로 현금을 되돌립니다."
|
||||
>
|
||||
<button type="button" className="stg-btn stg-btn--danger" onClick={onPaperAccountReset}>
|
||||
모의 계좌 초기화
|
||||
</button>
|
||||
</SettingRow>
|
||||
</SettingSection>
|
||||
<SettingSection title="실거래 (업비트 Open API)">
|
||||
<SettingRow label="실거래 자동매매" desc="ON이면 전략 시그널·손절/익절 시 업비트에 실제 주문합니다. API 키에 주문 권한이 필요합니다.">
|
||||
<label className="stg-toggle">
|
||||
<input type="checkbox" checked={liveAutoTradeEnabled} onChange={e => onLiveAutoTradeEnabled?.(e.target.checked)} />
|
||||
<span className="stg-toggle-slider" />
|
||||
</label>
|
||||
</SettingRow>
|
||||
<SettingRow label="API 키 상태" desc={hasUpbitKeys ? `등록됨 (${upbitAccessKeyMasked})` : 'Access / Secret 키를 입력 후 저장하세요.'}>
|
||||
<span style={{ fontSize: 12, color: hasUpbitKeys ? 'var(--accent)' : 'var(--text3)' }}>
|
||||
{hasUpbitKeys ? '연결됨' : '미등록'}
|
||||
</span>
|
||||
</SettingRow>
|
||||
<SettingRow label="Access Key" desc="입력 내용이 그대로 표시됩니다. 저장 시 DB에 AES 암호화되어 보관됩니다.">
|
||||
<input
|
||||
type="text"
|
||||
className="stg-input stg-input--sensitive"
|
||||
placeholder={hasUpbitKeys ? '변경 시 새 Access Key 입력' : 'Access Key'}
|
||||
value={accessKey}
|
||||
onChange={e => setAccessKey(e.target.value)}
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow label="Secret Key" desc="입력 내용이 그대로 표시됩니다. 저장 시 암호화되며 API로 전체 키는 반환되지 않습니다.">
|
||||
<input
|
||||
type="text"
|
||||
className="stg-input stg-input--sensitive"
|
||||
placeholder={hasUpbitKeys ? '변경 시 새 Secret Key 입력' : 'Secret Key'}
|
||||
value={secretKey}
|
||||
onChange={e => setSecretKey(e.target.value)}
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
/>
|
||||
</SettingRow>
|
||||
{(accessKey.trim() || secretKey.trim()) && (
|
||||
<SettingRow label="">
|
||||
<button
|
||||
type="button"
|
||||
className="stg-btn stg-btn--primary"
|
||||
onClick={() => {
|
||||
onUpbitKeys?.(accessKey.trim(), secretKey.trim());
|
||||
setAccessKey('');
|
||||
setSecretKey('');
|
||||
}}
|
||||
>
|
||||
API 키 저장
|
||||
</button>
|
||||
</SettingRow>
|
||||
)}
|
||||
<SettingRow label="실거래 자동매수 예산 (%)" desc="시장가 매수 시 KRW 잔고 대비 사용 비율 (모의투자 예산과 별도)">
|
||||
<input type="number" className="stg-input stg-input--narrow" min={1} max={100} step={1}
|
||||
value={liveAutoTradeBudgetPct}
|
||||
onChange={e => onLiveAutoTradeBudgetPct?.(Number(e.target.value))} />
|
||||
<span className="stg-unit">%</span>
|
||||
</SettingRow>
|
||||
</SettingSection>
|
||||
</>
|
||||
);
|
||||
|
||||
const VirtualTradingSettingsPanel: React.FC<{
|
||||
virtualTargetMaxCount?: number;
|
||||
onVirtualTargetMaxCount?: (v: number) => void;
|
||||
}> = ({ virtualTargetMaxCount = 20, onVirtualTargetMaxCount }) => (
|
||||
<SettingSection title="가상매매">
|
||||
<SettingRow
|
||||
label="투자대상 목록 최대 개수"
|
||||
desc="가상매매 화면·추세검색에서 등록할 수 있는 투자대상 종목 수 상한입니다."
|
||||
>
|
||||
<input
|
||||
type="number"
|
||||
className="stg-input stg-input--num"
|
||||
value={virtualTargetMaxCount}
|
||||
min={1}
|
||||
max={100}
|
||||
step={1}
|
||||
onChange={e => onVirtualTargetMaxCount?.(Number(e.target.value))}
|
||||
/>
|
||||
</SettingRow>
|
||||
<p className="stg-hint">
|
||||
모의투자 활성화·자동매매·계좌 비용은 설정의 「모의투자」 카테고리에서 관리합니다.
|
||||
</p>
|
||||
</SettingSection>
|
||||
);
|
||||
|
||||
const LiveTradingSettingsPanel: React.FC<{
|
||||
tradingMode?: string;
|
||||
onTradingMode?: (v: string) => void;
|
||||
liveAutoTradeEnabled?: boolean;
|
||||
onLiveAutoTradeEnabled?: (v: boolean) => void;
|
||||
hasUpbitKeys?: boolean;
|
||||
upbitAccessKeyMasked?: string | null;
|
||||
onUpbitKeys?: (access: string, secret: string) => void;
|
||||
liveAutoTradeBudgetPct?: number;
|
||||
onLiveAutoTradeBudgetPct?: (v: number) => void;
|
||||
}> = ({
|
||||
tradingMode = 'PAPER',
|
||||
onTradingMode,
|
||||
liveAutoTradeEnabled = false,
|
||||
onLiveAutoTradeEnabled,
|
||||
hasUpbitKeys = false,
|
||||
upbitAccessKeyMasked = null,
|
||||
onUpbitKeys,
|
||||
liveAutoTradeBudgetPct = 95,
|
||||
onLiveAutoTradeBudgetPct,
|
||||
}) => {
|
||||
const [accessKey, setAccessKey] = useState('');
|
||||
const [secretKey, setSecretKey] = useState('');
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingSection title="매매 운영 모드">
|
||||
<SettingRow label="실행 대상" desc="모의투자만, 실거래(업비트 API)만, 또는 둘 다 병행할 수 있습니다.">
|
||||
<select className="stg-select" value={tradingMode} onChange={e => onTradingMode?.(e.target.value)}>
|
||||
<option value="PAPER">모의투자만</option>
|
||||
<option value="LIVE">실거래만</option>
|
||||
<option value="BOTH">모의 + 실거래 병행</option>
|
||||
</select>
|
||||
</SettingRow>
|
||||
</SettingSection>
|
||||
<SettingSection title="실거래 (업비트 Open API)">
|
||||
<SettingRow label="실거래 자동매매" desc="ON이면 전략 시그널·손절/익절 시 업비트에 실제 주문합니다. API 키에 주문 권한이 필요합니다.">
|
||||
<label className="stg-toggle">
|
||||
<input type="checkbox" checked={liveAutoTradeEnabled} onChange={e => onLiveAutoTradeEnabled?.(e.target.checked)} />
|
||||
<span className="stg-toggle-slider" />
|
||||
</label>
|
||||
</SettingRow>
|
||||
<SettingRow label="API 키 상태" desc={hasUpbitKeys ? `등록됨 (${upbitAccessKeyMasked})` : 'Access / Secret 키를 입력 후 저장하세요.'}>
|
||||
<span style={{ fontSize: 12, color: hasUpbitKeys ? 'var(--accent)' : 'var(--text3)' }}>
|
||||
{hasUpbitKeys ? '연결됨' : '미등록'}
|
||||
</span>
|
||||
</SettingRow>
|
||||
<SettingRow label="Access Key" desc="입력 내용이 그대로 표시됩니다. 저장 시 DB에 AES 암호화되어 보관됩니다.">
|
||||
<input
|
||||
type="text"
|
||||
className="stg-input stg-input--sensitive"
|
||||
placeholder={hasUpbitKeys ? '변경 시 새 Access Key 입력' : 'Access Key'}
|
||||
value={accessKey}
|
||||
onChange={e => setAccessKey(e.target.value)}
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow label="Secret Key" desc="입력 내용이 그대로 표시됩니다. 저장 시 암호화되며 API로 전체 키는 반환되지 않습니다.">
|
||||
<input
|
||||
type="text"
|
||||
className="stg-input stg-input--sensitive"
|
||||
placeholder={hasUpbitKeys ? '변경 시 새 Secret Key 입력' : 'Secret Key'}
|
||||
value={secretKey}
|
||||
onChange={e => setSecretKey(e.target.value)}
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
/>
|
||||
</SettingRow>
|
||||
{(accessKey.trim() || secretKey.trim()) && (
|
||||
<SettingRow label="">
|
||||
<button
|
||||
type="button"
|
||||
className="stg-btn stg-btn--primary"
|
||||
onClick={() => {
|
||||
onUpbitKeys?.(accessKey.trim(), secretKey.trim());
|
||||
setAccessKey('');
|
||||
setSecretKey('');
|
||||
}}
|
||||
>
|
||||
API 키 저장
|
||||
</button>
|
||||
</SettingRow>
|
||||
)}
|
||||
<SettingRow label="실거래 자동매수 예산 (%)" desc="시장가 매수 시 KRW 잔고 대비 사용 비율 (모의투자 예산과 별도)">
|
||||
<input type="number" className="stg-input stg-input--narrow" min={1} max={100} step={1}
|
||||
value={liveAutoTradeBudgetPct}
|
||||
onChange={e => onLiveAutoTradeBudgetPct?.(Number(e.target.value))} />
|
||||
<span className="stg-unit">%</span>
|
||||
</SettingRow>
|
||||
</SettingSection>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1707,9 +1742,10 @@ const SettingsPage: React.FC<SettingsPageProps> = ({
|
||||
onVerificationIssueNotify,
|
||||
trendSearchSettings,
|
||||
onTrendSearchSettingsChange,
|
||||
initialCategory,
|
||||
}) => {
|
||||
const categories = filterCategories(menuPermissions);
|
||||
const [active, setActive] = useState<CategoryId>('general');
|
||||
const [active, setActive] = useState<CategoryId>(initialCategory ?? 'general');
|
||||
const [indicatorSaveUi, setIndicatorSaveUi] = useState<IndicatorSaveUiState | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -1719,6 +1755,13 @@ const SettingsPage: React.FC<SettingsPageProps> = ({
|
||||
}
|
||||
}, [categories, active]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!initialCategory) return;
|
||||
if (categories.some(c => c.id === initialCategory)) {
|
||||
setActive(initialCategory);
|
||||
}
|
||||
}, [initialCategory, categories]);
|
||||
|
||||
useEffect(() => {
|
||||
if (active !== 'indicators') setIndicatorSaveUi(null);
|
||||
}, [active]);
|
||||
@@ -1811,7 +1854,7 @@ const SettingsPage: React.FC<SettingsPageProps> = ({
|
||||
</AdminPasswordGate>
|
||||
);
|
||||
case 'paper': return (
|
||||
<PaperPanel
|
||||
<PaperTradingSettingsPanel
|
||||
paperTradingEnabled={paperTradingEnabled}
|
||||
onPaperTradingEnabled={onPaperTradingEnabled}
|
||||
paperInitialCapital={paperInitialCapital}
|
||||
@@ -1826,9 +1869,17 @@ const SettingsPage: React.FC<SettingsPageProps> = ({
|
||||
onPaperAutoTradeEnabled={onPaperAutoTradeEnabled}
|
||||
paperAutoTradeBudgetPct={paperAutoTradeBudgetPct}
|
||||
onPaperAutoTradeBudgetPct={onPaperAutoTradeBudgetPct}
|
||||
onPaperAccountReset={onPaperAccountReset}
|
||||
/>
|
||||
);
|
||||
case 'virtual': return (
|
||||
<VirtualTradingSettingsPanel
|
||||
virtualTargetMaxCount={virtualTargetMaxCount}
|
||||
onVirtualTargetMaxCount={onVirtualTargetMaxCount}
|
||||
onPaperAccountReset={onPaperAccountReset}
|
||||
/>
|
||||
);
|
||||
case 'live': return (
|
||||
<LiveTradingSettingsPanel
|
||||
tradingMode={tradingMode}
|
||||
onTradingMode={onTradingMode}
|
||||
liveAutoTradeEnabled={liveAutoTradeEnabled}
|
||||
|
||||
Reference in New Issue
Block a user