추세검색 설정 수정

This commit is contained in:
Macbook
2026-05-27 02:10:00 +09:00
parent 2e08c6b16f
commit fc06a16184
27 changed files with 1262 additions and 147 deletions
@@ -0,0 +1,89 @@
package com.goldenchart.service;
import com.goldenchart.dto.TrendSearchRequest;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
/**
* 추세검색 상승추세 배점·합격 점수 로직 검증.
*/
class TrendSearchBullishScoringTest {
private static TrendSearchBullishScoring.BullishFlags flags(
boolean align, boolean slope, boolean adx, boolean macd, boolean price) {
return new TrendSearchBullishScoring.BullishFlags() {
@Override public boolean bullishEmaAlignment() { return align; }
@Override public boolean bullishEmaSlope() { return slope; }
@Override public boolean bullishAdxTrend() { return adx; }
@Override public boolean bullishMacdMomentum() { return macd; }
@Override public boolean bullishPricePosition() { return price; }
};
}
@Test
void allPass_defaultWeights_scores100() {
TrendSearchRequest r = new TrendSearchRequest();
int score = TrendSearchBullishScoring.computeScore(
flags(true, true, true, true, true), r);
assertEquals(100, score);
}
@Test
void partialPass_sumsOnlyMatchedWeights() {
TrendSearchRequest r = new TrendSearchRequest();
r.setWeightMaAlignment(40);
r.setWeightMaSlope(10);
r.setWeightAdxTrend(20);
r.setWeightMacdMomentum(15);
r.setWeightPricePosition(15);
int score = TrendSearchBullishScoring.computeScore(
flags(true, false, true, false, true), r);
assertEquals(75, score);
}
@Test
void customWeights_onlyAlignment_countsAlignmentOnly() {
TrendSearchRequest r = new TrendSearchRequest();
r.setWeightMaAlignment(50);
r.setWeightMaSlope(0);
r.setWeightAdxTrend(0);
r.setWeightMacdMomentum(0);
r.setWeightPricePosition(0);
assertEquals(50, TrendSearchBullishScoring.computeScore(
flags(true, true, true, true, true), r));
assertEquals(0, TrendSearchBullishScoring.computeScore(
flags(false, true, true, true, true), r));
}
@Test
void scoreCappedAt100_whenWeightsOverflow() {
TrendSearchRequest r = new TrendSearchRequest();
r.setWeightMaAlignment(60);
r.setWeightMaSlope(60);
r.setWeightAdxTrend(60);
r.setWeightMacdMomentum(60);
r.setWeightPricePosition(60);
assertEquals(100, TrendSearchBullishScoring.computeScore(
flags(true, true, true, true, true), r));
}
@Test
void minTrendScore_filter() {
TrendSearchRequest r = new TrendSearchRequest();
r.setMinTrendScore(70);
assertTrue(TrendSearchBullishScoring.passesMinTrendScore(70, r));
assertTrue(TrendSearchBullishScoring.passesMinTrendScore(85, r));
assertFalse(TrendSearchBullishScoring.passesMinTrendScore(69, r));
r.setMinTrendScore(0);
assertTrue(TrendSearchBullishScoring.passesMinTrendScore(0, r));
}
@Test
void negativeWeights_treatedAsZero() {
TrendSearchRequest r = new TrendSearchRequest();
r.setWeightMaAlignment(-10);
r.setWeightMaSlope(20);
assertEquals(20, TrendSearchBullishScoring.computeScore(flags(true, true, false, false, false), r));
}
}
@@ -0,0 +1,122 @@
package com.goldenchart.service;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.goldenchart.dto.CandleBarDto;
import com.goldenchart.dto.TrendSearchRequest;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.web.reactive.function.client.WebClient;
import org.ta4j.core.BarSeries;
import org.ta4j.core.BaseBarSeriesBuilder;
import org.ta4j.core.bars.TimeBarBuilderFactory;
import org.ta4j.core.num.DoubleNumFactory;
import java.lang.reflect.Method;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.mock;
/**
* 합성 상승 시계열에서 지표·배점 점수가 일관되는지 검증.
*/
class TrendSearchServiceMetricsTest {
private TrendSearchService service;
private Method computeMetrics;
private Method computeBullishTrendScore;
private Class<?> tickerSnapClass;
@BeforeEach
void setUp() throws Exception {
service = new TrendSearchService(
mock(HistoricalDataService.class),
WebClient.builder(),
new ObjectMapper());
for (Class<?> c : TrendSearchService.class.getDeclaredClasses()) {
if ("TickerSnap".equals(c.getSimpleName())) tickerSnapClass = c;
}
for (Method m : TrendSearchService.class.getDeclaredMethods()) {
if ("computeMetrics".equals(m.getName())) computeMetrics = m;
if ("computeBullishTrendScore".equals(m.getName())) computeBullishTrendScore = m;
}
assertNotNull(computeMetrics);
assertNotNull(computeBullishTrendScore);
computeMetrics.setAccessible(true);
computeBullishTrendScore.setAccessible(true);
}
@Test
void strongUptrend_series_earnsHighBullishScore() throws Exception {
BarSeries series = buildUptrendSeries(150);
List<CandleBarDto> candles = toCandles(series);
Object tick = newTickerSnap();
TrendSearchRequest req = new TrendSearchRequest();
Object metrics = computeMetrics.invoke(service, series, candles, tick, req);
int score = (int) computeBullishTrendScore.invoke(service, metrics, req);
assertTrue(score >= 70,
"상승 추세 합성 데이터는 기본 배점(합 100) 기준 70점 이상이어야 함, actual=" + score);
}
@Test
void highMinTrendScore_excludesLowScores_inScanLogic() {
TrendSearchRequest r = new TrendSearchRequest();
r.setMinTrendScore(95);
assertFalse(TrendSearchBullishScoring.passesMinTrendScore(80, r));
assertTrue(TrendSearchBullishScoring.passesMinTrendScore(95, r));
}
private Object newTickerSnap() throws Exception {
assertNotNull(tickerSnapClass);
var ctor = tickerSnapClass.getDeclaredConstructor(
String.class, String.class, double.class, double.class, double.class);
ctor.setAccessible(true);
return ctor.newInstance("KRW-TEST", "테스트", 100.0, 5.0, 1_000_000_000.0);
}
private static BarSeries buildUptrendSeries(int bars) {
Duration period = Duration.ofDays(1);
BarSeries series = new BaseBarSeriesBuilder()
.withName("uptrend")
.withNumFactory(DoubleNumFactory.getInstance())
.withBarBuilderFactory(new TimeBarBuilderFactory())
.build();
TimeBarBuilderFactory factory = new TimeBarBuilderFactory();
Instant base = Instant.parse("2020-01-01T00:00:00Z");
double price = 100;
for (int i = 0; i < bars; i++) {
double close = price * 1.008;
double high = Math.max(price, close) * 1.005;
double low = Math.min(price, close) * 0.995;
factory.createBarBuilder(series)
.timePeriod(period)
.endTime(base.plus(period.multipliedBy(i + 1L)))
.openPrice(price).highPrice(high).lowPrice(low).closePrice(close)
.volume(1_000_000 + i * 1000L)
.add();
price = close;
}
return series;
}
private static List<CandleBarDto> toCandles(BarSeries series) {
List<CandleBarDto> out = new ArrayList<>();
for (int i = 0; i < series.getBarCount(); i++) {
var bar = series.getBar(i);
out.add(CandleBarDto.builder()
.time(bar.getBeginTime().getEpochSecond())
.open(bar.getOpenPrice().doubleValue())
.high(bar.getHighPrice().doubleValue())
.low(bar.getLowPrice().doubleValue())
.close(bar.getClosePrice().doubleValue())
.volume(bar.getVolume().doubleValue())
.build());
}
return out;
}
}
@@ -0,0 +1,64 @@
package com.goldenchart.service;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.goldenchart.dto.TrendSearchRequest;
import com.goldenchart.entity.GcAppSettings;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
/**
* 앱 설정 JSON → 스캔 요청 배점 병합 검증.
*/
class TrendSearchSettingsHelperTest {
private TrendSearchSettingsHelper helper;
@BeforeEach
void setUp() {
helper = new TrendSearchSettingsHelper(new ObjectMapper());
}
@Test
void mergeRequestWithSettings_appliesWeightsFromJson() throws Exception {
GcAppSettings app = GcAppSettings.builder()
.deviceId("test-device")
.trendSearchSettingsJson("""
{"weightMaAlignment":50,"weightMaSlope":10,"weightAdxTrend":15,
"weightMacdMomentum":10,"weightPricePosition":15,"minTrendScore":60,
"limit":15,"scanLimit":40}
""")
.build();
TrendSearchRequest body = new TrendSearchRequest();
body.setWeightMaAlignment(30);
body.setMinTrendScore(70);
TrendSearchRequest merged = helper.mergeRequestWithSettings(body, app);
assertEquals(50, merged.getWeightMaAlignment());
assertEquals(10, merged.getWeightMaSlope());
assertEquals(15, merged.getWeightAdxTrend());
assertEquals(60, merged.getMinTrendScore());
assertEquals(15, merged.getLimit());
assertEquals(40, merged.getScanLimit());
}
@Test
void buildRequest_usesLastRequestTimeframe_whenPresent() throws Exception {
GcAppSettings app = GcAppSettings.builder()
.defaultTimeframe("1D")
.trendSearchSettingsJson("{\"weightMaAlignment\":25}")
.build();
TrendSearchRequest last = new TrendSearchRequest();
last.setTimeframe("4h");
last.setWeightMaAlignment(99);
TrendSearchRequest built = helper.buildRequest(app, last);
assertEquals("4h", built.getTimeframe());
assertEquals(25, built.getWeightMaAlignment());
}
}