매매시그널 실행조건 화면 공용 적용

This commit is contained in:
Macbook
2026-06-22 00:02:59 +09:00
parent 0d14fd4ad2
commit 977dd54e64
10 changed files with 168 additions and 94 deletions
+33 -33
View File
@@ -29,6 +29,8 @@ interface LiveStrategyPanelProps {
market: string;
strategies: Strategy[];
settings: LiveStrategySettingsDto;
/** 백테스트 설정 positionMode — 전역 실시간 전략에 적용 */
backtestPositionMode?: 'LONG_ONLY' | 'SIGNAL_ONLY';
watchlistCount?: number;
monitoredMarkets?: string[];
/** 가상투자 세션에서 제어 — 차트 팝업은 읽기 전용 */
@@ -54,6 +56,7 @@ const CandleIcon: React.FC<{ color: string }> = ({ color }) => (
const LiveStrategyPanel: React.FC<LiveStrategyPanelProps> = ({
theme, market, strategies, settings,
backtestPositionMode = 'LONG_ONLY',
watchlistCount = 0, monitoredMarkets = [], virtualDriven = false,
onClearMarkers, onClose, onSettingsChange,
}) => {
@@ -73,7 +76,8 @@ const LiveStrategyPanel: React.FC<LiveStrategyPanelProps> = ({
const persist = useCallback(async (patch: Partial<LiveStrategySettingsDto>) => {
if (virtualDriven) return;
const next: LiveStrategySettingsDto = { ...settings, ...patch, market };
const positionMode = backtestPositionMode;
const next: LiveStrategySettingsDto = { ...settings, ...patch, market, positionMode };
const prev = settings;
if (next.isLiveCheck && next.strategyId != null) {
const synced = await syncStrategyTimeframesFromLayoutIfNeeded(next.strategyId);
@@ -102,7 +106,7 @@ const LiveStrategyPanel: React.FC<LiveStrategyPanelProps> = ({
} finally {
setSaving(false);
}
}, [settings, market, onClearMarkers, onSettingsChange, virtualDriven]);
}, [settings, market, onClearMarkers, onSettingsChange, virtualDriven, backtestPositionMode]);
const isOn = settings.isLiveCheck;
const execType = settings.executionType;
@@ -114,7 +118,10 @@ const LiveStrategyPanel: React.FC<LiveStrategyPanelProps> = ({
? settings.strategyCandleTypes.join(', ')
: '1m';
const execLabel = execType === 'REALTIME_TICK' ? '실시간 틱 (3초)' : '봉 마감 직후';
const posModeLabel = (settings.positionMode ?? 'LONG_ONLY') === 'LONG_ONLY'
const effectivePosMode = virtualDriven
? (settings.positionMode ?? 'LONG_ONLY')
: backtestPositionMode;
const posModeLabel = effectivePosMode === 'LONG_ONLY'
? '보유 자산 기준'
: '순수 지표 기준';
@@ -267,36 +274,29 @@ const LiveStrategyPanel: React.FC<LiveStrategyPanelProps> = ({
<div className={`lsp-section${!isOn ? ' lsp-section--disabled' : ''}`}>
<span className="lsp-section-label"> </span>
<div className="lsp-option-group">
<label className={`lsp-option${(settings.positionMode ?? 'LONG_ONLY') === 'LONG_ONLY' ? ' lsp-option--active' : ''}`}>
<input
type="radio"
name="lsp-posMode"
value="LONG_ONLY"
checked={(settings.positionMode ?? 'LONG_ONLY') === 'LONG_ONLY'}
disabled={virtualDriven || !isOn}
onChange={() => persist({ positionMode: 'LONG_ONLY' })}
/>
<span className="lsp-option-text">
<span className="lsp-option-title"> </span>
<span className="lsp-option-desc"> </span>
</span>
</label>
<label className={`lsp-option${settings.positionMode === 'SIGNAL_ONLY' ? ' lsp-option--active' : ''}`}>
<input
type="radio"
name="lsp-posMode"
value="SIGNAL_ONLY"
checked={settings.positionMode === 'SIGNAL_ONLY'}
disabled={virtualDriven || !isOn}
onChange={() => persist({ positionMode: 'SIGNAL_ONLY' })}
/>
<span className="lsp-option-text">
<span className="lsp-option-title"> </span>
<span className="lsp-option-desc"> , </span>
</span>
</label>
</div>
{virtualDriven ? (
<div className="lsp-option-group">
<label className={`lsp-option${effectivePosMode === 'LONG_ONLY' ? ' lsp-option--active' : ''}`}>
<input type="radio" name="lsp-posMode" value="LONG_ONLY" checked={effectivePosMode === 'LONG_ONLY'} disabled />
<span className="lsp-option-text">
<span className="lsp-option-title"> </span>
<span className="lsp-option-desc"> </span>
</span>
</label>
<label className={`lsp-option${effectivePosMode === 'SIGNAL_ONLY' ? ' lsp-option--active' : ''}`}>
<input type="radio" name="lsp-posMode" value="SIGNAL_ONLY" checked={effectivePosMode === 'SIGNAL_ONLY'} disabled />
<span className="lsp-option-text">
<span className="lsp-option-title"> </span>
<span className="lsp-option-desc"> , </span>
</span>
</label>
</div>
) : (
<p className="lsp-hint" style={{ margin: 0 }}>
<strong>{posModeLabel}</strong>
{' '}( )
</p>
)}
</div>
{isOn && !stratId && (
+28 -33
View File
@@ -5,6 +5,7 @@
import React, { useState, useEffect, useCallback } from 'react';
import type { Theme } from '../types';
import {
loadBacktestSettings,
saveLiveStrategySettings,
type LiveStrategySettingsDto,
} from '../utils/backendApi';
@@ -1347,6 +1348,15 @@ const StrategyPanel: React.FC<StrategyPanelProps> = ({
},
);
const [liveSaving, setLiveSaving] = useState(false);
const [backtestPositionMode, setBacktestPositionMode] = useState<'LONG_ONLY' | 'SIGNAL_ONLY'>('LONG_ONLY');
useEffect(() => {
void loadBacktestSettings().then(s => {
if (s?.positionMode === 'SIGNAL_ONLY' || s?.positionMode === 'LONG_ONLY') {
setBacktestPositionMode(s.positionMode);
}
});
}, []);
useEffect(() => {
if (liveSettingsProp) setLiveSettings(liveSettingsProp);
@@ -1354,7 +1364,12 @@ const StrategyPanel: React.FC<StrategyPanelProps> = ({
const persistLive = useCallback(async (patch: Partial<LiveStrategySettingsDto>) => {
if (virtualDriven) return;
const next: LiveStrategySettingsDto = { ...liveSettings, ...patch, market: liveMarket };
const next: LiveStrategySettingsDto = {
...liveSettings,
...patch,
market: liveMarket,
positionMode: backtestPositionMode,
};
if (next.isLiveCheck && next.strategyId != null) {
const synced = await syncStrategyTimeframesFromLayoutIfNeeded(next.strategyId);
if (!synced) return;
@@ -1370,7 +1385,7 @@ const StrategyPanel: React.FC<StrategyPanelProps> = ({
} finally {
setLiveSaving(false);
}
}, [liveSettings, liveMarket, onClearLiveMarkers, onLiveSettingsChange, virtualDriven]);
}, [liveSettings, liveMarket, onClearLiveMarkers, onLiveSettingsChange, virtualDriven, backtestPositionMode]);
const isOn = liveSettings.isLiveCheck;
const execType = liveSettings.executionType;
@@ -1502,38 +1517,18 @@ const StrategyPanel: React.FC<StrategyPanelProps> = ({
<SettingRow
className="stg-row--block"
label="포지션 종속성 모드"
desc="매도 시그널 발생 조건을 선택합니다. LONG_ONLY: 매수 이력이 있을 때만 매도 허용. SIGNAL_ONLY: 포지션 없이도 지표 규칙 충족 시 즉시 매도 시그널."
label="시그널 모드"
desc="백테스트 설정의 포지션 종속성 모드와 동일하게 적용됩니다. 변경은 설정 → 백테스트 설정에서 하세요."
>
<div className={`stg-radio-row${!isOn ? ' stg-radio-row--disabled' : ''}`}>
<label className="stg-radio-option stg-radio-option--blue">
<input
type="radio"
name="stg-pos-mode"
value="LONG_ONLY"
checked={(liveSettings.positionMode ?? 'LONG_ONLY') === 'LONG_ONLY'}
disabled={!isOn}
onChange={() => persistLive({ positionMode: 'LONG_ONLY' })}
/>
<span className="stg-radio-option-text">
<strong> (Long-Only)</strong>
<span> </span>
</span>
</label>
<label className="stg-radio-option stg-radio-option--orange">
<input
type="radio"
name="stg-pos-mode"
value="SIGNAL_ONLY"
checked={liveSettings.positionMode === 'SIGNAL_ONLY'}
disabled={!isOn}
onChange={() => persistLive({ positionMode: 'SIGNAL_ONLY' })}
/>
<span className="stg-radio-option-text">
<strong> (Signal-Only)</strong>
<span> , </span>
</span>
</label>
<div style={{
padding: '8px 10px', borderRadius: 6,
background: 'rgba(63,126,245,0.08)', border: '1px solid rgba(63,126,245,0.2)',
fontSize: 12, color: 'var(--text2)',
opacity: isOn ? 1 : 0.55,
}}>
{backtestPositionMode === 'LONG_ONLY'
? '보유 자산 기준 (Long-Only) — 매수 이력 있을 때만 매도 허용'
: '순수 지표 기준 (Signal-Only) — 포지션 무관, 지표 충족 시 즉시 매도'}
</div>
</SettingRow>
</SettingSection>