본문 바로가기
And - 실시간 조건 감지·자동매매 시스템

[신투 프디아] 맞춤 조건에 맞는 기업 찾기!

by SoU330 2025. 10. 17.

 

 

 

이 글은 알파코에서 진행되는 [신한투자증권] 프로디지털아카데미 과정 중, 김송아 강사님과 함께하는 '파이널 프로젝트'를 기반으로 작성되었습니다

 

 

 

조건 탐지 로직은 이번 프로젝트의 핵심 기능 중 하나이다.

주식 데이터가 실시간으로 들어오면, 각 사용자의 알림 조건이 충족되는지 평가하고 필요할 때 즉시 알림을 보내야 한다.

그래서 이걸 완전 자동화하려고 스케줄러 기반의 조건 탐지 시스템을 구현했다.

 

스케줄러 흐름

현재는 @Scheduled(cron = "0 * * * * *")으로 매분 실행되며 아래 과정을 순차적으로 수행한다.

 

  1. 조건형 알림 전체 조회
    현재 감시 중인 조건형 알림만 가져온다.
  2. 해당 알림이 감시 중인 종목 리스트 조회
    각 알림이 어떤 종목을 모니터링 중인지 확인한다.
  3. 종목별 조건 평가
    Redis에서 종목 데이터를 가져와 evaluator를 통해 조건 충족 여부를 판단한다.
  4. 상태 변환 감지 및 업데이트
    이전 상태와 현재 결과를 비교하고 변화가 있을 때만 업데이트 한다. 

 

 

AlertEvaluationService

이 서비스는 한 알림에 연결된 모든 조건들을 불러와서 카테고리별로 그룹화한 되 AND / OR 로직으로 평가한다.

  • 같은 카테고리 내 조건들은 OR 관계 
    -> 하나라도 충족하면 해당 카테고리는 충족
  • 카테고리끼리는 AND 관계
    -> 모든 카테고리가 충족해야 알림 전체가 트리거됨
거래량 조건 중 하나라도 만족하고, 가격 조건도 하나라도 만족하면 -> 알림 발동

이런 식이다

 

boolean overall = true;

for (Map.Entry<String, List<AlertConditionManager>> entry : grouped.entrySet()) {
    String category = entry.getKey();
    List<AlertConditionManager> list = entry.getValue();

    boolean categoryResult = false;

    for (AlertConditionManager manager : list) {
        Map<String, Double> metrics = evaluatorManager.loadMetrics(manager);
        boolean condResult = evaluatorManager.evaluate(manager, metrics);
        categoryResult |= condResult;
    }

    log.info("📁 [{}] 카테고리 결과: {}", category, categoryResult ? "충족" : "미충족");
    overall &= categoryResult;
}

이 구조 덕분에 조건이 몇 개가 되든 새로운 카테고리가 추가되든 코어 로직은 건드리지 않아도 된다.

 

 

종목 단위 평가

조건형 알림에서는 특정 종목별로 평가가 필요하다

 

이때는 evaluateAlertForCondition() 메서드를 사용한다.
Alert 엔티티와 stockCode를 함께 넘겨주면 해당 종목의 Redis 데이터를 읽고 조건을 판별한다.

 

Map<String, Double> metrics = evaluatorManager.loadMetricsForStock(manager, stockCode);
boolean condResult = evaluatorManager.evaluate(manager, metrics);

 

이 구조는 42개 evaluator와 Redis key(daily:005930, minute:000660) 체계를 그대로 활용한다.

 

 

느낀점

처음엔 그냥 단일 조건 하나만 평가하는 메서드라고 생각했다.

근데 카테고리별 묶음, 다중 종목 탐지, 조건형/종목형 분리 요구를 생각하면서 하나의 메서드가 감당할 수 없었다.

그래서 evaluateAlert()와 evaluateAlertForCondition()을 분리했다.
결국 이게 구조적으로는 훨씬 깔끔했다.