매수, 매도 색상 수정
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
package com.goldenchart.service;
|
||||
|
||||
import com.goldenchart.dto.CandleBarDto;
|
||||
import com.goldenchart.entity.GcLiveStrategySettings;
|
||||
import com.goldenchart.repository.GcLiveStrategySettingsRepository;
|
||||
import com.goldenchart.trading.pipeline.OrderExecutionQueue;
|
||||
import com.goldenchart.websocket.TradingWebSocketBroker;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.ta4j.core.Bar;
|
||||
import org.ta4j.core.BarSeries;
|
||||
|
||||
import com.goldenchart.storage.Ta4jStorage;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* 봉 마감 시점 전략 평가 — BarBuilder·갭 백필 등 단일 진입점.
|
||||
*
|
||||
* <p>활성 live 설정 중 전략 DSL에 포함된 분봉({@link StrategyConditionTimeframeService})
|
||||
* 에서만 평가하며, 동일 봉 마감(endTime)에 대해 중복 평가하지 않는다.
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class BarCloseStrategyEvaluationService {
|
||||
|
||||
private final LiveStrategyEvaluator liveStrategyEvaluator;
|
||||
private final TradeSignalService tradeSignalService;
|
||||
private final OrderExecutionQueue orderExecutionQueue;
|
||||
private final GcLiveStrategySettingsRepository liveSettingsRepo;
|
||||
private final StrategyConditionTimeframeService conditionTimeframes;
|
||||
private final TradingWebSocketBroker broker;
|
||||
private final Ta4jStorage ta4jStorage;
|
||||
|
||||
/** market:candleType:barEndEpoch — 동일 마감봉 중복 평가 방지 */
|
||||
private final ConcurrentHashMap<String, Long> evaluatedBarCloseKeys = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 확정된 봉(마감)에 대해 전략 평가 + BUY/SELL 시 STOMP·DB·주문 큐.
|
||||
*/
|
||||
public void onMaturedBarClose(String market, String candleType, int maturedIndex, Bar signalBar) {
|
||||
String ct = LiveStrategyTimeframeService.normalize(candleType);
|
||||
long barEndEpoch = signalBar.getEndTime().getEpochSecond();
|
||||
String dedupKey = market + ":" + ct + ":" + barEndEpoch;
|
||||
if (evaluatedBarCloseKeys.putIfAbsent(dedupKey, barEndEpoch) != null) {
|
||||
return;
|
||||
}
|
||||
trimEvaluatedKeys();
|
||||
|
||||
List<GcLiveStrategySettings> activeSettings = liveSettingsRepo.findActiveByMarket(market).stream()
|
||||
.filter(s -> Boolean.TRUE.equals(s.getIsLiveCheck()) && s.getStrategyId() != null)
|
||||
.toList();
|
||||
if (activeSettings.isEmpty()) return;
|
||||
|
||||
long candleTimeEpoch = barEndEpoch - signalBar.getTimePeriod().getSeconds();
|
||||
|
||||
for (GcLiveStrategySettings s : activeSettings) {
|
||||
if (!conditionTimeframes.usesTimeframe(s.getStrategyId(), ct)) continue;
|
||||
|
||||
String closeSignal = "NONE";
|
||||
String signalExecType = null;
|
||||
|
||||
if ("CANDLE_CLOSE".equals(s.getExecutionType())) {
|
||||
closeSignal = liveStrategyEvaluator.evaluateSettingOnCandleClose(
|
||||
s, market, ct, maturedIndex);
|
||||
signalExecType = "CANDLE_CLOSE";
|
||||
} else if ("REALTIME_TICK".equals(s.getExecutionType())) {
|
||||
closeSignal = liveStrategyEvaluator.evaluateSettingRealtimeAtClose(
|
||||
s, market, ct, maturedIndex);
|
||||
signalExecType = "REALTIME_TICK";
|
||||
}
|
||||
|
||||
if (!"BUY".equals(closeSignal) && !"SELL".equals(closeSignal)) continue;
|
||||
|
||||
publishStrategySignal(market, ct, signalBar, closeSignal, s, signalExecType);
|
||||
try {
|
||||
tradeSignalService.save(
|
||||
s.getDeviceId(), s.getUserId(),
|
||||
market, s.getStrategyId(), null,
|
||||
closeSignal, signalBar.getClosePrice().doubleValue(),
|
||||
candleTimeEpoch, ct, signalExecType);
|
||||
if (s.getDeviceId() != null) {
|
||||
orderExecutionQueue.submitSignal(
|
||||
s.getDeviceId(), s.getUserId(), market,
|
||||
s.getStrategyId(), closeSignal,
|
||||
signalBar.getClosePrice().doubleValue());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("[BarCloseEval] 시그널 처리 실패 ({}/{} strategyId={}): {}",
|
||||
market, ct, s.getStrategyId(), e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* REST 갭 백필 등으로 시리즈가 갱신된 뒤, 병합된 확정봉 구간을 순회하며 평가한다.
|
||||
*
|
||||
* @param maxMaturedBars 병합된 봉 수(상한) — 중간 마감봉 누락 방지
|
||||
*/
|
||||
public void evaluateRecentMaturedBarsAfterBackfill(String market, String candleType, int maxMaturedBars) {
|
||||
if (maxMaturedBars <= 0 || !ta4jStorage.exists(market, candleType)) return;
|
||||
String ct = LiveStrategyTimeframeService.normalize(candleType);
|
||||
if (!shouldEvaluateOnClose(market, ct)) return;
|
||||
|
||||
BarSeries series = ta4jStorage.getOrCreate(market, candleType);
|
||||
if (series.isEmpty()) return;
|
||||
|
||||
int end = series.getEndIndex();
|
||||
int begin = series.getBeginIndex();
|
||||
int maturedEnd = Math.max(begin, end - 1);
|
||||
int maturedStart = Math.max(begin, maturedEnd - maxMaturedBars + 1);
|
||||
|
||||
for (int i = maturedStart; i <= maturedEnd; i++) {
|
||||
onMaturedBarClose(market, ct, i, series.getBar(i));
|
||||
}
|
||||
}
|
||||
|
||||
public void invalidateMarket(String market) {
|
||||
evaluatedBarCloseKeys.keySet().removeIf(k -> k.startsWith(market + ":"));
|
||||
}
|
||||
|
||||
/** 활성 실시간 전략 중 DSL에 해당 분봉이 포함된 항목이 있는지 */
|
||||
public boolean shouldEvaluateOnClose(String market, String candleType) {
|
||||
String ct = LiveStrategyTimeframeService.normalize(candleType);
|
||||
return liveSettingsRepo.findActiveByMarket(market).stream()
|
||||
.filter(s -> Boolean.TRUE.equals(s.getIsLiveCheck()) && s.getStrategyId() != null)
|
||||
.anyMatch(s -> conditionTimeframes.usesTimeframe(s.getStrategyId(), ct));
|
||||
}
|
||||
|
||||
private void publishStrategySignal(String market, String candleType, Bar bar, String signal,
|
||||
GcLiveStrategySettings setting, String executionType) {
|
||||
long barStartEpoch = bar.getEndTime().getEpochSecond() - bar.getTimePeriod().getSeconds();
|
||||
CandleBarDto dto = CandleBarDto.builder()
|
||||
.time(barStartEpoch)
|
||||
.open(bar.getOpenPrice().doubleValue())
|
||||
.high(bar.getHighPrice().doubleValue())
|
||||
.low(bar.getLowPrice().doubleValue())
|
||||
.close(bar.getClosePrice().doubleValue())
|
||||
.volume(bar.getVolume().doubleValue())
|
||||
.signal(signal)
|
||||
.candleType(candleType)
|
||||
.strategyId(setting.getStrategyId())
|
||||
.userId(setting.getUserId())
|
||||
.deviceId(setting.getDeviceId())
|
||||
.executionType(executionType)
|
||||
.build();
|
||||
broker.publish(market, candleType, dto);
|
||||
log.info("[BarCloseEval] bar-close signal={} market={} candleType={} strategyId={} userId={}",
|
||||
signal, market, candleType, setting.getStrategyId(), setting.getUserId());
|
||||
}
|
||||
|
||||
private void trimEvaluatedKeys() {
|
||||
if (evaluatedBarCloseKeys.size() > 50_000) {
|
||||
evaluatedBarCloseKeys.clear();
|
||||
log.info("[BarCloseEval] evaluatedBarCloseKeys cleared (size cap)");
|
||||
}
|
||||
}
|
||||
}
|
||||
+5
-6
@@ -67,17 +67,12 @@ public class StrategyConditionTimeframeService {
|
||||
GcStrategy strategy = opt.get();
|
||||
ensureDslRepaired(strategy);
|
||||
|
||||
Set<String> fromFlowLayout = collectFromFlowLayoutJson(strategy.getFlowLayoutJson());
|
||||
if (!fromFlowLayout.isEmpty()) {
|
||||
return sanitizeEvaluationTimeframes(fromFlowLayout, strategy);
|
||||
}
|
||||
|
||||
Set<String> buyOut = new LinkedHashSet<>();
|
||||
Set<String> sellOut = new LinkedHashSet<>();
|
||||
boolean buyScoped = collectStartScopeFromJson(strategy.getBuyConditionJson(), buyOut);
|
||||
boolean sellScoped = collectStartScopeFromJson(strategy.getSellConditionJson(), sellOut);
|
||||
|
||||
// 매수 START 분봉 우선 — 매도 쪽 레거시 1m TIMEFRAME 이 합집합에 섞이지 않도록
|
||||
// 평가 분봉은 buy/sell DSL(TIMEFRAME·START)이 단일 원천 — flow_layout 은 편집기 UI 보조
|
||||
if (buyScoped && !buyOut.isEmpty()) {
|
||||
return sanitizeEvaluationTimeframes(buyOut, strategy);
|
||||
}
|
||||
@@ -96,6 +91,10 @@ public class StrategyConditionTimeframeService {
|
||||
}
|
||||
|
||||
if (out.isEmpty()) {
|
||||
Set<String> fromFlowLayout = collectFromFlowLayoutJson(strategy.getFlowLayoutJson());
|
||||
if (!fromFlowLayout.isEmpty()) {
|
||||
return sanitizeEvaluationTimeframes(fromFlowLayout, strategy);
|
||||
}
|
||||
String fromName = StrategyDslTimeframeNormalizer.inferFromStrategyName(strategy.getName());
|
||||
return Set.of(fromName != null ? fromName : "1m");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user