Skip to content

Commit cdabca1

Browse files
authored
Merge pull request #248 from FunD-StockProject/refactor/portfolio-result-edit
Refactor/portfolio result edit
2 parents 4ada68f + b84d0c9 commit cdabca1

4 files changed

Lines changed: 167 additions & 25 deletions

File tree

src/main/java/com/fund/stockProject/experiment/repository/ExperimentRepository.java

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,58 @@ public interface ExperimentRepository extends JpaRepository<Experiment, Integer>
106106
+ "WHERE a.ratio BETWEEN :startRange AND :endRange", nativeQuery = true)
107107
int countSameGradeUser(@Param("startRange") int startRange, @Param("endRange") int endRange);
108108

109+
@Query(value = "SELECT count(*) "
110+
+ "FROM "
111+
+ "( "
112+
+ " SELECT ROUND(IFNULL(profitable.cnt, 0) / total.cnt * 100, 1) AS ratio "
113+
+ " FROM "
114+
+ " ( SELECT u.email, COUNT(e.id) AS cnt "
115+
+ " FROM experiment e "
116+
+ " JOIN users u ON e.user_id = u.id "
117+
+ " WHERE e.status = 'COMPLETE' "
118+
+ " GROUP BY u.email "
119+
+ " HAVING COUNT(e.id) > 0 "
120+
+ " ) AS total "
121+
+ " LEFT JOIN "
122+
+ " ( "
123+
+ " SELECT u.email, COUNT(e.id) AS cnt "
124+
+ " FROM experiment e "
125+
+ " JOIN users u ON e.user_id = u.id "
126+
+ " WHERE e.status = 'COMPLETE' AND e.roi > 0 "
127+
+ " GROUP BY u.email "
128+
+ " ) AS profitable "
129+
+ " ON total.email = profitable.email "
130+
+ " WHERE total.cnt > 0 "
131+
+ ") a "
132+
+ "WHERE a.ratio >= :startRange AND a.ratio < :endRange", nativeQuery = true)
133+
int countUsersBySuccessRateRange(@Param("startRange") double startRange, @Param("endRange") double endRange);
134+
135+
@Query(value = "SELECT count(*) "
136+
+ "FROM "
137+
+ "( "
138+
+ " SELECT ROUND(IFNULL(profitable.cnt, 0) / total.cnt * 100, 1) AS ratio "
139+
+ " FROM "
140+
+ " ( SELECT u.email, COUNT(e.id) AS cnt "
141+
+ " FROM experiment e "
142+
+ " JOIN users u ON e.user_id = u.id "
143+
+ " WHERE e.status = 'COMPLETE' "
144+
+ " GROUP BY u.email "
145+
+ " HAVING COUNT(e.id) > 0 "
146+
+ " ) AS total "
147+
+ " LEFT JOIN "
148+
+ " ( "
149+
+ " SELECT u.email, COUNT(e.id) AS cnt "
150+
+ " FROM experiment e "
151+
+ " JOIN users u ON e.user_id = u.id "
152+
+ " WHERE e.status = 'COMPLETE' AND e.roi > 0 "
153+
+ " GROUP BY u.email "
154+
+ " ) AS profitable "
155+
+ " ON total.email = profitable.email "
156+
+ " WHERE total.cnt > 0 "
157+
+ ") a "
158+
+ "WHERE a.ratio >= :startRange", nativeQuery = true)
159+
int countUsersBySuccessRateAtLeast(@Param("startRange") double startRange);
160+
109161
@Query("SELECT COUNT(DISTINCT e.user.id) FROM Experiment e WHERE e.status = 'COMPLETE'")
110162
long countUsersWithCompletedExperiments();
111163

src/main/java/com/fund/stockProject/experiment/service/ExperimentService.java

