99 lines
4.3 KiB
Java
99 lines
4.3 KiB
Java
package com.goldenchart.service;
|
|
|
|
import lombok.extern.slf4j.Slf4j;
|
|
import org.springframework.stereotype.Component;
|
|
import org.ta4j.core.Rule;
|
|
import org.ta4j.core.Strategy;
|
|
import org.ta4j.core.TradingRecord;
|
|
|
|
/**
|
|
* 포지션 종속성 선택에 따른 매매 시그널 판정 코어 컴포넌트.
|
|
*
|
|
* <h3>모드 설명</h3>
|
|
* <ul>
|
|
* <li><b>LONG_ONLY</b> — Ta4j 표준 엄격 모드.
|
|
* {@code strategy.shouldEnter/shouldExit} 를 사용하므로,
|
|
* TradingRecord 에 열린 포지션이 없으면 매도 시그널을 내보내지 않는다.</li>
|
|
* <li><b>SIGNAL_ONLY</b> — 포지션 락 우회 모드.
|
|
* 내부 Rule 인스턴스를 직접 호출({@code rule.isSatisfied(index)})하므로,
|
|
* 이전 매수 기록 여부와 무관하게 지표 수식만 충족되면 시그널을 확정한다.</li>
|
|
* </ul>
|
|
*/
|
|
@Component
|
|
@Slf4j
|
|
public class StrategySignalDeterminer {
|
|
|
|
/**
|
|
* positionMode 에 따라 포지션 제약 유무를 분기하여 시그널을 결정한다.
|
|
*
|
|
* @param strategy Ta4j Strategy (entryRule / exitRule 포함)
|
|
* @param tradingRecord 현재 TradingRecord 상태 (LONG_ONLY 모드에서 참조)
|
|
* @param index BarSeries 인덱스
|
|
* @param positionMode "LONG_ONLY" | "SIGNAL_ONLY" (null → LONG_ONLY 기본 적용)
|
|
* @return "BUY" | "SELL" | "NONE"
|
|
*/
|
|
public String determineSignal(Strategy strategy, TradingRecord tradingRecord,
|
|
int index, String positionMode) {
|
|
try {
|
|
if ("SIGNAL_ONLY".equals(positionMode)) {
|
|
return determineSignalOnly(strategy, index);
|
|
}
|
|
// LONG_ONLY (기본)
|
|
return determineLongOnly(strategy, tradingRecord, index);
|
|
} catch (Exception e) {
|
|
log.warn("[SignalDeterminer] 시그널 판정 오류 idx={} mode={}: {}", index, positionMode, e.getMessage());
|
|
return "NONE";
|
|
}
|
|
}
|
|
|
|
// ── 보조 오버로드: Rule[] 직접 전달 (LiveStrategyEvaluator 호환) ─────────────
|
|
|
|
/**
|
|
* Rule 배열을 직접 받아 positionMode 에 따라 시그널 판정.
|
|
* entryRule = rules[0], exitRule = rules[1]
|
|
*/
|
|
public String determineSignalFromRules(Rule entryRule, Rule exitRule,
|
|
TradingRecord tradingRecord,
|
|
int index, String positionMode) {
|
|
try {
|
|
if ("SIGNAL_ONLY".equals(positionMode)) {
|
|
boolean enterOk = entryRule != null && isRisingEdge(entryRule, index);
|
|
boolean exitOk = exitRule != null && isRisingEdge(exitRule, index);
|
|
if (enterOk) return "BUY";
|
|
if (exitOk) return "SELL";
|
|
return "NONE";
|
|
}
|
|
// LONG_ONLY — tradingRecord 를 이용한 엄격 모드
|
|
boolean enterOk = entryRule != null && entryRule.isSatisfied(index, tradingRecord);
|
|
boolean exitOk = exitRule != null && exitRule.isSatisfied(index, tradingRecord);
|
|
if (enterOk) return "BUY";
|
|
if (exitOk) return "SELL";
|
|
return "NONE";
|
|
} catch (Exception e) {
|
|
log.warn("[SignalDeterminer] Rule 판정 오류 idx={} mode={}: {}", index, positionMode, e.getMessage());
|
|
return "NONE";
|
|
}
|
|
}
|
|
|
|
// ── Private ───────────────────────────────────────────────────────────────
|
|
|
|
private String determineLongOnly(Strategy strategy, TradingRecord record, int index) {
|
|
if (strategy.shouldEnter(index, record)) return "BUY";
|
|
if (strategy.shouldExit(index, record)) return "SELL";
|
|
return "NONE";
|
|
}
|
|
|
|
private String determineSignalOnly(Strategy strategy, int index) {
|
|
if (isRisingEdge(strategy.getEntryRule(), index)) return "BUY";
|
|
if (isRisingEdge(strategy.getExitRule(), index)) return "SELL";
|
|
return "NONE";
|
|
}
|
|
|
|
/** GTE 등 유지형 조건 — 충족 시작 봉만 true (연속 시그널 방지) */
|
|
private boolean isRisingEdge(Rule rule, int index) {
|
|
if (rule == null || !rule.isSatisfied(index)) return false;
|
|
if (index <= 0) return true;
|
|
return !rule.isSatisfied(index - 1);
|
|
}
|
|
}
|