Coverage for strategies / momentum_strategy.py: 90%
68 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-04 15:08 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-04 15:08 +0000
1# strategies/momentum_strategy.py
2import logging
3from interfaces.strategy import Strategy
4from typing import List, Dict, Optional, Callable
5from common.types import ErrorCode, ResCommonResponse
6import inspect
9class MomentumStrategy(Strategy):
10 def __init__(
11 self,
12 broker,
13 min_change_rate: float = 10.0,
14 min_follow_through: float = 3.0,
15 min_follow_through_time: int = 5, # 몇 분 후 기준인지
16 mode: str = "live", # 'live' or 'backtest'
17 backtest_lookup: Optional[Callable[[str, Dict, int], int]] = None,
18 logger: Optional[logging.Logger] = None
19 ):
20 self.broker = broker # ✅ 통합 wrapper (API + CSV)
21 self.min_change_rate = min_change_rate
22 self.min_follow_through = min_follow_through
23 self.min_follow_through_time = min_follow_through_time # 추가
24 self.mode = mode
25 self.backtest_lookup = backtest_lookup
26 self.logger = logger or logging.getLogger(__name__)
28 async def run(self, stock_codes: List[str]) -> Dict:
29 results = []
31 for code in stock_codes:
32 summary : ResCommonResponse = await self.broker.get_price_summary(code) # ✅ wrapper 통해 조회
34 if not summary: 34 ↛ 35line 34 didn't jump to line 35 because the condition on line 34 was never true
35 self.logger.warning(f"종목 {code}: 가격 요약 데이터를 받지 못했습니다. 건너뜁니다.")
36 continue
38 if hasattr(summary, 'data') and summary.data is None: 38 ↛ 39line 38 didn't jump to line 39 because the condition on line 38 was never true
39 self.logger.warning(f"종목 {code}: 가격 요약 데이터가 비어있습니다. 건너뜁니다.")
40 continue
42 if self.mode == "backtest":
43 if not self.backtest_lookup:
44 raise ValueError("Backtest 모드에서는 backtest_lookup 함수가 필요합니다.")
46 result = self.backtest_lookup(
47 code,
48 summary.data,
49 self.min_follow_through_time
50 )
51 after_price = await result if inspect.isawaitable(result) else result
52 else:
53 price_data : ResCommonResponse = await self.broker.get_current_price(code, caller="MomentumStrategy") # ✅ wrapper 통해 조회
54 after_price = int(price_data.data.get("stck_prpr", "0") or 0) if price_data and price_data.data else 0
56 if after_price is None: 56 ↛ 57line 56 didn't jump to line 57 because the condition on line 56 was never true
57 self.logger.warning(f"종목 {code}: after_price를 받지 못했습니다. 0으로 대체합니다.")
58 after_price = 0
60 summary.data["after"] = after_price
61 current_price = summary.data.get("current", 0) or 0
62 summary.data["after_rate"] = (
63 (after_price - current_price) / current_price * 100
64 if current_price else 0
65 )
66 results.append(summary.data)
68 follow_through = []
69 not_follow_through = []
72 for s in results:
73 code = s["symbol"]
74 name : str = await self.broker.get_name_by_code(code)
75 display = f"{name}({code})" if name else code
77 # ▼▼▼▼▼ 핵심 로직 및 로그 수정 ▼▼▼▼▼
78 # 1. 초기 모멘텀 (시가 대비 현재가) 조건 확인
79 initial_momentum_ok = s["change_rate"] >= self.min_change_rate
80 # 2. 추세 지속 (현재가 대비 N분 후 가격) 조건 확인
81 follow_through_ok = s["after_rate"] >= self.min_follow_through
83 is_success = initial_momentum_ok and follow_through_ok
85 # 이제 로그에 각 단계의 실제값과 기준을 명확히 기록합니다.
86 log_initial_rate = f"초기 등락률: {s['change_rate']:.2f}% (기준: {self.min_change_rate}%)"
87 # ▼▼▼▼▼ 여기가 핵심 수정 부분입니다 ▼▼▼▼▼
88 # N분 전/후 가격을 로그에 포함시킵니다.
89 before_price = s['current']
90 after_price = s['after']
91 log_follow_rate = (
92 f"{self.min_follow_through_time}분 후 상승률: {s['after_rate']:.2f}% "
93 f"({before_price:,}원 → {after_price:,}원, 기준: {self.min_follow_through}%)"
94 )
95 # ▲▲▲▲▲ 여기가 핵심 수정 부분입니다 ▲▲▲▲▲
96 if is_success:
97 follow_through.append({"code": code, "name": name})
98 self.logger.info(
99 f"[성공] 종목: {display} | {log_initial_rate} | {log_follow_rate}"
100 )
101 else:
102 failure_reasons = []
103 if not initial_momentum_ok:
104 failure_reasons.append("초기 등락률 미달")
105 if not follow_through_ok:
106 failure_reasons.append("추세 지속 실패")
108 reason_str = " & ".join(failure_reasons)
109 not_follow_through.append({"code": code, "name": name})
110 self.logger.info(
111 f"[실패] 종목: {display} | 사유: {reason_str} | {log_initial_rate} | {log_follow_rate}"
112 )
113 # ▲▲▲▲▲ 핵심 로직 및 로그 수정 ▲▲▲▲▲
115 total = len(results)
116 success = len(follow_through)
117 fail = len(not_follow_through)
118 success_rate = (success / total * 100) if total > 0 else 0
120 self.logger.info(
121 f"[결과 요약] 총 종목: {total}, 성공: {success}, 실패: {fail}, 성공률: {success_rate:.2f}%, 모드: {self.mode}"
122 )
124 return {
125 "follow_through": follow_through, # [{"code": "005930", "name": "삼성전자"}, ...]
126 "not_follow_through": not_follow_through
127 }