goldenChat base source add

This commit is contained in:
aidev
2026-05-23 15:11:48 +09:00
commit a4ea7762b5
2081 changed files with 1155760 additions and 0 deletions
@@ -0,0 +1,335 @@
/**
* 설정 화면 — 백테스팅 설정 패널 (2열 카드 레이아웃)
*/
import React, { useState, useEffect, useCallback } from 'react';
import {
type BacktestSettingsDto,
DEFAULT_BACKTEST_SETTINGS,
loadBacktestSettings,
saveBacktestSettings,
} from '../utils/backendApi';
// ── 백테스팅 전용 UI 컴포넌트 ───────────────────────────────────────────────
const BtSection: React.FC<{ title: string; children: React.ReactNode }> = ({ title, children }) => (
<section className="stg-bt-section">
<h3 className="stg-bt-section-title">{title}</h3>
<div className="stg-bt-section-body">{children}</div>
</section>
);
const BtGrid: React.FC<{ children: React.ReactNode }> = ({ children }) => (
<div className="stg-bt-grid">{children}</div>
);
const BtField: React.FC<{
label: string;
desc?: string;
full?: boolean;
children: React.ReactNode;
}> = ({ label, desc, full, children }) => (
<div className={`stg-bt-field${full ? ' stg-bt-field--full' : ''}`}>
<div className="stg-bt-field-head">
<span className="stg-bt-field-label">{label}</span>
{desc && <span className="stg-bt-field-desc">{desc}</span>}
</div>
<div className="stg-bt-field-ctrl">{children}</div>
</div>
);
export interface BacktestSettingsPanelProps {
btAutoPopup?: boolean;
onBtAutoPopup?: (v: boolean) => void;
btShowPrice?: boolean;
onBtShowPrice?: (v: boolean) => void;
}
export const BacktestSettingsPanel: React.FC<BacktestSettingsPanelProps> = ({
btAutoPopup = true,
onBtAutoPopup,
btShowPrice = true,
onBtShowPrice,
}) => {
const [cfg, setCfg] = useState<BacktestSettingsDto>(DEFAULT_BACKTEST_SETTINGS);
const [saving, setSaving] = useState(false);
const [saved, setSaved] = useState(false);
useEffect(() => {
loadBacktestSettings().then(s => { if (s) setCfg(s); });
}, []);
const update = useCallback(async (patch: Partial<BacktestSettingsDto>) => {
const next = { ...cfg, ...patch };
setCfg(next);
setSaving(true);
setSaved(false);
try {
await saveBacktestSettings(next);
setSaved(true);
setTimeout(() => setSaved(false), 1500);
} finally {
setSaving(false);
}
}, [cfg]);
const field = <K extends keyof BacktestSettingsDto>(key: K, value: BacktestSettingsDto[K]) =>
update({ [key]: value } as Partial<BacktestSettingsDto>);
return (
<div className="stg-bt-panel">
{(saving || saved) && (
<div className={`stg-bt-status${saved ? ' stg-bt-status--ok' : ''}`}>
{saving ? '저장 중…' : '✓ 저장됨'}
</div>
)}
<BtSection title="표시 설정">
<BtGrid>
<BtField
label="결과 팝업 자동 표시"
desc="백테스팅 실행 완료 후 결과 팝업을 자동으로 띄웁니다. OFF이면 툴바의 '결과보기' 버튼을 직접 클릭해야 합니다."
>
<label className="stg-toggle">
<input type="checkbox" checked={btAutoPopup} onChange={e => onBtAutoPopup?.(e.target.checked)} />
<span className="stg-toggle-slider" />
</label>
</BtField>
<BtField
label="매수/매도 금액 표시"
desc="차트 마커에 체결 금액을 함께 표시합니다. ON: '매수 : 1,234,000' / OFF: '매수'만 표시."
>
<label className="stg-toggle">
<input type="checkbox" checked={btShowPrice} onChange={e => onBtShowPrice?.(e.target.checked)} />
<span className="stg-toggle-slider" />
</label>
</BtField>
</BtGrid>
</BtSection>
<BtSection title="자본 · 거래 규모">
<BtGrid>
<BtField
label="초기 자본 (원)"
desc="백테스팅 시뮬레이션의 시작 자본금입니다. 수익/손실과 최종 자산을 이 금액 기준으로 계산합니다."
>
<input
className="stg-input stg-input--wide"
type="number" min={100000} step={1000000}
value={cfg.initialCapital}
onChange={e => field('initialCapital', Number(e.target.value))}
/>
</BtField>
<BtField label="거래 규모 유형" desc="자본 비율(복리) 또는 매 거래 고정 금액 중 선택합니다.">
<select className="stg-select stg-select--wide"
value={cfg.tradeSizeType}
onChange={e => field('tradeSizeType', e.target.value as 'CAPITAL_PCT' | 'FIXED_AMOUNT')}>
<option value="CAPITAL_PCT"> (%)</option>
<option value="FIXED_AMOUNT"> ()</option>
</select>
</BtField>
<BtField
label={cfg.tradeSizeType === 'CAPITAL_PCT' ? '사용 자본 비율 (%)' : '거래당 금액 (원)'}
desc={cfg.tradeSizeType === 'CAPITAL_PCT'
? '매 거래에 사용할 자본 비율. 100 = 전체 자본.'
: '매 거래마다 사용할 원화 금액.'}
>
<input
className="stg-input stg-input--wide"
type="number" min={1} step={cfg.tradeSizeType === 'CAPITAL_PCT' ? 1 : 100000}
value={cfg.tradeSizeValue}
onChange={e => field('tradeSizeValue', Number(e.target.value))}
/>
</BtField>
</BtGrid>
</BtSection>
<BtSection title="비용 모델">
<BtGrid>
<BtField label="수수료 유형" desc="LINEAR: 거래금액 × 수수료율. ZERO: 수수료 없음.">
<select className="stg-select stg-select--wide"
value={cfg.commissionType}
onChange={e => field('commissionType', e.target.value as 'LINEAR' | 'ZERO')}>
<option value="LINEAR">LINEAR ( )</option>
<option value="ZERO">ZERO ( )</option>
</select>
</BtField>
{cfg.commissionType === 'LINEAR' ? (
<BtField label="수수료율 (%)" desc="매수·매도 각 1회당 수수료율. 업비트 현물 약 0.05%.">
<input
className="stg-input stg-input--wide"
type="number" min={0} max={5} step={0.01}
value={(cfg.commissionRate ?? 0) * 100}
onChange={e => field('commissionRate', Number(e.target.value) / 100)}
/>
</BtField>
) : (
<div className="stg-bt-field stg-bt-field--placeholder" aria-hidden />
)}
<BtField
label="슬리피지 (%)"
desc="의도 가격과 실제 체결 가격 차이. 매수는 높게, 매도는 낮게 체결되는 효과."
>
<input
className="stg-input stg-input--wide"
type="number" min={0} max={5} step={0.01}
value={(cfg.slippageRate ?? 0) * 100}
onChange={e => field('slippageRate', Number(e.target.value) / 100)}
/>
</BtField>
</BtGrid>
</BtSection>
<BtSection title="거래 가격 기준">
<BtGrid>
<BtField label="진입 가격 기준" desc="매수 신호 시 진입 가격. 종가 즉시 진입 또는 다음 봉 시가 진입.">
<select className="stg-select stg-select--wide"
value={cfg.entryPriceType}
onChange={e => field('entryPriceType', e.target.value as 'CLOSE' | 'NEXT_OPEN')}>
<option value="CLOSE"> (Close)</option>
<option value="NEXT_OPEN"> (Next Open)</option>
</select>
</BtField>
<BtField label="청산 가격 기준" desc="매도 신호 시 청산 가격. 종가 즉시 청산 또는 다음 봉 시가 청산.">
<select className="stg-select stg-select--wide"
value={cfg.exitPriceType}
onChange={e => field('exitPriceType', e.target.value as 'CLOSE' | 'NEXT_OPEN')}>
<option value="CLOSE"> (Close)</option>
<option value="NEXT_OPEN"> (Next Open)</option>
</select>
</BtField>
<BtField label="포지션 방향" desc="LONG(매수만), SHORT(공매도), BOTH(양방향) 중 선택.">
<select className="stg-select stg-select--wide"
value={cfg.positionDirection}
onChange={e => field('positionDirection', e.target.value as 'LONG' | 'SHORT' | 'BOTH')}>
<option value="LONG">LONG ()</option>
<option value="SHORT">SHORT ()</option>
<option value="BOTH">BOTH ()</option>
</select>
</BtField>
</BtGrid>
</BtSection>
<BtSection title="리스크 관리">
<BtGrid>
<BtField label="손절 활성화" desc="진입가 대비 N% 하락 시 자동 청산 (StopLossRule).">
<label className="stg-toggle">
<input type="checkbox" checked={!!cfg.stopLossEnabled}
onChange={e => field('stopLossEnabled', e.target.checked)} />
<span className="stg-toggle-slider" />
</label>
</BtField>
{cfg.stopLossEnabled ? (
<BtField label="손절 비율 (%)" desc="진입가 대비 하락 허용 비율.">
<input className="stg-input stg-input--wide" type="number" min={0.1} max={50} step={0.1}
value={cfg.stopLossPct ?? 2}
onChange={e => field('stopLossPct', Number(e.target.value))} />
</BtField>
) : <div className="stg-bt-field stg-bt-field--placeholder" aria-hidden />}
<BtField label="익절 활성화" desc="진입가 대비 N% 상승 시 자동 청산 (StopGainRule).">
<label className="stg-toggle">
<input type="checkbox" checked={!!cfg.takeProfitEnabled}
onChange={e => field('takeProfitEnabled', e.target.checked)} />
<span className="stg-toggle-slider" />
</label>
</BtField>
{cfg.takeProfitEnabled ? (
<BtField label="익절 비율 (%)" desc="진입가 대비 상승 목표 비율.">
<input className="stg-input stg-input--wide" type="number" min={0.1} max={200} step={0.1}
value={cfg.takeProfitPct ?? 5}
onChange={e => field('takeProfitPct', Number(e.target.value))} />
</BtField>
) : <div className="stg-bt-field stg-bt-field--placeholder" aria-hidden />}
<BtField label="트레일링 스탑 활성화" desc="최고가 대비 N% 하락 시 자동 청산.">
<label className="stg-toggle">
<input type="checkbox" checked={!!cfg.trailingStopEnabled}
onChange={e => field('trailingStopEnabled', e.target.checked)} />
<span className="stg-toggle-slider" />
</label>
</BtField>
{cfg.trailingStopEnabled ? (
<BtField label="트레일링 비율 (%)" desc="최고가 대비 하락 허용 비율.">
<input className="stg-input stg-input--wide" type="number" min={0.1} max={50} step={0.1}
value={cfg.trailingStopPct ?? 2}
onChange={e => field('trailingStopPct', Number(e.target.value))} />
</BtField>
) : <div className="stg-bt-field stg-bt-field--placeholder" aria-hidden />}
</BtGrid>
</BtSection>
<BtSection title="재진입 · 분할 청산">
<BtGrid>
<BtField label="재진입 대기 봉 수" desc="매도 후 다음 매수까지 최소 대기 캔들 수. 0 = 즉시 재진입.">
<input className="stg-input stg-input--wide" type="number" min={0} max={100} step={1}
value={cfg.reentryWaitBars ?? 0}
onChange={e => field('reentryWaitBars', Number(e.target.value))} />
</BtField>
<BtField label="최대 동시 포지션" desc="동시 보유 가능한 최대 포지션 수 (현재 단일 포지션 지원).">
<input className="stg-input stg-input--wide" type="number" min={1} max={10} step={1}
value={cfg.maxOpenTrades ?? 1}
onChange={e => field('maxOpenTrades', Number(e.target.value))} />
</BtField>
<BtField label="분할 청산 활성화" desc="첫 매도 신호 시 포지션의 일부만 청산합니다.">
<label className="stg-toggle">
<input type="checkbox" checked={!!cfg.partialExitEnabled}
onChange={e => field('partialExitEnabled', e.target.checked)} />
<span className="stg-toggle-slider" />
</label>
</BtField>
{cfg.partialExitEnabled ? (
<BtField label="1차 청산 비율 (%)" desc="첫 매도 시 청산할 포지션 비율.">
<input className="stg-input stg-input--wide" type="number" min={10} max={90} step={5}
value={cfg.partialExitPct ?? 50}
onChange={e => field('partialExitPct', Number(e.target.value))} />
</BtField>
) : <div className="stg-bt-field stg-bt-field--placeholder" aria-hidden />}
</BtGrid>
</BtSection>
<BtSection title="포지션 종속성 모드">
<BtField
full
label="매매 시그널 발생 조건"
desc="매도 시그널이 이전 매수(포지션) 없이도 발생할지 선택합니다."
>
<div className="stg-bt-radio-group">
<label className="stg-bt-radio">
<input
type="radio"
name="bt-pos-mode"
value="LONG_ONLY"
checked={(cfg.positionMode ?? 'LONG_ONLY') === 'LONG_ONLY'}
onChange={() => field('positionMode', 'LONG_ONLY')}
/>
<span className="stg-bt-radio-text">
<strong> (Long-Only)</strong>
<span> · Ta4j </span>
</span>
</label>
<label className="stg-bt-radio">
<input
type="radio"
name="bt-pos-mode"
value="SIGNAL_ONLY"
checked={cfg.positionMode === 'SIGNAL_ONLY'}
onChange={() => field('positionMode', 'SIGNAL_ONLY')}
/>
<span className="stg-bt-radio-text">
<strong> (Signal-Only)</strong>
<span> , · </span>
</span>
</label>
</div>
</BtField>
{cfg.positionMode === 'SIGNAL_ONLY' && (
<div className="stg-bt-hint stg-bt-hint--warn">
Signal-Only 모드: 매도
</div>
)}
</BtSection>
</div>
);
};
export default BacktestSettingsPanel;