Lines changed: 111 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -769,21 +769,11 @@ public PortfolioResultResponse getPortfolioResult(final CustomUserDetails custom
769769
distribution.put("good", 0);
770770
distribution.put("best", 0);
771771
} else {
772-
distribution.put("worst", (int) (experimentRepository.countSameGradeUser(0, 20) * 100L / completedUserCount));
773-
distribution.put("bad", (int) (experimentRepository.countSameGradeUser(21, 40) * 100L / completedUserCount));
774-
distribution.put("normal", (int) (experimentRepository.countSameGradeUser(41, 60) * 100L / completedUserCount));
775-
distribution.put("good", (int) (experimentRepository.countSameGradeUser(61, 80) * 100L / completedUserCount));
776-
distribution.put("best", (int) (experimentRepository.countSameGradeUser(81, 100) * 100L / completedUserCount));
777-
}
778-
779-
// 최고/최저 수익률 실험의 점수 산출
780-
Integer bestYieldScore = null;
781-
Integer worstYieldScore = null;
782-
if (!completed.isEmpty()) {
783-
Experiment max = completed.stream().max((a, b) -> Double.compare(a.getRoi(), b.getRoi())).get();
784-
Experiment min = completed.stream().min((a, b) -> Double.compare(a.getRoi(), b.getRoi())).get();
785-
bestYieldScore = max.getScore();
786-
worstYieldScore = min.getScore();
772+
distribution.put("worst", (int) (experimentRepository.countUsersBySuccessRateRange(0, 20) * 100L / completedUserCount));
773+
distribution.put("bad", (int) (experimentRepository.countUsersBySuccessRateRange(20, 40) * 100L / completedUserCount));
774+
distribution.put("normal", (int) (experimentRepository.countUsersBySuccessRateRange(40, 60) * 100L / completedUserCount));
775+
distribution.put("good", (int) (experimentRepository.countUsersBySuccessRateRange(60, 80) * 100L / completedUserCount));
776+
distribution.put("best", (int) (experimentRepository.countUsersBySuccessRateAtLeast(80) * 100L / completedUserCount));
787777
}
788778

