매매시그널 실행조건 화면 공용 적용
This commit is contained in:
@@ -76,6 +76,7 @@ public class LiveConditionController {
|
|||||||
body.getBars(),
|
body.getBars(),
|
||||||
body.getTimeframe(),
|
body.getTimeframe(),
|
||||||
body.getIndicatorParams(),
|
body.getIndicatorParams(),
|
||||||
body.getEvaluationBarCount()));
|
body.getEvaluationBarCount(),
|
||||||
|
body.getPositionMode()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,4 +18,6 @@ public class LiveConditionScanSignalsRequest {
|
|||||||
private String timeframe;
|
private String timeframe;
|
||||||
/** 평가·표시 구간 봉 수 — null 이면 전체 bars */
|
/** 평가·표시 구간 봉 수 — null 이면 전체 bars */
|
||||||
private Integer evaluationBarCount;
|
private Integer evaluationBarCount;
|
||||||
|
/** "LONG_ONLY" | "SIGNAL_ONLY" — 백테스트 설정 positionMode 와 동일 */
|
||||||
|
private String positionMode;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import lombok.RequiredArgsConstructor;
|
|||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.ta4j.core.BarSeries;
|
import org.ta4j.core.BarSeries;
|
||||||
|
import org.ta4j.core.BaseTradingRecord;
|
||||||
import org.ta4j.core.Rule;
|
import org.ta4j.core.Rule;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
@@ -759,7 +760,8 @@ public class LiveConditionStatusService {
|
|||||||
List<OhlcvBar> chartBars,
|
List<OhlcvBar> chartBars,
|
||||||
String chartTimeframe,
|
String chartTimeframe,
|
||||||
Map<String, Map<String, Object>> indicatorParamsOverride,
|
Map<String, Map<String, Object>> indicatorParamsOverride,
|
||||||
Integer evaluationBarCount) {
|
Integer evaluationBarCount,
|
||||||
|
String positionMode) {
|
||||||
Optional<GcStrategy> opt = strategyRepo.findById(strategyId);
|
Optional<GcStrategy> opt = strategyRepo.findById(strategyId);
|
||||||
if (opt.isEmpty() || chartBars == null || chartBars.isEmpty()
|
if (opt.isEmpty() || chartBars == null || chartBars.isEmpty()
|
||||||
|| chartTimeframe == null || chartTimeframe.isBlank()) {
|
|| chartTimeframe == null || chartTimeframe.isBlank()) {
|
||||||
@@ -808,10 +810,15 @@ public class LiveConditionStatusService {
|
|||||||
List<BacktestResponse.Signal> signals = new ArrayList<>();
|
List<BacktestResponse.Signal> signals = new ArrayList<>();
|
||||||
int buyHits = 0;
|
int buyHits = 0;
|
||||||
int sellHits = 0;
|
int sellHits = 0;
|
||||||
|
String posMode = "SIGNAL_ONLY".equals(positionMode) ? "SIGNAL_ONLY" : "LONG_ONLY";
|
||||||
|
boolean signalOnly = "SIGNAL_ONLY".equals(posMode);
|
||||||
|
BaseTradingRecord record = signalOnly ? null : new BaseTradingRecord();
|
||||||
|
boolean inPosition = false;
|
||||||
for (int i = startIdx; i <= primarySeries.getEndIndex(); i++) {
|
for (int i = startIdx; i <= primarySeries.getEndIndex(); i++) {
|
||||||
long barTimeSec = barOpenEpochSec(primarySeries, i);
|
long barTimeSec = barOpenEpochSec(primarySeries, i);
|
||||||
double close = primarySeries.getBar(i).getClosePrice().doubleValue();
|
double close = primarySeries.getBar(i).getClosePrice().doubleValue();
|
||||||
|
|
||||||
|
if (signalOnly) {
|
||||||
if (entryRule.isSatisfied(i, null)) {
|
if (entryRule.isSatisfied(i, null)) {
|
||||||
buyHits++;
|
buyHits++;
|
||||||
signals.add(BacktestResponse.Signal.builder()
|
signals.add(BacktestResponse.Signal.builder()
|
||||||
@@ -832,15 +839,45 @@ public class LiveConditionStatusService {
|
|||||||
.quantity(0)
|
.quantity(0)
|
||||||
.build());
|
.build());
|
||||||
}
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!inPosition) {
|
||||||
|
if (entryRule.isSatisfied(i, record)) {
|
||||||
|
buyHits++;
|
||||||
|
signals.add(BacktestResponse.Signal.builder()
|
||||||
|
.time(barTimeSec)
|
||||||
|
.type("BUY")
|
||||||
|
.price(close)
|
||||||
|
.barIndex(i)
|
||||||
|
.quantity(0)
|
||||||
|
.build());
|
||||||
|
record.enter(i, primarySeries.numFactory().numOf(close),
|
||||||
|
primarySeries.numFactory().numOf(1));
|
||||||
|
inPosition = true;
|
||||||
|
}
|
||||||
|
} else if (exitRule.isSatisfied(i, record)) {
|
||||||
|
sellHits++;
|
||||||
|
signals.add(BacktestResponse.Signal.builder()
|
||||||
|
.time(barTimeSec)
|
||||||
|
.type("SELL")
|
||||||
|
.price(close)
|
||||||
|
.barIndex(i)
|
||||||
|
.quantity(0)
|
||||||
|
.build());
|
||||||
|
record.exit(i, primarySeries.numFactory().numOf(close),
|
||||||
|
primarySeries.numFactory().numOf(1));
|
||||||
|
inPosition = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (signals.isEmpty()) {
|
if (signals.isEmpty()) {
|
||||||
log.info("[LiveCondition:scan] 0 signals market={} strategy={} tf={} bars={} eval={}..{} cond={} probe={}",
|
log.info("[LiveCondition:scan] 0 signals market={} strategy={} tf={} bars={} eval={}..{} mode={} cond={} probe={}",
|
||||||
market, strategyId, primaryTf, primarySeries.getBarCount(), startIdx,
|
market, strategyId, primaryTf, primarySeries.getBarCount(), startIdx,
|
||||||
primarySeries.getEndIndex(), summarizeBuyCondition(buyDsl),
|
primarySeries.getEndIndex(), posMode, summarizeBuyCondition(buyDsl),
|
||||||
probeEntryRule(entryRule, buyDsl, ruleCtx, startIdx));
|
probeEntryRule(entryRule, buyDsl, ruleCtx, startIdx));
|
||||||
} else {
|
} else {
|
||||||
log.debug("[LiveCondition:scan] market={} strategy={} tf={} buy={} sell={}",
|
log.debug("[LiveCondition:scan] market={} strategy={} tf={} mode={} buy={} sell={}",
|
||||||
market, strategyId, primaryTf, buyHits, sellHits);
|
market, strategyId, primaryTf, posMode, buyHits, sellHits);
|
||||||
}
|
}
|
||||||
return signals;
|
return signals;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
|||||||
@@ -3,11 +3,14 @@ package com.goldenchart.service;
|
|||||||
import com.goldenchart.entity.GcLiveStrategySettings;
|
import com.goldenchart.entity.GcLiveStrategySettings;
|
||||||
import com.goldenchart.entity.GcStrategy;
|
import com.goldenchart.entity.GcStrategy;
|
||||||
import com.goldenchart.entity.GcTradeSignal;
|
import com.goldenchart.entity.GcTradeSignal;
|
||||||
|
import com.goldenchart.dto.BacktestSettingsDto;
|
||||||
|
import com.goldenchart.entity.GcAppSettings;
|
||||||
import com.goldenchart.repository.GcLiveStrategySettingsRepository;
|
import com.goldenchart.repository.GcLiveStrategySettingsRepository;
|
||||||
import com.goldenchart.repository.GcPaperAccountRepository;
|
import com.goldenchart.repository.GcPaperAccountRepository;
|
||||||
import com.goldenchart.repository.GcPaperPositionRepository;
|
import com.goldenchart.repository.GcPaperPositionRepository;
|
||||||
import com.goldenchart.repository.GcStrategyRepository;
|
import com.goldenchart.repository.GcStrategyRepository;
|
||||||
import com.goldenchart.repository.GcTradeSignalRepository;
|
import com.goldenchart.repository.GcTradeSignalRepository;
|
||||||
|
import com.goldenchart.trading.TradingAccess;
|
||||||
import com.goldenchart.storage.Ta4jStorage;
|
import com.goldenchart.storage.Ta4jStorage;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
@@ -71,6 +74,8 @@ public class LiveStrategyEvaluator {
|
|||||||
private final StrategyConditionTimeframeService conditionTimeframes;
|
private final StrategyConditionTimeframeService conditionTimeframes;
|
||||||
private final StrategyTriggerBranchEvaluator triggerBranchEvaluator;
|
private final StrategyTriggerBranchEvaluator triggerBranchEvaluator;
|
||||||
private final StrategyBranchStateCache branchStateCache;
|
private final StrategyBranchStateCache branchStateCache;
|
||||||
|
private final AppSettingsService appSettingsService;
|
||||||
|
private final BacktestSettingsService backtestSettingsService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 트리거 분봉별 Rule 캐시: "market:candleType:strategyId" → (entryRule, exitRule)
|
* 트리거 분봉별 Rule 캐시: "market:candleType:strategyId" → (entryRule, exitRule)
|
||||||
@@ -158,12 +163,13 @@ public class LiveStrategyEvaluator {
|
|||||||
if (!ta4jStorage.exists(market, candleType)) return "NONE";
|
if (!ta4jStorage.exists(market, candleType)) return "NONE";
|
||||||
if (!conditionTimeframes.usesTimeframe(s.getStrategyId(), candleType)) return "NONE";
|
if (!conditionTimeframes.usesTimeframe(s.getStrategyId(), candleType)) return "NONE";
|
||||||
|
|
||||||
|
String posMode = resolvePositionMode(s);
|
||||||
String result = evaluate(market, candleType, s.getStrategyId(),
|
String result = evaluate(market, candleType, s.getStrategyId(),
|
||||||
s.getPositionMode(), maturedIndex,
|
posMode, maturedIndex,
|
||||||
s.getDeviceId(), s.getUserId(), true /* useConfirmedOnly */);
|
s.getDeviceId(), s.getUserId(), true /* useConfirmedOnly */);
|
||||||
if (!"NONE".equals(result)) {
|
if (!"NONE".equals(result)) {
|
||||||
log.info("[Evaluator] CANDLE_CLOSE strategyId={} {} {} idx={} mode={} → {}",
|
log.info("[Evaluator] CANDLE_CLOSE strategyId={} {} {} idx={} mode={} → {}",
|
||||||
s.getStrategyId(), market, candleType, maturedIndex, s.getPositionMode(), result);
|
s.getStrategyId(), market, candleType, maturedIndex, posMode, result);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@@ -213,13 +219,14 @@ public class LiveStrategyEvaluator {
|
|||||||
Integer lastSignaledIdx = realtimeSignaledIdx.get(dedupeKey);
|
Integer lastSignaledIdx = realtimeSignaledIdx.get(dedupeKey);
|
||||||
if (lastSignaledIdx != null && lastSignaledIdx == currentIndex) return "NONE";
|
if (lastSignaledIdx != null && lastSignaledIdx == currentIndex) return "NONE";
|
||||||
|
|
||||||
|
String posMode = resolvePositionMode(s);
|
||||||
// 캐시 완전 우회: 매번 신규 Rule 빌드 (CachedIndicator stale 방지 + 동시성 보장)
|
// 캐시 완전 우회: 매번 신규 Rule 빌드 (CachedIndicator stale 방지 + 동시성 보장)
|
||||||
String result = evaluateWithFreshRules(market, candleType, s.getStrategyId(),
|
String result = evaluateWithFreshRules(market, candleType, s.getStrategyId(),
|
||||||
s.getPositionMode(), currentIndex,
|
posMode, currentIndex,
|
||||||
s.getDeviceId(), s.getUserId(), false /* useConfirmedOnly */);
|
s.getDeviceId(), s.getUserId(), false /* useConfirmedOnly */);
|
||||||
if (!"NONE".equals(result)) {
|
if (!"NONE".equals(result)) {
|
||||||
log.info("[Evaluator] REALTIME_TICK strategyId={} {} {} idx={} mode={} → {}",
|
log.info("[Evaluator] REALTIME_TICK strategyId={} {} {} idx={} mode={} → {}",
|
||||||
s.getStrategyId(), market, candleType, currentIndex, s.getPositionMode(), result);
|
s.getStrategyId(), market, candleType, currentIndex, posMode, result);
|
||||||
realtimeSignaledIdx.put(dedupeKey, currentIndex);
|
realtimeSignaledIdx.put(dedupeKey, currentIndex);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
@@ -257,13 +264,14 @@ public class LiveStrategyEvaluator {
|
|||||||
if (!ta4jStorage.exists(market, candleType)) return "NONE";
|
if (!ta4jStorage.exists(market, candleType)) return "NONE";
|
||||||
if (!conditionTimeframes.usesTimeframe(s.getStrategyId(), candleType)) return "NONE";
|
if (!conditionTimeframes.usesTimeframe(s.getStrategyId(), candleType)) return "NONE";
|
||||||
|
|
||||||
|
String posMode = resolvePositionMode(s);
|
||||||
// 확정봉 기준 평가: 캐시 우회 + useConfirmedOnly=true
|
// 확정봉 기준 평가: 캐시 우회 + useConfirmedOnly=true
|
||||||
String result = evaluateWithFreshRules(market, candleType, s.getStrategyId(),
|
String result = evaluateWithFreshRules(market, candleType, s.getStrategyId(),
|
||||||
s.getPositionMode(), maturedIndex,
|
posMode, maturedIndex,
|
||||||
s.getDeviceId(), s.getUserId(), true /* useConfirmedOnly */);
|
s.getDeviceId(), s.getUserId(), true /* useConfirmedOnly */);
|
||||||
if (!"NONE".equals(result)) {
|
if (!"NONE".equals(result)) {
|
||||||
log.info("[Evaluator] REALTIME_TICK @candle_close strategyId={} {} {} idx={} mode={} → {}",
|
log.info("[Evaluator] REALTIME_TICK @candle_close strategyId={} {} {} idx={} mode={} → {}",
|
||||||
s.getStrategyId(), market, candleType, maturedIndex, s.getPositionMode(), result);
|
s.getStrategyId(), market, candleType, maturedIndex, posMode, result);
|
||||||
realtimeSignaledIdx.put(market + ":" + candleType + ":" + s.getStrategyId(), maturedIndex);
|
realtimeSignaledIdx.put(market + ":" + candleType + ":" + s.getStrategyId(), maturedIndex);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
@@ -371,6 +379,31 @@ public class LiveStrategyEvaluator {
|
|||||||
return "NONE";
|
return "NONE";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 포지션 모드 해석 — 전역 실시간 전략은 백테스트 설정, 가상투자 per-market 은 live 행 값 사용.
|
||||||
|
*/
|
||||||
|
private String resolvePositionMode(GcLiveStrategySettings s) {
|
||||||
|
try {
|
||||||
|
GcAppSettings app = appSettingsService.getEntity(s.getUserId(), s.getDeviceId());
|
||||||
|
boolean globalLive = Boolean.TRUE.equals(app.getLiveStrategyCheck());
|
||||||
|
if (!globalLive && Boolean.TRUE.equals(s.getIsLiveCheck())) {
|
||||||
|
return s.getPositionMode() != null ? s.getPositionMode() : "LONG_ONLY";
|
||||||
|
}
|
||||||
|
String deviceKey = s.getUserId() != null
|
||||||
|
? TradingAccess.accountDeviceKey(s.getUserId())
|
||||||
|
: s.getDeviceId();
|
||||||
|
if (deviceKey != null && !deviceKey.isBlank()) {
|
||||||
|
BacktestSettingsDto bt = backtestSettingsService.get(deviceKey);
|
||||||
|
if (bt.getPositionMode() != null) {
|
||||||
|
return "SIGNAL_ONLY".equals(bt.getPositionMode()) ? "SIGNAL_ONLY" : "LONG_ONLY";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.debug("[Evaluator] positionMode resolve fallback: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
return s.getPositionMode() != null ? s.getPositionMode() : "LONG_ONLY";
|
||||||
|
}
|
||||||
|
|
||||||
private TriggerRules buildTriggerRules(String market, String candleType, long strategyId,
|
private TriggerRules buildTriggerRules(String market, String candleType, long strategyId,
|
||||||
String deviceId, Long userId, boolean useConfirmedOnly) {
|
String deviceId, Long userId, boolean useConfirmedOnly) {
|
||||||
GcStrategy strategy = resolveStrategy(strategyId);
|
GcStrategy strategy = resolveStrategy(strategyId);
|
||||||
|
|||||||
@@ -882,6 +882,7 @@ function ChartWorkspaceViewInner(props: ChartWorkspaceViewProps) {
|
|||||||
market={symbol}
|
market={symbol}
|
||||||
strategies={liveStrategies}
|
strategies={liveStrategies}
|
||||||
settings={liveStrategySettings}
|
settings={liveStrategySettings}
|
||||||
|
backtestPositionMode={btSettings.positionMode ?? 'LONG_ONLY'}
|
||||||
watchlistCount={isVirtualActive ? monitoredMarkets.length : watchlist.length}
|
watchlistCount={isVirtualActive ? monitoredMarkets.length : watchlist.length}
|
||||||
monitoredMarkets={monitoredMarkets}
|
monitoredMarkets={monitoredMarkets}
|
||||||
virtualDriven={isVirtualActive}
|
virtualDriven={isVirtualActive}
|
||||||
|
|||||||
@@ -797,10 +797,10 @@ export function useChartWorkspace({
|
|||||||
strategyId: appDefaults.liveStrategyId ?? null,
|
strategyId: appDefaults.liveStrategyId ?? null,
|
||||||
isLiveCheck: appDefaults.liveStrategyCheck ?? false,
|
isLiveCheck: appDefaults.liveStrategyCheck ?? false,
|
||||||
executionType: appDefaults.liveExecutionType ?? 'CANDLE_CLOSE',
|
executionType: appDefaults.liveExecutionType ?? 'CANDLE_CLOSE',
|
||||||
positionMode: appDefaults.livePositionMode ?? 'LONG_ONLY',
|
positionMode: btSettings.positionMode ?? 'LONG_ONLY',
|
||||||
strategyCandleTypes: liveStrategyCandleTypes,
|
strategyCandleTypes: liveStrategyCandleTypes,
|
||||||
};
|
};
|
||||||
}, [isVirtualActive, activeLiveSettings, symbol, virtualSession, appDefaults, liveStrategyCandleTypes]);
|
}, [isVirtualActive, activeLiveSettings, symbol, virtualSession, appDefaults, liveStrategyCandleTypes, btSettings.positionMode]);
|
||||||
|
|
||||||
/** STOMP 구독 대상 — 가상투자 실행 중이면 투자대상 종목, 아니면 관심종목 */
|
/** STOMP 구독 대상 — 가상투자 실행 중이면 투자대상 종목, 아니면 관심종목 */
|
||||||
const monitoredMarkets = useMemo(() => {
|
const monitoredMarkets = useMemo(() => {
|
||||||
@@ -856,7 +856,7 @@ export function useChartWorkspace({
|
|||||||
liveStrategyCheck: saved.isLiveCheck,
|
liveStrategyCheck: saved.isLiveCheck,
|
||||||
liveStrategyId: saved.strategyId ?? undefined,
|
liveStrategyId: saved.strategyId ?? undefined,
|
||||||
liveExecutionType: saved.executionType,
|
liveExecutionType: saved.executionType,
|
||||||
livePositionMode: saved.positionMode,
|
livePositionMode: btSettings.positionMode ?? 'LONG_ONLY',
|
||||||
});
|
});
|
||||||
if (saved.strategyCandleTypes) {
|
if (saved.strategyCandleTypes) {
|
||||||
setLiveStrategyCandleTypes(saved.strategyCandleTypes);
|
setLiveStrategyCandleTypes(saved.strategyCandleTypes);
|
||||||
@@ -864,7 +864,7 @@ export function useChartWorkspace({
|
|||||||
if (appDefaults.liveStrategyCheck && saved.isLiveCheck) {
|
if (appDefaults.liveStrategyCheck && saved.isLiveCheck) {
|
||||||
refreshActiveLiveSettings();
|
refreshActiveLiveSettings();
|
||||||
}
|
}
|
||||||
}, [saveAppDef, appDefaults.liveStrategyCheck, isVirtualActive, refreshActiveLiveSettings]);
|
}, [saveAppDef, appDefaults.liveStrategyCheck, isVirtualActive, refreshActiveLiveSettings, btSettings.positionMode]);
|
||||||
|
|
||||||
const prevFavoritesRef = useRef<string[]>(getFavorites());
|
const prevFavoritesRef = useRef<string[]>(getFavorites());
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ interface LiveStrategyPanelProps {
|
|||||||
market: string;
|
market: string;
|
||||||
strategies: Strategy[];
|
strategies: Strategy[];
|
||||||
settings: LiveStrategySettingsDto;
|
settings: LiveStrategySettingsDto;
|
||||||
|
/** 백테스트 설정 positionMode — 전역 실시간 전략에 적용 */
|
||||||
|
backtestPositionMode?: 'LONG_ONLY' | 'SIGNAL_ONLY';
|
||||||
watchlistCount?: number;
|
watchlistCount?: number;
|
||||||
monitoredMarkets?: string[];
|
monitoredMarkets?: string[];
|
||||||
/** 가상투자 세션에서 제어 — 차트 팝업은 읽기 전용 */
|
/** 가상투자 세션에서 제어 — 차트 팝업은 읽기 전용 */
|
||||||
@@ -54,6 +56,7 @@ const CandleIcon: React.FC<{ color: string }> = ({ color }) => (
|
|||||||
|
|
||||||
const LiveStrategyPanel: React.FC<LiveStrategyPanelProps> = ({
|
const LiveStrategyPanel: React.FC<LiveStrategyPanelProps> = ({
|
||||||
theme, market, strategies, settings,
|
theme, market, strategies, settings,
|
||||||
|
backtestPositionMode = 'LONG_ONLY',
|
||||||
watchlistCount = 0, monitoredMarkets = [], virtualDriven = false,
|
watchlistCount = 0, monitoredMarkets = [], virtualDriven = false,
|
||||||
onClearMarkers, onClose, onSettingsChange,
|
onClearMarkers, onClose, onSettingsChange,
|
||||||
}) => {
|
}) => {
|
||||||
@@ -73,7 +76,8 @@ const LiveStrategyPanel: React.FC<LiveStrategyPanelProps> = ({
|
|||||||
|
|
||||||
const persist = useCallback(async (patch: Partial<LiveStrategySettingsDto>) => {
|
const persist = useCallback(async (patch: Partial<LiveStrategySettingsDto>) => {
|
||||||
if (virtualDriven) return;
|
if (virtualDriven) return;
|
||||||
const next: LiveStrategySettingsDto = { ...settings, ...patch, market };
|
const positionMode = backtestPositionMode;
|
||||||
|
const next: LiveStrategySettingsDto = { ...settings, ...patch, market, positionMode };
|
||||||
const prev = settings;
|
const prev = settings;
|
||||||
if (next.isLiveCheck && next.strategyId != null) {
|
if (next.isLiveCheck && next.strategyId != null) {
|
||||||
const synced = await syncStrategyTimeframesFromLayoutIfNeeded(next.strategyId);
|
const synced = await syncStrategyTimeframesFromLayoutIfNeeded(next.strategyId);
|
||||||
@@ -102,7 +106,7 @@ const LiveStrategyPanel: React.FC<LiveStrategyPanelProps> = ({
|
|||||||
} finally {
|
} finally {
|
||||||
setSaving(false);
|
setSaving(false);
|
||||||
}
|
}
|
||||||
}, [settings, market, onClearMarkers, onSettingsChange, virtualDriven]);
|
}, [settings, market, onClearMarkers, onSettingsChange, virtualDriven, backtestPositionMode]);
|
||||||
|
|
||||||
const isOn = settings.isLiveCheck;
|
const isOn = settings.isLiveCheck;
|
||||||
const execType = settings.executionType;
|
const execType = settings.executionType;
|
||||||
@@ -114,7 +118,10 @@ const LiveStrategyPanel: React.FC<LiveStrategyPanelProps> = ({
|
|||||||
? settings.strategyCandleTypes.join(', ')
|
? settings.strategyCandleTypes.join(', ')
|
||||||
: '1m';
|
: '1m';
|
||||||
const execLabel = execType === 'REALTIME_TICK' ? '실시간 틱 (3초)' : '봉 마감 직후';
|
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' : ''}`}>
|
<div className={`lsp-section${!isOn ? ' lsp-section--disabled' : ''}`}>
|
||||||
<span className="lsp-section-label">시그널 모드</span>
|
<span className="lsp-section-label">시그널 모드</span>
|
||||||
|
{virtualDriven ? (
|
||||||
<div className="lsp-option-group">
|
<div className="lsp-option-group">
|
||||||
<label className={`lsp-option${(settings.positionMode ?? 'LONG_ONLY') === 'LONG_ONLY' ? ' lsp-option--active' : ''}`}>
|
<label className={`lsp-option${effectivePosMode === 'LONG_ONLY' ? ' lsp-option--active' : ''}`}>
|
||||||
<input
|
<input type="radio" name="lsp-posMode" value="LONG_ONLY" checked={effectivePosMode === 'LONG_ONLY'} disabled />
|
||||||
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-text">
|
||||||
<span className="lsp-option-title">보유 자산 기준</span>
|
<span className="lsp-option-title">보유 자산 기준</span>
|
||||||
<span className="lsp-option-desc">매수 이력 있을 때만 매도 허용</span>
|
<span className="lsp-option-desc">매수 이력 있을 때만 매도 허용</span>
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<label className={`lsp-option${settings.positionMode === 'SIGNAL_ONLY' ? ' lsp-option--active' : ''}`}>
|
<label className={`lsp-option${effectivePosMode === 'SIGNAL_ONLY' ? ' lsp-option--active' : ''}`}>
|
||||||
<input
|
<input type="radio" name="lsp-posMode" value="SIGNAL_ONLY" checked={effectivePosMode === 'SIGNAL_ONLY'} disabled />
|
||||||
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-text">
|
||||||
<span className="lsp-option-title">순수 지표 기준</span>
|
<span className="lsp-option-title">순수 지표 기준</span>
|
||||||
<span className="lsp-option-desc">포지션 무관, 지표 충족 시 즉시 매도</span>
|
<span className="lsp-option-desc">포지션 무관, 지표 충족 시 즉시 매도</span>
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className="lsp-hint" style={{ margin: 0 }}>
|
||||||
|
백테스트 설정과 동일 — <strong>{posModeLabel}</strong>
|
||||||
|
{' '}(백테스트 설정 패널에서 변경)
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isOn && !stratId && (
|
{isOn && !stratId && (
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
import React, { useState, useEffect, useCallback } from 'react';
|
import React, { useState, useEffect, useCallback } from 'react';
|
||||||
import type { Theme } from '../types';
|
import type { Theme } from '../types';
|
||||||
import {
|
import {
|
||||||
|
loadBacktestSettings,
|
||||||
saveLiveStrategySettings,
|
saveLiveStrategySettings,
|
||||||
type LiveStrategySettingsDto,
|
type LiveStrategySettingsDto,
|
||||||
} from '../utils/backendApi';
|
} from '../utils/backendApi';
|
||||||
@@ -1347,6 +1348,15 @@ const StrategyPanel: React.FC<StrategyPanelProps> = ({
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
const [liveSaving, setLiveSaving] = useState(false);
|
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(() => {
|
useEffect(() => {
|
||||||
if (liveSettingsProp) setLiveSettings(liveSettingsProp);
|
if (liveSettingsProp) setLiveSettings(liveSettingsProp);
|
||||||
@@ -1354,7 +1364,12 @@ const StrategyPanel: React.FC<StrategyPanelProps> = ({
|
|||||||
|
|
||||||
const persistLive = useCallback(async (patch: Partial<LiveStrategySettingsDto>) => {
|
const persistLive = useCallback(async (patch: Partial<LiveStrategySettingsDto>) => {
|
||||||
if (virtualDriven) return;
|
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) {
|
if (next.isLiveCheck && next.strategyId != null) {
|
||||||
const synced = await syncStrategyTimeframesFromLayoutIfNeeded(next.strategyId);
|
const synced = await syncStrategyTimeframesFromLayoutIfNeeded(next.strategyId);
|
||||||
if (!synced) return;
|
if (!synced) return;
|
||||||
@@ -1370,7 +1385,7 @@ const StrategyPanel: React.FC<StrategyPanelProps> = ({
|
|||||||
} finally {
|
} finally {
|
||||||
setLiveSaving(false);
|
setLiveSaving(false);
|
||||||
}
|
}
|
||||||
}, [liveSettings, liveMarket, onClearLiveMarkers, onLiveSettingsChange, virtualDriven]);
|
}, [liveSettings, liveMarket, onClearLiveMarkers, onLiveSettingsChange, virtualDriven, backtestPositionMode]);
|
||||||
|
|
||||||
const isOn = liveSettings.isLiveCheck;
|
const isOn = liveSettings.isLiveCheck;
|
||||||
const execType = liveSettings.executionType;
|
const execType = liveSettings.executionType;
|
||||||
@@ -1502,38 +1517,18 @@ const StrategyPanel: React.FC<StrategyPanelProps> = ({
|
|||||||
|
|
||||||
<SettingRow
|
<SettingRow
|
||||||
className="stg-row--block"
|
className="stg-row--block"
|
||||||
label="포지션 종속성 모드"
|
label="시그널 모드"
|
||||||
desc="매도 시그널 발생 조건을 선택합니다. LONG_ONLY: 매수 이력이 있을 때만 매도 허용. SIGNAL_ONLY: 포지션 없이도 지표 규칙 충족 시 즉시 매도 시그널."
|
desc="백테스트 설정의 포지션 종속성 모드와 동일하게 적용됩니다. 변경은 설정 → 백테스트 설정에서 하세요."
|
||||||
>
|
>
|
||||||
<div className={`stg-radio-row${!isOn ? ' stg-radio-row--disabled' : ''}`}>
|
<div style={{
|
||||||
<label className="stg-radio-option stg-radio-option--blue">
|
padding: '8px 10px', borderRadius: 6,
|
||||||
<input
|
background: 'rgba(63,126,245,0.08)', border: '1px solid rgba(63,126,245,0.2)',
|
||||||
type="radio"
|
fontSize: 12, color: 'var(--text2)',
|
||||||
name="stg-pos-mode"
|
opacity: isOn ? 1 : 0.55,
|
||||||
value="LONG_ONLY"
|
}}>
|
||||||
checked={(liveSettings.positionMode ?? 'LONG_ONLY') === 'LONG_ONLY'}
|
{backtestPositionMode === 'LONG_ONLY'
|
||||||
disabled={!isOn}
|
? '보유 자산 기준 (Long-Only) — 매수 이력 있을 때만 매도 허용'
|
||||||
onChange={() => persistLive({ positionMode: 'LONG_ONLY' })}
|
: '순수 지표 기준 (Signal-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>
|
</div>
|
||||||
</SettingRow>
|
</SettingRow>
|
||||||
</SettingSection>
|
</SettingSection>
|
||||||
|
|||||||
@@ -1709,6 +1709,7 @@ export async function fetchLiveConditionScanSignals(
|
|||||||
chartTimeframe: string,
|
chartTimeframe: string,
|
||||||
indicatorParams?: Record<string, Record<string, unknown>> | null,
|
indicatorParams?: Record<string, Record<string, unknown>> | null,
|
||||||
evaluationBarCount?: number | null,
|
evaluationBarCount?: number | null,
|
||||||
|
positionMode?: 'LONG_ONLY' | 'SIGNAL_ONLY' | null,
|
||||||
): Promise<BacktestSignal[]> {
|
): Promise<BacktestSignal[]> {
|
||||||
if (chartBars.length === 0 || !chartTimeframe) return [];
|
if (chartBars.length === 0 || !chartTimeframe) return [];
|
||||||
const list = await requestOrThrow<BacktestSignal[]>('/strategy/live-conditions/scan-signals', {
|
const list = await requestOrThrow<BacktestSignal[]>('/strategy/live-conditions/scan-signals', {
|
||||||
@@ -1728,6 +1729,7 @@ export async function fetchLiveConditionScanSignals(
|
|||||||
})),
|
})),
|
||||||
timeframe: chartTimeframe,
|
timeframe: chartTimeframe,
|
||||||
evaluationBarCount: evaluationBarCount ?? undefined,
|
evaluationBarCount: evaluationBarCount ?? undefined,
|
||||||
|
positionMode: positionMode ?? undefined,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
return list ?? [];
|
return list ?? [];
|
||||||
|
|||||||
@@ -53,6 +53,8 @@ export async function fetchStrategyEvaluationSignals(opts: {
|
|||||||
const initialCapital = btSettings.initialCapital ?? 10_000_000;
|
const initialCapital = btSettings.initialCapital ?? 10_000_000;
|
||||||
const commissionRate = resolveEvaluationCommissionRate(btSettings);
|
const commissionRate = resolveEvaluationCommissionRate(btSettings);
|
||||||
|
|
||||||
|
const positionMode = btSettings.positionMode ?? 'LONG_ONLY';
|
||||||
|
|
||||||
if (isScanSignalsExecutionMode(btSettings)) {
|
if (isScanSignalsExecutionMode(btSettings)) {
|
||||||
const signals = await fetchLiveConditionScanSignals(
|
const signals = await fetchLiveConditionScanSignals(
|
||||||
opts.market,
|
opts.market,
|
||||||
@@ -61,11 +63,12 @@ export async function fetchStrategyEvaluationSignals(opts: {
|
|||||||
opts.timeframe,
|
opts.timeframe,
|
||||||
indicatorParams,
|
indicatorParams,
|
||||||
opts.evaluationBarCount,
|
opts.evaluationBarCount,
|
||||||
|
positionMode,
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
signals,
|
signals,
|
||||||
initialCapital,
|
initialCapital,
|
||||||
settings: { ...btSettings, positionMode: 'SIGNAL_ONLY', tradeExecutionMode },
|
settings: { ...btSettings, positionMode, tradeExecutionMode },
|
||||||
commissionRate,
|
commissionRate,
|
||||||
tradeExecutionMode,
|
tradeExecutionMode,
|
||||||
};
|
};
|
||||||
@@ -73,7 +76,7 @@ export async function fetchStrategyEvaluationSignals(opts: {
|
|||||||
|
|
||||||
const settings: BacktestSettingsDto = {
|
const settings: BacktestSettingsDto = {
|
||||||
...btSettings,
|
...btSettings,
|
||||||
positionMode: 'SIGNAL_ONLY',
|
positionMode,
|
||||||
tradeExecutionMode: 'BACKTEST_ENGINE',
|
tradeExecutionMode: 'BACKTEST_ENGINE',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user