@@ -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 () {
0 commit comments