🎯 구현 목표 (Context & Goal)
현재 구축된 Backend (Spring Boot + Ta4j)와 Frontend (Lightweight Charts) 시스템에 "실시간 매매전략 실행 설정 기능"을 도입합니다. 유저가 화면에서 설정한 옵션(실행 타이밍, 모니터링 여부)에 따라 백엔드 연산 주기와 프론트엔드 차트 시그널 마커(setMarkers)가 유기적으로 연동되도록 아키텍처를 확장해 주세요.

🛠️ 1. 요구사항 명세 및 정책 (Requirements)
① 설정 화면 UI 구성 (Frontend)
실시간 전략 체크 토글: ON / OFF 스위치

매매전략 실행 방식 라디오 버튼: (실시간 전략 체크가 ON 일 때만 활성화)

A. 봉 마감 직후 체크 (At Candle Close)

B. 실시간 틱(장중) 체크 (Intraday Tick)

주의: 이 설정값들은 전역 상태(Redux/Zustand 등)로 관리되며, 변경 즉시 백엔드로 REST API를 통해 반영되어 런타임에 동적으로 감시 스레드가 제어되어야 합니다.

② 백엔드 실행 코어 제어 정책 (Backend Threading)
설정값 OFF 시: 해당 종목 및 유저에 대한 기술적 분석 지표 및 전략 체크 루프를 완전히 중단하여 서버 자원을 방어합니다.

설정값 ON + 방식 A (봉 마감 직후):

업비트 웹소켓에서 들어온 틱 데이터로 분봉 조립 중, '분이 바뀌어 새로운 Bar가 추가되는 시점(addBar)'에만 해당 종목의 Strategy.isSatisfied(lastMaturedBarIndex)를 1회 판정합니다.

설정값 ON + 방식 B (실시간 틱 체크):

백엔드의 3초 주기 라운드 로빈 스케줄러가 돌아갈 때, 아직 마감되지 않은 현재 진행 중인 캔들 인덱스(series.getEndIndex())를 기준으로 전략 만족 여부를 강제 판정합니다.

💾 2. 데이터 구조 및 인터페이스 설계 (Data Struct)
① [PUT] /api/strategy/settings (전략 설정 변경 API)
JSON
{
  "market": "KRW-BTC",
  "isLiveCheck": true,          // 실시간 전략 체크 ON/OFF
  "executionType": "CANDLE_CLOSE" // "CANDLE_CLOSE" (방식A) 또는 "REALTIME_TICK" (방식B)
}
② [STOMP WebSocket] 확장된 실시간 데이터 프로토콜
백엔드에서 프론트엔드로 실시간 캔들을 발행할 때, 설정 상태와 매칭된 시그널 유무를 명확히 DTO 포맷에 주입합니다.

Topic: /sub/charts/{market}/{type}

Payload (JSON):

JSON
{
  "t": 1779229260,
  "o": 92080000.0,
  "h": 92120000.0,
  "l": 92010000.0,
  "c": 92050000.0,
  "v": 1.845,
  "rsi": 38.40,
  "signal": "BUY" // "BUY", "SELL", "NONE" 중 하나 (전략 체크가 OFF이거나 조건 미달 시 무조건 "NONE")
}
💻 3. AI 구현 가이드라인 코드 (Reference Code for AI)
[Backend] 가상 코드를 참고하여 동적 분기 서비스 레이어를 작성해 주세요.
Java
@Service
@RequiredArgsConstructor
public class LiveStrategyEvaluator {
    
    private final Ta4jStorage ta4jStorage;
    private final UserStrategySettingsRepository settingsRepository;

    // 방식 A: 봉 마감 엔진에서 트리거됨
    public String evaluateCandleClose(String market, String candleType, int maturedIndex) {
        UserSetting setting = settingsRepository.findByMarket(market);
        if (!setting.isLiveCheck() || !setting.getExecutionType().equals("CANDLE_CLOSE")) {
            return "NONE";
        }
        
        Strategy strategy = ta4jStorage.getStrategy(market);
        BarSeries series = ta4jStorage.getSeries(market, candleType);
        
        if (strategy.getEntryRule().isSatisfied(maturedIndex)) return "BUY";
        if (strategy.getExitRule().isSatisfied(maturedIndex)) return "SELL";
        
        return "NONE";
    }

    // 방식 B: 3초 스케줄러 엔진에서 트리거됨
    public String evaluateRealtimeTick(String market, String candleType) {
        UserSetting setting = settingsRepository.findByMarket(market);
        if (!setting.isLiveCheck() || !setting.getExecutionType().equals("REALTIME_TICK")) {
            return "NONE";
        }
        
        BarSeries series = ta4jStorage.getSeries(market, candleType);
        Strategy strategy = ta4jStorage.getStrategy(market);
        int currentIndex = series.getEndIndex(); // 마감되지 않은 현재 진행형 캔들
        
        if (strategy.getEntryRule().isSatisfied(currentIndex)) return "BUY";
        if (strategy.getExitRule().isSatisfied(currentIndex)) return "SELL";
        
        return "NONE";
    }
}
[Frontend] STOMP 데이터를 받아 Lightweight Charts에 마커를 누적 처리하는 훅(Hook) 또는 컴포넌트 로직을 작성해 주세요.
JavaScript
// 프론트엔드 캔들 및 마커 실시간 업데이트 제어부
let activeMarkers = []; // 차트에 표시된 마커 상태 캐시 배열

stompClient.subscribe(`/sub/charts/${market}/${candleType}`, (message) => {
    const data = JSON.parse(message.body);
    
    // 1. 기본 캔들 렌더링 무조건 실행
    candleSeries.update({
        time: data.t,
        open: data.o,
        high: data.h,
        low: data.l,
        close: data.c
    });

    // 2. 백엔드에서 주입된 signal 판별 (NONE이 아닐 때만 차트 오버레이 마커 추가)
    if (data.signal !== 'NONE') {
        // 중복 타임스탬프 마커 삽입 방지 (방식 B 장중 틱 동적 체크 대응)
        const isDuplicate = activeMarkers.some(m => m.time === data.t && m.signalType === data.signal);
        
        if (!isDuplicate) {
            const newMarker = {
                time: data.t,
                position: data.signal === 'BUY' ? 'belowBar' : 'aboveBar',
                color: data.signal === 'BUY' ? '#26a69a' : '#ef5350',
                shape: data.signal === 'BUY' ? 'arrowUp' : 'arrowDown',
                text: data.signal === 'BUY' ? '실시간 매수' : '실시간 매도',
                signalType: data.signal // 커스텀 식별자
            };
            
            activeMarkers.push(newMarker);
            candleSeries.setMarkers(activeMarkers);
        }
    }
});