매매시그널 실행조건 화면 공용 적용
This commit is contained in:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user