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

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
@@ -882,6 +882,7 @@ function ChartWorkspaceViewInner(props: ChartWorkspaceViewProps) {
market={symbol}
strategies={liveStrategies}
settings={liveStrategySettings}
backtestPositionMode={btSettings.positionMode ?? 'LONG_ONLY'}
watchlistCount={isVirtualActive ? monitoredMarkets.length : watchlist.length}
monitoredMarkets={monitoredMarkets}
virtualDriven={isVirtualActive}
+4 -4
View File
@@ -797,10 +797,10 @@ export function useChartWorkspace({
strategyId: appDefaults.liveStrategyId ?? null,
isLiveCheck: appDefaults.liveStrategyCheck ?? false,
executionType: appDefaults.liveExecutionType ?? 'CANDLE_CLOSE',
positionMode: appDefaults.livePositionMode ?? 'LONG_ONLY',
positionMode: btSettings.positionMode ?? 'LONG_ONLY',
strategyCandleTypes: liveStrategyCandleTypes,
};
}, [isVirtualActive, activeLiveSettings, symbol, virtualSession, appDefaults, liveStrategyCandleTypes]);
}, [isVirtualActive, activeLiveSettings, symbol, virtualSession, appDefaults, liveStrategyCandleTypes, btSettings.positionMode]);
/** STOMP 구독 대상 — 가상투자 실행 중이면 투자대상 종목, 아니면 관심종목 */
const monitoredMarkets = useMemo(() => {
@@ -856,7 +856,7 @@ export function useChartWorkspace({
liveStrategyCheck: saved.isLiveCheck,
liveStrategyId: saved.strategyId ?? undefined,
liveExecutionType: saved.executionType,
livePositionMode: saved.positionMode,
livePositionMode: btSettings.positionMode ?? 'LONG_ONLY',
});
if (saved.strategyCandleTypes) {
setLiveStrategyCandleTypes(saved.strategyCandleTypes);
@@ -864,7 +864,7 @@ export function useChartWorkspace({
if (appDefaults.liveStrategyCheck && saved.isLiveCheck) {
refreshActiveLiveSettings();
}
}, [saveAppDef, appDefaults.liveStrategyCheck, isVirtualActive, refreshActiveLiveSettings]);
}, [saveAppDef, appDefaults.liveStrategyCheck, isVirtualActive, refreshActiveLiveSettings, btSettings.positionMode]);
const prevFavoritesRef = useRef<string[]>(getFavorites());
+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>
+2
View File
@@ -1709,6 +1709,7 @@ export async function fetchLiveConditionScanSignals(
chartTimeframe: string,
indicatorParams?: Record<string, Record<string, unknown>> | null,
evaluationBarCount?: number | null,
positionMode?: 'LONG_ONLY' | 'SIGNAL_ONLY' | null,
): Promise<BacktestSignal[]> {
if (chartBars.length === 0 || !chartTimeframe) return [];
const list = await requestOrThrow<BacktestSignal[]>('/strategy/live-conditions/scan-signals', {
@@ -1728,6 +1729,7 @@ export async function fetchLiveConditionScanSignals(
})),
timeframe: chartTimeframe,
evaluationBarCount: evaluationBarCount ?? undefined,
positionMode: positionMode ?? undefined,
}),
});
return list ?? [];
@@ -53,6 +53,8 @@ export async function fetchStrategyEvaluationSignals(opts: {
const initialCapital = btSettings.initialCapital ?? 10_000_000;
const commissionRate = resolveEvaluationCommissionRate(btSettings);
const positionMode = btSettings.positionMode ?? 'LONG_ONLY';
if (isScanSignalsExecutionMode(btSettings)) {
const signals = await fetchLiveConditionScanSignals(
opts.market,
@@ -61,11 +63,12 @@ export async function fetchStrategyEvaluationSignals(opts: {
opts.timeframe,
indicatorParams,
opts.evaluationBarCount,
positionMode,
);
return {
signals,
initialCapital,
settings: { ...btSettings, positionMode: 'SIGNAL_ONLY', tradeExecutionMode },
settings: { ...btSettings, positionMode, tradeExecutionMode },
commissionRate,
tradeExecutionMode,
};
@@ -73,7 +76,7 @@ export async function fetchStrategyEvaluationSignals(opts: {
const settings: BacktestSettingsDto = {
...btSettings,
positionMode: 'SIGNAL_ONLY',
positionMode,
tradeExecutionMode: 'BACKTEST_ENGINE',
};