789779
// 점수 구간별 사용자 평균 수익률 및 전체 유저 평균
@@ -803,6 +793,10 @@ public PortfolioResultResponse getPortfolioResult(final CustomUserDetails custom
803793
scoreTable.add(PortfolioResultResponse.ScoreTableItem.builder().min(80).max(89).avgYieldTotal(t_80_89).avgYieldUser(u_80_89).build());
804794
scoreTable.add(PortfolioResultResponse.ScoreTableItem.builder().min(90).max(100).avgYieldTotal(t_90_100).avgYieldUser(u_90_100).build());
805795

796+
BestWorstRangeScores bestWorstRangeScores = resolveBestWorstRangeScores(scoreTable);
797+
Integer bestYieldScore = bestWorstRangeScores.bestScore();
798+
Integer worstYieldScore = bestWorstRangeScores.worstScore();
799+
806800
// HumanIndicator type 결정 (성공률 기반)
807801
String humanIndicatorType;
808802
if (totalCompleted == 0 || successRateVal <= 20) {
@@ -834,7 +828,7 @@ public PortfolioResultResponse getPortfolioResult(final CustomUserDetails custom
834828
.map(e -> PortfolioResultResponse.HistoryPoint.builder()
835829
.date(PortfolioResultResponse.HistoryPoint.toDateLabel(e.getSellAt() != null ? e.getSellAt() : e.getBuyAt()))
836830
.score(e.getScore())
837-
.yield(e.getRoi() != null ? e.getRoi() : 0.0)
831+
.roi(roundTo1Decimal(e.getRoi() != null ? e.getRoi() : 0.0))
838832
.stockId(e.getStock().getId())
839833
.stockName(e.getStock().getSecurityName())
840834
.isDuplicateName(stockNameCount.get(e.getStock().getSecurityName()) > 1)
@@ -850,14 +844,14 @@ public PortfolioResultResponse getPortfolioResult(final CustomUserDetails custom
850844

851845
for (PortfolioResultResponse.HistoryPoint point : history) {
852846
double dx = point.getScore() - 50.0;
853-
double dy = point.getYield();
847+
double dy = point.getRoi();
854848
double distance = Math.sqrt(dx * dx + dy * dy);
855849

856-
if (point.getScore() < 50 && point.getYield() > 0) {
850+
if (point.getScore() < 50 && point.getRoi() > 0) {
857851
valuePreemptiveSum += distance; // 가치 선점형
858-
} else if (point.getScore() >= 50 && point.getYield() > 0) {
852+
} else if (point.getScore() >= 50 && point.getRoi() > 0) {
859853
trendPreemptiveSum += distance; // 트렌드 선점형
860-
} else if (point.getScore() < 50 && point.getYield() <= 0) {
854+
} else if (point.getScore() < 50 && point.getRoi() <= 0) {
861855
reverseInvestorSum += distance; // 역행 투자형
862856
} else {
863857
laggingFollowerSum += distance; // 후행 추종형
@@ -937,6 +931,99 @@ public HumanIndicatorDistributionResponse getHumanIndicatorDistribution() {
937931
.build();
938932
}
939933

934+
private BestWorstRangeScores resolveBestWorstRangeScores(List<PortfolioResultResponse.ScoreTableItem> scoreTable) {
935+
if (scoreTable == null || scoreTable.isEmpty()) {
936+
return new BestWorstRangeScores(null, null);
937+
}
938+
939+
ScoreTableItemRange bestRange = null;
940+
ScoreTableItemRange worstRange = null;
941+
942+
List<PortfolioResultResponse.ScoreTableItem> userItems = filterByMetric(scoreTable, true);
943+
if (!userItems.isEmpty()) {
944+
bestRange = toRange(selectBestRange(userItems, true));
945+
worstRange = toRange(selectWorstRange(excludeRange(userItems, bestRange), true));
946+
}
947+
948+
if (bestRange == null) {
949+
List<PortfolioResultResponse.ScoreTableItem> totalItems = filterByMetric(scoreTable, false);
950+
bestRange = toRange(selectBestRange(totalItems, false));
951+
}
952+
953+
if (worstRange == null) {
954+
List<PortfolioResultResponse.ScoreTableItem> totalItems = filterByMetric(scoreTable, false);
955+
worstRange = toRange(selectWorstRange(excludeRange(totalItems, bestRange), false));
956+
}
957+
958+
Integer bestScore = bestRange != null ? bestRange.min() : null;
959+
Integer worstScore = worstRange != null ? worstRange.min() : null;
960+
return new BestWorstRangeScores(bestScore, worstScore);
961+
}
962+
963+
private List<PortfolioResultResponse.ScoreTableItem> filterByMetric(
964+
List<PortfolioResultResponse.ScoreTableItem> scoreTable,
965+
boolean useUser
966+
) {
967+
return scoreTable.stream()
968+
.filter(item -> getMetric(item, useUser) != null)
969+
.toList();
970+
}
971+
972+
private PortfolioResultResponse.ScoreTableItem selectBestRange(
973+
List<PortfolioResultResponse.ScoreTableItem> items,
974+
boolean useUser
975+
) {
976+
if (items == null || items.isEmpty()) {
977+
return null;
978+
}
979+
return items.stream()
980+
.max(java.util.Comparator.<PortfolioResultResponse.ScoreTableItem>comparingDouble(
981+
item -> getMetric(item, useUser))
982+
.thenComparingInt(PortfolioResultResponse.ScoreTableItem::getMin))
983+
.orElse(null);
984+
}
985+
986+
private PortfolioResultResponse.ScoreTableItem selectWorstRange(
987+
List<PortfolioResultResponse.ScoreTableItem> items,
988+
boolean useUser
989+
) {
990+
if (items == null || items.isEmpty()) {
991+
return null;
992+
}
993+
return items.stream()
994+
.min(java.util.Comparator.<PortfolioResultResponse.ScoreTableItem>comparingDouble(
995+
item -> getMetric(item, useUser))
996+
.thenComparingInt(PortfolioResultResponse.ScoreTableItem::getMin))
997+
.orElse(null);
998+
}
999+
1000+
private List<PortfolioResultResponse.ScoreTableItem> excludeRange(
1001+
List<PortfolioResultResponse.ScoreTableItem> items,
1002+
ScoreTableItemRange range
1003+
) {
1004+
if (items == null || items.isEmpty() || range == null) {
1005+
return items;
1006+
}
1007+
return items.stream()
1008+
.filter(item -> item.getMin() != range.min() || item.getMax() != range.max())
1009+
.toList();
1010+
}
1011+
1012+
private Double getMetric(PortfolioResultResponse.ScoreTableItem item, boolean useUser) {
1013+
return useUser ? item.getAvgYieldUser() : item.getAvgYieldTotal();
1014+
}
1015+
1016+
private ScoreTableItemRange toRange(PortfolioResultResponse.ScoreTableItem item) {
1017+
if (item == null) {
1018+
return null;
1019+
}
1020+
return new ScoreTableItemRange(item.getMin(), item.getMax());
1021+
}
1022+
1023+
private record ScoreTableItemRange(int min, int max) { }
1024+
1025+
private record BestWorstRangeScores(Integer bestScore, Integer worstScore) { }
1026+
9401027
private String toScoreRangeLabel(int score) {
9411028
if (score <= 59) return "60점 이하";
9421029
if (score <= 69) return "60-69";
@@ -945,6 +1032,10 @@ private String toScoreRangeLabel(int score) {
9451032
return "90+";
9461033
}
9471034

1035+
private double roundTo1Decimal(double value) {
1036+
return Math.round(value * 10.0) / 10.0;
1037+
}
1038+
9481039
// 영업일 기준 실험 진행한 기간이 5일 이상 지난 실험 데이터 조회
9491040
@Transactional(readOnly = true)
9501041
public List<Experiment> findExperimentsAfter5BusinessDays() {

src/main/java/com/fund/stockProject/notification/controller/NotificationController.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ public ResponseEntity<NotificationResponse> createTestNotification(
164164

165165
Notification testNotification = Notification.builder()
166166
.user(user)
167-
.notificationType(NotificationType.TEST)
167+
.notificationType(NotificationType.SCORE_SPIKE)
168168
.title("테스트 알림")
169169
.body("이것은 테스트 알림입니다. " + System.currentTimeMillis())
170170
.isRead(false)

src/main/java/com/fund/stockProject/portfolio/dto/PortfolioResultResponse.java

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ public class PortfolioResultResponse {
2626
@AllArgsConstructor
2727
public static class Recommend {
2828
private int weeklyExperimentCount; // 이번 주 실험 진행 횟수
29-
private Integer bestYieldScore; // 가장 높은 수익률 실험의 점수
30-
private Integer worstYieldScore; // 가장 낮은 수익률 실험의 점수
29+
private Integer bestYieldScore; // 가장 높은 수익률 구간의 대표 점수(구간 min)
30+
private Integer worstYieldScore; // 가장 낮은 수익률 구간의 대표 점수(구간 min)
3131
private List<ScoreTableItem> scoreTable; // 점수대별 통계
3232
}
3333

@@ -72,7 +72,7 @@ public static class Pattern {
7272
public static class HistoryPoint {
7373
private String date; // MM.DD
7474
private int score; // X축 (0~100)
75-
private double yield; // Y축
75+
private double roi; // Y축 (수익률)
7676
private Integer stockId; // 종목 ID
7777
private String stockName; // 종목명
7878
private boolean isDuplicateName; // 리스트 내 이름 중복 여부
@@ -82,4 +82,3 @@ public static String toDateLabel(LocalDateTime dateTime) {
8282
}
8383
}
8484
}
85-

0 commit comments

Comments
 (0)