132 lines
5.9 KiB
Markdown
132 lines
5.9 KiB
Markdown
🎯 구현 목표 (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);
|
|
}
|
|
}
|
|
}); |