@@ -53,7 +53,13 @@ def execute_backtest(stock_id: int, pattern_id: int, request: BacktestRequest, d
5353 )
5454
5555 # dtw 로직
56- idxes , _ = find_match_indices (pattern_obj .points , closes , pattern_obj .tolerance )
56+ idxes , distances = find_match_indices (pattern_obj .points , closes , pattern_obj .tolerance )
57+
58+ # 유사도 계산 (백테스팅 평가 지표)
59+ similarities = [BacktestService ._convert_distance_to_similarity (d ) for d in distances ]
60+ if similarities :
61+ avg_sim = sum (similarities ) / len (similarities )
62+ logger .info (f"[Backtest] 평균 유사도: { avg_sim :.3f} , 매칭 개수: { len (similarities )} " )
5763
5864 # 수익률 계산
5965 returns = BacktestService ._calculate_returns (idxes , closes , timestamps , unit , value , len (pattern_obj .points ))
@@ -130,10 +136,39 @@ def _fetch_timeseries_by_unit(
130136 if not rows :
131137 raise APIException (ErrorStatus .STOCK_OHLCV_NOT_FOUND )
132138
133- # (timestamp, close) 형태로 분리 후 리스트 반환
139+ # (timestamp, close) 형태로 분리
134140 timestamps , closes = zip (* rows )
141+
142+ # 노이즈 제거
143+ closes = BacktestService ._preprocess_series (closes )
144+
145+ # 리스트 반환
135146 return list (timestamps ), list (closes )
136147
148+ @staticmethod
149+ def _preprocess_series (
150+ closes : List [float ],
151+ window : int = 5
152+ ) -> List [float ]:
153+ """
154+ 노이즈를 제거하고 정규화 과정을 진행시킵니다.
155+ """
156+
157+ # 노이즈 제거
158+ series = pd .Series (closes ).rolling (window = window , center = True ).mean ().bfill ().ffill ()
159+
160+ # 정규화
161+ normed = (series - series .mean ()) / (series .std () + 1e-8 )
162+ return normed .to_list ()
163+
164+ @staticmethod
165+ def _convert_distance_to_similarity (distance : float ) -> float :
166+ """
167+ DTW 거리값을 0~1 사이 유사도로 변환합니다.
168+ - formula: similarity = 1 / (1 + distance)
169+ """
170+ return round (1 / (1 + distance ), 4 )
171+
137172 @staticmethod
138173 def _calculate_returns (
139174 idxes : List [int ],
@@ -187,9 +222,22 @@ def _calculate_returns(
187222 elif pos > 0 and (timestamps [pos ] - tgt ) > (tgt - timestamps [pos - 1 ]):
188223 pos -= 1
189224
190- # 진입가, 청산가 활용하여 수익률 계산
191- entry_p , exit_p = closes [entry_i ], closes [pos ]
192- ret = (exit_p - entry_p ) / entry_p * 100
225+ # 진입가
226+ entry_p = closes [entry_i ]
227+
228+ # 평균가
229+ segment_prices = closes [entry_i :pos + 1 ]
230+ if len (segment_prices ) < 2 :
231+ continue
232+ avg_price = sum (segment_prices ) / len (segment_prices )
233+
234+ # 청산가
235+ exit_p = segment_prices [- 1 ]
236+
237+ # 하이브리드 수익률 계산 (진입가, 평균가, 청산가 활용)
238+ ret_avg = (avg_price - entry_p ) / entry_p * 100
239+ ret_exit = (exit_p - entry_p ) / entry_p * 100
240+ ret = (ret_avg + ret_exit ) / 2
193241
194242 # 매칭 구간 저장
195243 match_start = timestamps [idx ]
0 commit comments