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

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
@@ -76,6 +76,7 @@ public class LiveConditionController {
body.getBars(),
body.getTimeframe(),
body.getIndicatorParams(),
body.getEvaluationBarCount()));
body.getEvaluationBarCount(),
body.getPositionMode()));
}
}
@@ -18,4 +18,6 @@ public class LiveConditionScanSignalsRequest {
private String timeframe;
/** 평가·표시 구간 봉 수 — null 이면 전체 bars */
private Integer evaluationBarCount;
/** "LONG_ONLY" | "SIGNAL_ONLY" — 백테스트 설정 positionMode 와 동일 */
private String positionMode;
}
@@ -15,6 +15,7 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.ta4j.core.BarSeries;
import org.ta4j.core.BaseTradingRecord;
import org.ta4j.core.Rule;
import org.springframework.transaction.annotation.Transactional;
@@ -759,7 +760,8 @@ public class LiveConditionStatusService {
List<OhlcvBar> chartBars,
String chartTimeframe,
Map<String, Map<String, Object>> indicatorParamsOverride,
Integer evaluationBarCount) {
Integer evaluationBarCount,
String positionMode) {
Optional<GcStrategy> opt = strategyRepo.findById(strategyId);
if (opt.isEmpty() || chartBars == null || chartBars.isEmpty()
|| chartTimeframe == null || chartTimeframe.isBlank()) {
@@ -808,21 +810,53 @@ public class LiveConditionStatusService {
List<BacktestResponse.Signal> signals = new ArrayList<>();
int buyHits = 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++) {
long barTimeSec = barOpenEpochSec(primarySeries, i);
double close = primarySeries.getBar(i).getClosePrice().doubleValue();
if (entryRule.isSatisfied(i, null)) {
buyHits++;
signals.add(BacktestResponse.Signal.builder()
.time(barTimeSec)
.type("BUY")
.price(close)
.barIndex(i)
.quantity(0)
.build());
if (signalOnly) {
if (entryRule.isSatisfied(i, null)) {
buyHits++;
signals.add(BacktestResponse.Signal.builder()
.time(barTimeSec)
.type("BUY")
.price(close)
.barIndex(i)
.quantity(0)
.build());
}
if (exitRule.isSatisfied(i, null)) {
sellHits++;
signals.add(BacktestResponse.Signal.builder()
.time(barTimeSec)
.type("SELL")
.price(close)
.barIndex(i)
.quantity(0)
.build());
}
continue;
}
if (exitRule.isSatisfied(i, null)) {
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)
@@ -831,16 +865,19 @@ public class LiveConditionStatusService {
.barIndex(i)
.quantity(0)
.build());
record.exit(i, primarySeries.numFactory().numOf(close),
primarySeries.numFactory().numOf(1));
inPosition = false;
}
}
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,
primarySeries.getEndIndex(), summarizeBuyCondition(buyDsl),
primarySeries.getEndIndex(), posMode, summarizeBuyCondition(buyDsl),
probeEntryRule(entryRule, buyDsl, ruleCtx, startIdx));
} else {
log.debug("[LiveCondition:scan] market={} strategy={} tf={} buy={} sell={}",
market, strategyId, primaryTf, buyHits, sellHits);
log.debug("[LiveCondition:scan] market={} strategy={} tf={} mode={} buy={} sell={}",
market, strategyId, primaryTf, posMode, buyHits, sellHits);
}
return signals;
} catch (Exception e) {
@@ -3,11 +3,14 @@ package com.goldenchart.service;
import com.goldenchart.entity.GcLiveStrategySettings;
import com.goldenchart.entity.GcStrategy;
import com.goldenchart.entity.GcTradeSignal;
import com.goldenchart.dto.BacktestSettingsDto;
import com.goldenchart.entity.GcAppSettings;
import com.goldenchart.repository.GcLiveStrategySettingsRepository;
import com.goldenchart.repository.GcPaperAccountRepository;
import com.goldenchart.repository.GcPaperPositionRepository;
import com.goldenchart.repository.GcStrategyRepository;
import com.goldenchart.repository.GcTradeSignalRepository;
import com.goldenchart.trading.TradingAccess;
import com.goldenchart.storage.Ta4jStorage;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -71,6 +74,8 @@ public class LiveStrategyEvaluator {
private final StrategyConditionTimeframeService conditionTimeframes;
private final StrategyTriggerBranchEvaluator triggerBranchEvaluator;
private final StrategyBranchStateCache branchStateCache;
private final AppSettingsService appSettingsService;
private final BacktestSettingsService backtestSettingsService;
/**
* 트리거 분봉별 Rule 캐시: "market:candleType:strategyId" → (entryRule, exitRule)
@@ -158,12 +163,13 @@ public class LiveStrategyEvaluator {
if (!ta4jStorage.exists(market, candleType)) return "NONE";
if (!conditionTimeframes.usesTimeframe(s.getStrategyId(), candleType)) return "NONE";
String posMode = resolvePositionMode(s);
String result = evaluate(market, candleType, s.getStrategyId(),
s.getPositionMode(), maturedIndex,
posMode, maturedIndex,
s.getDeviceId(), s.getUserId(), true /* useConfirmedOnly */);
if (!"NONE".equals(result)) {
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;
}
@@ -213,13 +219,14 @@ public class LiveStrategyEvaluator {
Integer lastSignaledIdx = realtimeSignaledIdx.get(dedupeKey);
if (lastSignaledIdx != null && lastSignaledIdx == currentIndex) return "NONE";
String posMode = resolvePositionMode(s);
// 캐시 완전 우회: 매번 신규 Rule 빌드 (CachedIndicator stale 방지 + 동시성 보장)
String result = evaluateWithFreshRules(market, candleType, s.getStrategyId(),
s.getPositionMode(), currentIndex,
posMode, currentIndex,
s.getDeviceId(), s.getUserId(), false /* useConfirmedOnly */);
if (!"NONE".equals(result)) {
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);
}
return result;
@@ -257,13 +264,14 @@ public class LiveStrategyEvaluator {
if (!ta4jStorage.exists(market, candleType)) return "NONE";
if (!conditionTimeframes.usesTimeframe(s.getStrategyId(), candleType)) return "NONE";
String posMode = resolvePositionMode(s);
// 확정봉 기준 평가: 캐시 우회 + useConfirmedOnly=true
String result = evaluateWithFreshRules(market, candleType, s.getStrategyId(),
s.getPositionMode(), maturedIndex,
posMode, maturedIndex,
s.getDeviceId(), s.getUserId(), true /* useConfirmedOnly */);
if (!"NONE".equals(result)) {
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);
}
return result;
@@ -371,6 +379,31 @@ public class LiveStrategyEvaluator {
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,
String deviceId, Long userId, boolean useConfirmedOnly) {
GcStrategy strategy = resolveStrategy(strategyId);