전략평가 개선
This commit is contained in:
@@ -4,6 +4,8 @@ import com.goldenchart.entity.GcLiveStrategySettings;
|
||||
import com.goldenchart.entity.GcStrategy;
|
||||
import com.goldenchart.entity.GcTradeSignal;
|
||||
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.storage.Ta4jStorage;
|
||||
@@ -15,6 +17,7 @@ import org.ta4j.core.Rule;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
@@ -60,6 +63,8 @@ public class LiveStrategyEvaluator {
|
||||
private final GcLiveStrategySettingsRepository settingsRepo;
|
||||
private final GcStrategyRepository strategyRepo;
|
||||
private final GcTradeSignalRepository tradeSignalRepo;
|
||||
private final GcPaperAccountRepository paperAccountRepo;
|
||||
private final GcPaperPositionRepository paperPositionRepo;
|
||||
private final Ta4jStorage ta4jStorage;
|
||||
private final IndicatorSettingsService indicatorSettingsService;
|
||||
private final StrategySignalDeterminer determiner;
|
||||
@@ -308,7 +313,7 @@ public class LiveStrategyEvaluator {
|
||||
triggerRulesCache.put(cacheKey, rules);
|
||||
}
|
||||
|
||||
return applySignalLogic(rules, market, candleType, strategyId, positionMode, index);
|
||||
return applySignalLogic(rules, market, candleType, strategyId, positionMode, index, deviceId, userId);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -321,12 +326,13 @@ public class LiveStrategyEvaluator {
|
||||
String deviceId, Long userId, boolean useConfirmedOnly) {
|
||||
TriggerRules rules = buildTriggerRules(market, candleType, strategyId, deviceId, userId, useConfirmedOnly);
|
||||
if (rules == null) return "NONE";
|
||||
return applySignalLogic(rules, market, candleType, strategyId, positionMode, index);
|
||||
return applySignalLogic(rules, market, candleType, strategyId, positionMode, index, deviceId, userId);
|
||||
}
|
||||
|
||||
/** Rule 판정 + TradingRecord 갱신 공통 로직 */
|
||||
private String applySignalLogic(TriggerRules rules, String market, String candleType,
|
||||
long strategyId, String positionMode, int index) {
|
||||
long strategyId, String positionMode, int index,
|
||||
String deviceId, Long userId) {
|
||||
String cacheKey = market + ":" + candleType + ":" + strategyId;
|
||||
try {
|
||||
String mode = positionMode != null ? positionMode : "LONG_ONLY";
|
||||
@@ -338,7 +344,7 @@ public class LiveStrategyEvaluator {
|
||||
|
||||
BaseTradingRecord record = tradingRecordCache.computeIfAbsent(
|
||||
cacheKey, k -> new BaseTradingRecord());
|
||||
boolean isOpen = resolvePositionOpen(cacheKey, market, strategyId);
|
||||
boolean isOpen = resolvePositionOpen(cacheKey, market, strategyId, deviceId, userId);
|
||||
String signal = determiner.determineSignalFromRules(
|
||||
rules.entryRule(), rules.exitRule(), record, index, "LONG_ONLY");
|
||||
|
||||
@@ -448,18 +454,73 @@ public class LiveStrategyEvaluator {
|
||||
* <li>최신 시그널 = SELL 또는 없음 → 포지션 없음</li>
|
||||
* </ul>
|
||||
*/
|
||||
private boolean resolvePositionOpen(String cacheKey, String market, long strategyId) {
|
||||
/**
|
||||
* LONG_ONLY 포지션 오픈 여부 복원 — gc_trade_signal + gc_paper_position 교차 검증.
|
||||
*
|
||||
* <p>[항목3 보완] 서버 재시작 시 gc_trade_signal DB 의 최신 레코드만 보면 "마지막 신호=BUY"라서
|
||||
* 포지션이 열려 있다고 판단할 수 있지만, 서버 다운 중 사용자가 수동으로 물량을 전량 매도했다면
|
||||
* 실제 가상 잔고(gc_paper_position.quantity)는 0 이다. 이 경우 gc_trade_signal 에 SELL 기록이
|
||||
* 없어도 포지션이 없다고 바르게 판정해야 한다.
|
||||
*
|
||||
* <p>로직 순서:
|
||||
* <ol>
|
||||
* <li>캐시 히트 → 즉시 반환</li>
|
||||
* <li>gc_trade_signal 최신 레코드 조회 → 마지막 신호가 BUY 이면 "열림" 후보</li>
|
||||
* <li>"열림" 후보인 경우에만 gc_paper_position.quantity 교차 검증:
|
||||
* quantity == 0 → 수동 청산된 것으로 판단, isOpen = false 로 보정</li>
|
||||
* </ol>
|
||||
*/
|
||||
private boolean resolvePositionOpen(String cacheKey, String market, long strategyId,
|
||||
String deviceId, Long userId) {
|
||||
Boolean cached = positionOpenCache.get(cacheKey);
|
||||
if (cached != null) return cached;
|
||||
|
||||
// DB 복원: 해당 마켓×전략의 최신 시그널
|
||||
// Step 1: gc_trade_signal 최신 레코드로 1차 판정
|
||||
Optional<GcTradeSignal> latest =
|
||||
tradeSignalRepo.findFirstByMarketAndStrategyIdOrderByCreatedAtDesc(market, strategyId);
|
||||
boolean isOpen = latest.map(s -> "BUY".equals(s.getSignalType())).orElse(false);
|
||||
|
||||
// Step 2: "열림" 후보인 경우만 gc_paper_position 교차 검증
|
||||
if (isOpen) {
|
||||
try {
|
||||
Long accountId = resolveAccountId(userId, deviceId);
|
||||
if (accountId != null) {
|
||||
// 심볼 형식: gc_paper_position 은 market 값 그대로 저장 (e.g. "KRW-BTC")
|
||||
boolean hasPosition = paperPositionRepo
|
||||
.findByAccountIdAndSymbol(accountId, market)
|
||||
.map(p -> p.getQuantity() != null
|
||||
&& p.getQuantity().compareTo(BigDecimal.ZERO) > 0)
|
||||
.orElse(false);
|
||||
if (!hasPosition) {
|
||||
isOpen = false;
|
||||
log.info("[Evaluator] 포지션 교차검증: gc_trade_signal=BUY 지만 " +
|
||||
"gc_paper_position.quantity=0 → 수동청산 감지, key={} → CLOSED", cacheKey);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("[Evaluator] 포지션 교차검증 실패 key={}: {} — signal-only 판정 유지", cacheKey, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
positionOpenCache.put(cacheKey, isOpen);
|
||||
if (isOpen) {
|
||||
log.info("[Evaluator] 포지션 상태 복원 (DB) key={} → OPEN (BUY 포지션 유지)", cacheKey);
|
||||
log.info("[Evaluator] 포지션 상태 복원 (DB+position 교차검증) key={} → OPEN", cacheKey);
|
||||
}
|
||||
return isOpen;
|
||||
}
|
||||
|
||||
/** userId 또는 deviceId 로 가상계좌 ID 조회 */
|
||||
private Long resolveAccountId(Long userId, String deviceId) {
|
||||
if (userId != null) {
|
||||
return paperAccountRepo.findByUserId(userId)
|
||||
.map(a -> a.getId())
|
||||
.orElse(null);
|
||||
}
|
||||
if (deviceId != null && !deviceId.isBlank()) {
|
||||
return paperAccountRepo.findByDeviceId(deviceId)
|
||||
.map(a -> a.getId())
|
||||
.orElse(null);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user