goldenChat base source add
This commit is contained in:
@@ -0,0 +1,217 @@
|
||||
/**
|
||||
* LiveStrategyPanel
|
||||
*
|
||||
* 실시간 전략 체크 — 전역 설정 UI.
|
||||
* 관심종목(★) 등록 종목 전체가 체크 대상이며, 여기서 전략·실행 방식을 지정합니다.
|
||||
*/
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import type { Theme } from '../types';
|
||||
import {
|
||||
saveLiveStrategySettings,
|
||||
type LiveStrategySettingsDto,
|
||||
} from '../utils/backendApi';
|
||||
|
||||
interface Strategy {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface LiveStrategyPanelProps {
|
||||
theme: Theme;
|
||||
market: string;
|
||||
strategies: Strategy[];
|
||||
settings: LiveStrategySettingsDto;
|
||||
watchlistCount?: number;
|
||||
monitoredMarkets?: string[];
|
||||
onClearMarkers?: () => void;
|
||||
onClose?: () => void;
|
||||
onSettingsChange?: (settings: LiveStrategySettingsDto) => void;
|
||||
}
|
||||
|
||||
const LiveStrategyPanel: React.FC<LiveStrategyPanelProps> = ({
|
||||
theme, market, strategies, settings,
|
||||
watchlistCount = 0, monitoredMarkets = [],
|
||||
onClearMarkers, onClose, onSettingsChange,
|
||||
}) => {
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
const persist = useCallback(async (patch: Partial<LiveStrategySettingsDto>) => {
|
||||
const next: LiveStrategySettingsDto = { ...settings, ...patch, market };
|
||||
const prev = settings;
|
||||
onSettingsChange?.(next);
|
||||
setSaving(true);
|
||||
try {
|
||||
const saved = await saveLiveStrategySettings(next);
|
||||
if (saved) {
|
||||
onSettingsChange?.(saved);
|
||||
onClearMarkers?.();
|
||||
} else {
|
||||
onSettingsChange?.(prev);
|
||||
window.alert('실시간 전략 설정 저장에 실패했습니다. 네트워크·서버 상태를 확인해 주세요.');
|
||||
}
|
||||
} catch {
|
||||
onSettingsChange?.(prev);
|
||||
window.alert('실시간 전략 설정 저장 중 오류가 발생했습니다.');
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
}, [settings, market, onClearMarkers, onSettingsChange]);
|
||||
|
||||
const isOn = settings.isLiveCheck;
|
||||
const execType = settings.executionType;
|
||||
const stratId = settings.strategyId;
|
||||
const monCount = monitoredMarkets.length;
|
||||
|
||||
return (
|
||||
<div className={`lsp-wrap lsp-wrap--${theme}`}>
|
||||
<div className="lsp-header">
|
||||
<svg className="lsp-icon" width="13" height="13" viewBox="0 0 14 14"
|
||||
fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round">
|
||||
<circle cx="7" cy="7" r="5.5"/>
|
||||
<polyline points="5,7 7,9 10,5"/>
|
||||
</svg>
|
||||
<span className="lsp-title">실시간 전략 체크</span>
|
||||
{saving && <span className="lsp-saving">저장 중…</span>}
|
||||
{onClose && (
|
||||
<button className="lsp-close-btn" onClick={onClose} title="닫기">
|
||||
<svg width="11" height="11" viewBox="0 0 11 11" fill="none"
|
||||
stroke="currentColor" strokeWidth="1.8" strokeLinecap="round">
|
||||
<line x1="1" y1="1" x2="10" y2="10"/>
|
||||
<line x1="10" y1="1" x2="1" y2="10"/>
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="lsp-monitor-stats">
|
||||
<span>관심 {watchlistCount}개</span>
|
||||
<span className="lsp-monitor-sep">·</span>
|
||||
<span className={monCount > 0 ? 'lsp-monitor-on' : ''}>
|
||||
체크 대상 {monCount}개
|
||||
</span>
|
||||
</div>
|
||||
{isOn && stratId && watchlistCount > 0 && (
|
||||
<p className="lsp-monitor-hint">
|
||||
★ 관심종목으로 등록한 종목이 자동으로 전략 체크 대상입니다.
|
||||
</p>
|
||||
)}
|
||||
{monCount > 0 && (
|
||||
<div className="lsp-monitor-list" title={monitoredMarkets.join(', ')}>
|
||||
{monitoredMarkets.slice(0, 8).map(m => (
|
||||
<span key={m} className="lsp-monitor-chip">{m.replace('KRW-', '')}</span>
|
||||
))}
|
||||
{monCount > 8 && <span className="lsp-monitor-chip">+{monCount - 8}</span>}
|
||||
</div>
|
||||
)}
|
||||
{isOn && !stratId && (
|
||||
<p className="lsp-monitor-hint">전략을 선택하면 관심 {watchlistCount}개 종목에 적용됩니다.</p>
|
||||
)}
|
||||
|
||||
<div className="lsp-row">
|
||||
<span className="lsp-label">실시간 체크</span>
|
||||
<label className="lsp-toggle">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={isOn}
|
||||
onChange={e => persist({ isLiveCheck: e.target.checked })}
|
||||
/>
|
||||
<span className="lsp-toggle-slider" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className={`lsp-row${!isOn ? ' lsp-row--disabled' : ''}`}>
|
||||
<span className="lsp-label">전략 선택</span>
|
||||
<select
|
||||
className="lsp-select"
|
||||
disabled={!isOn}
|
||||
value={stratId ?? ''}
|
||||
onChange={e => persist({ strategyId: e.target.value ? Number(e.target.value) : null })}
|
||||
>
|
||||
<option value="">전략 선택…</option>
|
||||
{strategies.map(s => (
|
||||
<option key={s.id} value={s.id}>{s.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className={`lsp-exec-wrap${!isOn ? ' lsp-row--disabled' : ''}`}>
|
||||
<span className="lsp-label">실행 방식</span>
|
||||
<div className="lsp-radio-group">
|
||||
<label className="lsp-radio">
|
||||
<input
|
||||
type="radio"
|
||||
name="execType"
|
||||
value="CANDLE_CLOSE"
|
||||
checked={execType === 'CANDLE_CLOSE'}
|
||||
disabled={!isOn}
|
||||
onChange={() => persist({ executionType: 'CANDLE_CLOSE' })}
|
||||
/>
|
||||
<span className="lsp-radio-label">
|
||||
<span className="lsp-radio-title">봉 마감 직후</span>
|
||||
<span className="lsp-radio-desc">완성된 캔들 기준으로 1회 판정</span>
|
||||
</span>
|
||||
</label>
|
||||
<label className="lsp-radio">
|
||||
<input
|
||||
type="radio"
|
||||
name="execType"
|
||||
value="REALTIME_TICK"
|
||||
checked={execType === 'REALTIME_TICK'}
|
||||
disabled={!isOn}
|
||||
onChange={() => persist({ executionType: 'REALTIME_TICK' })}
|
||||
/>
|
||||
<span className="lsp-radio-label">
|
||||
<span className="lsp-radio-title">실시간 틱 (3초)</span>
|
||||
<span className="lsp-radio-desc">진행 중인 캔들 기준, 3초 주기 판정</span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={`lsp-exec-wrap${!isOn ? ' lsp-row--disabled' : ''}`}>
|
||||
<span className="lsp-label">시그널 모드</span>
|
||||
<div className="lsp-radio-group">
|
||||
<label className="lsp-radio">
|
||||
<input
|
||||
type="radio"
|
||||
name="lsp-posMode"
|
||||
value="LONG_ONLY"
|
||||
checked={(settings.positionMode ?? 'LONG_ONLY') === 'LONG_ONLY'}
|
||||
disabled={!isOn}
|
||||
onChange={() => persist({ positionMode: 'LONG_ONLY' })}
|
||||
/>
|
||||
<span className="lsp-radio-label">
|
||||
<span className="lsp-radio-title">보유 자산 기준</span>
|
||||
<span className="lsp-radio-desc">매수 이력 있을 때만 매도 허용</span>
|
||||
</span>
|
||||
</label>
|
||||
<label className="lsp-radio">
|
||||
<input
|
||||
type="radio"
|
||||
name="lsp-posMode"
|
||||
value="SIGNAL_ONLY"
|
||||
checked={settings.positionMode === 'SIGNAL_ONLY'}
|
||||
disabled={!isOn}
|
||||
onChange={() => persist({ positionMode: 'SIGNAL_ONLY' })}
|
||||
/>
|
||||
<span className="lsp-radio-label">
|
||||
<span className="lsp-radio-title">순수 지표 기준</span>
|
||||
<span className="lsp-radio-desc">포지션 무관, 지표 충족 시 즉시 매도</span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isOn && (
|
||||
<div className="lsp-status">
|
||||
<span className={`lsp-dot${isOn ? ' lsp-dot--on' : ''}`} />
|
||||
{stratId
|
||||
? `${strategies.find(s => s.id === stratId)?.name ?? '전략'} · 관심 ${monCount}종목`
|
||||
: '전략을 선택하세요'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LiveStrategyPanel;
|
||||
Reference in New Issue
Block a user