추세검색 수정
This commit is contained in:
@@ -47,8 +47,8 @@ public class TrendSearchService {
|
||||
private static final int MIN_BARS_BASE = 65;
|
||||
private static final int HIGH_MATCH_THRESHOLD = 90;
|
||||
private static final int HISTORY_BARS = 365;
|
||||
/** 스캔 1회당 최대 캔들 (업비트 rate limit·MA120 기준) */
|
||||
private static final int SCAN_MAX_BARS = 120;
|
||||
/** 스캔 1회당 최대 캔들 (업비트 rate limit·EMA120 기준) */
|
||||
private static final int SCAN_MAX_BARS = 150;
|
||||
private static final long CANDLE_CACHE_TTL_MS = 10 * 60 * 1000L;
|
||||
|
||||
/** market|tf → 일봉 캐시 (rate limit·재스캔 대응) */
|
||||
@@ -70,6 +70,8 @@ public class TrendSearchService {
|
||||
Map<String, TickerSnap> tickers = fetchTickers(markets);
|
||||
int minMatchRate = Math.min(100, Math.max(0, r.getMinMatchRate()));
|
||||
int enabledFilters = countEnabledFilters(r);
|
||||
boolean useBullishScore = r.isBullishTrendEnabled();
|
||||
int minTrendScore = Math.min(100, Math.max(0, r.getMinTrendScore()));
|
||||
|
||||
List<ScoredMarket> scored = new ArrayList<>();
|
||||
for (int i = 0; i < markets.size(); i++) {
|
||||
@@ -84,13 +86,17 @@ public class TrendSearchService {
|
||||
|
||||
TickerSnap tick = tickers.getOrDefault(market, TickerSnap.empty(market));
|
||||
MetricValues m = computeMetrics(series, candles, tick, r);
|
||||
m.trendStrength = computeTrendStrength(m);
|
||||
m.matchScore = enabledFilters == 0
|
||||
? m.trendStrength
|
||||
: computeMatchScore(m, r);
|
||||
|
||||
// minMatchRate>0 일 때만 하한 필터 (기본 0 = 일치율 순 전체 랭킹)
|
||||
if (minMatchRate > 0 && m.matchScore < minMatchRate) continue;
|
||||
if (useBullishScore) {
|
||||
m.bullishTrendScore = computeBullishTrendScore(m, r);
|
||||
m.matchScore = m.bullishTrendScore;
|
||||
if (minTrendScore > 0 && m.bullishTrendScore < minTrendScore) continue;
|
||||
} else {
|
||||
m.trendStrength = computeTrendStrength(m);
|
||||
m.matchScore = enabledFilters == 0
|
||||
? m.trendStrength
|
||||
: computeMatchScore(m, r);
|
||||
if (minMatchRate > 0 && m.matchScore < minMatchRate) continue;
|
||||
}
|
||||
|
||||
scored.add(new ScoredMarket(market, tick, candles, m));
|
||||
} catch (Exception e) {
|
||||
@@ -99,15 +105,19 @@ public class TrendSearchService {
|
||||
}
|
||||
|
||||
if (scored.isEmpty()) {
|
||||
log.warn("[TrendSearch] scan empty tf={} markets={} enabledFilters={} (캔들·rate limit 확인)",
|
||||
tf, markets.size(), enabledFilters);
|
||||
log.warn("[TrendSearch] scan empty tf={} markets={} bullish={} (캔들·rate limit 확인)",
|
||||
tf, markets.size(), useBullishScore);
|
||||
return List.of();
|
||||
}
|
||||
|
||||
Comparator<ScoredMarket> byTrendStrength = useBullishScore
|
||||
? Comparator.comparingInt((ScoredMarket s) -> s.metrics.bullishTrendScore).reversed()
|
||||
: Comparator.comparingInt((ScoredMarket s) -> s.metrics.matchScore).reversed();
|
||||
|
||||
return scored.stream()
|
||||
.sorted(Comparator.comparingDouble((ScoredMarket s) -> s.metrics.matchScore).reversed()
|
||||
.thenComparingDouble(s -> s.metrics.trendStrength).reversed()
|
||||
.thenComparingDouble(s -> s.metrics.turnover5dAvg).reversed())
|
||||
.sorted(byTrendStrength
|
||||
.thenComparingDouble(s -> s.metrics.turnover5dAvg).reversed()
|
||||
.thenComparing(ScoredMarket::market))
|
||||
.limit(limit)
|
||||
.map(s -> toResult(s, r))
|
||||
.collect(Collectors.toList());
|
||||
@@ -127,9 +137,14 @@ public class TrendSearchService {
|
||||
TickerSnap tick = tickers.getOrDefault(market, TickerSnap.empty(market));
|
||||
BarSeries series = toSeries(candles, tf);
|
||||
MetricValues m = computeMetrics(series, candles, tick, r);
|
||||
m.trendStrength = computeTrendStrength(m);
|
||||
int enabled = countEnabledFilters(r);
|
||||
m.matchScore = enabled > 0 ? computeMatchScore(m, r) : m.trendStrength;
|
||||
if (r.isBullishTrendEnabled()) {
|
||||
m.bullishTrendScore = computeBullishTrendScore(m, r);
|
||||
m.matchScore = m.bullishTrendScore;
|
||||
} else {
|
||||
m.trendStrength = computeTrendStrength(m);
|
||||
int enabled = countEnabledFilters(r);
|
||||
m.matchScore = enabled > 0 ? computeMatchScore(m, r) : m.trendStrength;
|
||||
}
|
||||
return toResult(new ScoredMarket(market, tick, candles, m), r);
|
||||
}
|
||||
|
||||
@@ -203,6 +218,23 @@ public class TrendSearchService {
|
||||
double adxVal = end >= diLen + 5 ? adx.getValue(end).doubleValue() : 0;
|
||||
boolean dmiBullish = pdi > mdi && adxVal >= r.getAdxMin();
|
||||
|
||||
EMAIndicator ema20 = new EMAIndicator(close, 20);
|
||||
EMAIndicator ema60 = new EMAIndicator(close, 60);
|
||||
EMAIndicator ema120 = new EMAIndicator(close, 120);
|
||||
double e20 = safeEma(ema20, end);
|
||||
double e60 = safeEma(ema60, end);
|
||||
double e120 = safeEma(ema120, end);
|
||||
boolean bullishEmaAlignment = end >= 120 && validMa(e20) && validMa(e60) && validMa(e120)
|
||||
&& e20 > e60 && e60 > e120;
|
||||
int slopeLookback = Math.max(1, Math.min(r.getEmaSlopeLookback(), 20));
|
||||
double e20Past = safeEma(ema20, end - slopeLookback);
|
||||
boolean bullishEmaSlope = end >= 20 + slopeLookback && validMa(e20) && validMa(e20Past)
|
||||
&& e20 > e20Past;
|
||||
double adxTrendMin = Math.max(10, r.getAdxTrendMin());
|
||||
boolean bullishAdxTrend = end >= diLen + 5 && pdi > mdi && adxVal >= adxTrendMin;
|
||||
boolean bullishMacdMomentum = end >= 35 && macdVal > signalVal && macdVal > 0;
|
||||
boolean bullishPricePosition = end >= 20 && validMa(e20) && price > e20;
|
||||
|
||||
return new MetricValues(
|
||||
priceAboveMa, maAlignment, maConvergence, maSpreadPct,
|
||||
ma20SlopeUp, newHighBreakout, periodHigh, price,
|
||||
@@ -210,7 +242,9 @@ public class TrendSearchService {
|
||||
volPriorPct, volumeVsPrior200, turnover5dAvg, minTurnover5dAvg, volMa5Over20,
|
||||
macdVal, signalVal, macdGoldenCross,
|
||||
rsiVal, rsiBand,
|
||||
pdi, mdi, adxVal, dmiBullish
|
||||
pdi, mdi, adxVal, dmiBullish,
|
||||
e20, e60, e120, e20Past,
|
||||
bullishEmaAlignment, bullishEmaSlope, bullishAdxTrend, bullishMacdMomentum, bullishPricePosition
|
||||
);
|
||||
}
|
||||
|
||||
@@ -241,6 +275,10 @@ public class TrendSearchService {
|
||||
|
||||
private int requiredMinBars(TrendSearchRequest r) {
|
||||
int need = MIN_BARS_BASE;
|
||||
if (r.isBullishTrendEnabled()) {
|
||||
int lookback = Math.max(1, Math.min(r.getEmaSlopeLookback(), 20));
|
||||
need = Math.max(need, 120 + lookback + 5);
|
||||
}
|
||||
if (r.isMaAlignEnabled() && r.isMaAlignment()) need = Math.max(need, 120);
|
||||
if (r.isTrendEnabled() && r.isIchimokuAboveCloud()) need = Math.max(need, 80);
|
||||
if (r.isTrendEnabled() && r.isNewHighBreakout()) {
|
||||
@@ -322,6 +360,17 @@ public class TrendSearchService {
|
||||
return Math.round(matched * 100f / total);
|
||||
}
|
||||
|
||||
/** 상승추세 검색그룹 — 가중 점수 합산 (0~100) */
|
||||
private int computeBullishTrendScore(MetricValues m, TrendSearchRequest r) {
|
||||
int score = 0;
|
||||
if (m.bullishEmaAlignment) score += Math.max(0, r.getWeightMaAlignment());
|
||||
if (m.bullishEmaSlope) score += Math.max(0, r.getWeightMaSlope());
|
||||
if (m.bullishAdxTrend) score += Math.max(0, r.getWeightAdxTrend());
|
||||
if (m.bullishMacdMomentum) score += Math.max(0, r.getWeightMacdMomentum());
|
||||
if (m.bullishPricePosition) score += Math.max(0, r.getWeightPricePosition());
|
||||
return Math.min(100, score);
|
||||
}
|
||||
|
||||
/** 정배열·상승세·거래량 복합 추세 강도 (0~100) */
|
||||
private int computeTrendStrength(MetricValues m) {
|
||||
double score = 0;
|
||||
@@ -366,9 +415,19 @@ public class TrendSearchService {
|
||||
|
||||
private TrendSearchResultDto toResult(ScoredMarket s, TrendSearchRequest r) {
|
||||
List<TrendSearchConditionDto> conditions = buildConditions(s.metrics, r);
|
||||
int matched = (int) conditions.stream().filter(c -> "match".equals(c.getStatus())).count();
|
||||
int total = conditions.size();
|
||||
int matchRate = total > 0 ? Math.round(matched * 100f / total) : (int) s.metrics.matchScore;
|
||||
final int matched;
|
||||
final int total;
|
||||
final int matchRate;
|
||||
|
||||
if (r.isBullishTrendEnabled()) {
|
||||
matchRate = s.metrics.bullishTrendScore;
|
||||
matched = matchRate;
|
||||
total = 100;
|
||||
} else {
|
||||
matched = (int) conditions.stream().filter(c -> "match".equals(c.getStatus())).count();
|
||||
total = conditions.size();
|
||||
matchRate = total > 0 ? Math.round(matched * 100f / total) : s.metrics.matchScore;
|
||||
}
|
||||
s.metrics.matchScore = matchRate;
|
||||
double lastClose = s.candles.get(s.candles.size() - 1).getClose();
|
||||
|
||||
@@ -387,11 +446,35 @@ public class TrendSearchService {
|
||||
|
||||
private List<TrendSearchConditionDto> buildConditions(MetricValues m, TrendSearchRequest r) {
|
||||
List<TrendSearchConditionDto> out = new ArrayList<>();
|
||||
String catBullish = "상승추세";
|
||||
String catMa = "Ⅰ. 정배열";
|
||||
String catTrend = "Ⅱ. 상승세";
|
||||
String catVol = "Ⅲ. 돈과 힘";
|
||||
String catInd = "Ⅳ. 보조지표";
|
||||
|
||||
if (r.isBullishTrendEnabled()) {
|
||||
addBullishScore(out, "bullishEmaAlignment", catBullish, "이평선 정배열",
|
||||
m.bullishEmaAlignment, r.getWeightMaAlignment(),
|
||||
"EMA20>60>120", round2(m.ema20), round2(m.ema60), round2(m.ema120));
|
||||
addBullishScore(out, "bullishEmaSlope", catBullish, "이평선 기울기",
|
||||
m.bullishEmaSlope, r.getWeightMaSlope(),
|
||||
"EMA20 > " + r.getEmaSlopeLookback() + "봉 전",
|
||||
round2(m.ema20), round2(m.ema20Past), null);
|
||||
addBullishScore(out, "bullishAdxTrend", catBullish, "추세강도 (ADX)",
|
||||
m.bullishAdxTrend, r.getWeightAdxTrend(),
|
||||
"+DI>" + round2(m.mdi) + " ADX≥" + round2(r.getAdxTrendMin()),
|
||||
round2(m.pdi), round2(m.adxVal), null);
|
||||
addBullishScore(out, "bullishMacdMomentum", catBullish, "모멘텀 (MACD)",
|
||||
m.bullishMacdMomentum, r.getWeightMacdMomentum(),
|
||||
"MACD>Signal & MACD>0",
|
||||
round2(m.macdVal), round2(m.signalVal), null);
|
||||
addBullishScore(out, "bullishPricePosition", catBullish, "가격위치",
|
||||
m.bullishPricePosition, r.getWeightPricePosition(),
|
||||
"종가 > EMA20",
|
||||
round2(m.price), round2(m.ema20), null);
|
||||
return out;
|
||||
}
|
||||
|
||||
if (r.isMaAlignEnabled()) {
|
||||
addFilter(out, "priceAboveMa", catMa, "주가 > 5·20·60 이평", r.isPriceAboveMa(), m.priceAboveMa);
|
||||
addFilter(out, "maAlignment", catMa, "5 > 20 > 60 > 120 정배열", r.isMaAlignment(), m.maAlignment);
|
||||
@@ -457,6 +540,32 @@ public class TrendSearchService {
|
||||
out.add(cond(id, cat, label, pass ? 1.0 : 0.0, "필수 충족", pass ? 100 : 0, pass ? "match" : "mismatch"));
|
||||
}
|
||||
|
||||
private void addBullishScore(List<TrendSearchConditionDto> out, String id, String cat, String label,
|
||||
boolean pass, int maxPoints, String threshold,
|
||||
double v1, double v2, Double v3) {
|
||||
int earned = pass ? maxPoints : 0;
|
||||
int progress = maxPoints > 0 ? Math.round(earned * 100f / maxPoints) : 0;
|
||||
String currentLabel = v3 != null
|
||||
? round2(v1) + " / " + round2(v2) + " / " + round2(v3)
|
||||
: (id.equals("bullishEmaSlope")
|
||||
? round2(v1) + " vs " + round2(v2)
|
||||
: round2(v1) + (Double.isFinite(v2) && v2 != 0 ? " · " + round2(v2) : ""));
|
||||
out.add(cond(id, cat, label + " (" + maxPoints + "점)",
|
||||
pass ? (double) earned : 0.0,
|
||||
threshold + " · " + currentLabel,
|
||||
progress, pass ? "match" : "mismatch"));
|
||||
}
|
||||
|
||||
private static double safeEma(EMAIndicator ema, int idx) {
|
||||
if (idx < 0) return Double.NaN;
|
||||
try {
|
||||
double v = ema.getValue(idx).doubleValue();
|
||||
return Double.isFinite(v) ? v : Double.NaN;
|
||||
} catch (Exception e) {
|
||||
return Double.NaN;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Indicator helpers ───────────────────────────────────────────────────
|
||||
|
||||
private static double maSpreadPct(double m5, double m20, double m60) {
|
||||
@@ -689,8 +798,12 @@ public class TrendSearchService {
|
||||
final boolean rsiBand;
|
||||
final double pdi, mdi, adxVal;
|
||||
final boolean dmiBullish;
|
||||
final double ema20, ema60, ema120, ema20Past;
|
||||
final boolean bullishEmaAlignment, bullishEmaSlope, bullishAdxTrend;
|
||||
final boolean bullishMacdMomentum, bullishPricePosition;
|
||||
int matchScore;
|
||||
int trendStrength;
|
||||
int bullishTrendScore;
|
||||
|
||||
MetricValues(boolean priceAboveMa, boolean maAlignment, boolean maConvergence, double maSpreadPct,
|
||||
boolean ma20SlopeUp, boolean newHighBreakout, double periodHigh, double price,
|
||||
@@ -699,7 +812,10 @@ public class TrendSearchService {
|
||||
boolean minTurnover5dAvg, boolean volMa5Over20,
|
||||
double macdVal, double signalVal, boolean macdGoldenCross,
|
||||
double rsiVal, boolean rsiBand,
|
||||
double pdi, double mdi, double adxVal, boolean dmiBullish) {
|
||||
double pdi, double mdi, double adxVal, boolean dmiBullish,
|
||||
double ema20, double ema60, double ema120, double ema20Past,
|
||||
boolean bullishEmaAlignment, boolean bullishEmaSlope, boolean bullishAdxTrend,
|
||||
boolean bullishMacdMomentum, boolean bullishPricePosition) {
|
||||
this.priceAboveMa = priceAboveMa;
|
||||
this.maAlignment = maAlignment;
|
||||
this.maConvergence = maConvergence;
|
||||
@@ -723,6 +839,15 @@ public class TrendSearchService {
|
||||
this.mdi = mdi;
|
||||
this.adxVal = adxVal;
|
||||
this.dmiBullish = dmiBullish;
|
||||
this.ema20 = ema20;
|
||||
this.ema60 = ema60;
|
||||
this.ema120 = ema120;
|
||||
this.ema20Past = ema20Past;
|
||||
this.bullishEmaAlignment = bullishEmaAlignment;
|
||||
this.bullishEmaSlope = bullishEmaSlope;
|
||||
this.bullishAdxTrend = bullishAdxTrend;
|
||||
this.bullishMacdMomentum = bullishMacdMomentum;
|
||||
this.bullishPricePosition = bullishPricePosition;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user