candle_count 및 tf_map 수정, OHLCV 데이터 무결성 테스트 추가
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"symbols_file": "config/symbols.txt",
|
||||
"symbol_delay": 1.0,
|
||||
"candle_count": 200,
|
||||
"candle_count": 201,
|
||||
"buy_check_interval_minutes": 240,
|
||||
"stop_loss_check_interval_minutes": 60,
|
||||
"profit_taking_check_interval_minutes": 240,
|
||||
|
||||
@@ -20,6 +20,8 @@ class DataFetchError(Exception):
|
||||
|
||||
|
||||
# OHLCV 데이터 캐시 (TTL 5분)
|
||||
# ⚠️ 메모리 캐시: 프로그램 재시작 시 자동 초기화됨
|
||||
# tf_map 수정 후에는 반드시 프로그램을 재시작하여 오염된 캐시를 제거해야 함
|
||||
_ohlcv_cache = {}
|
||||
_cache_lock = threading.RLock() # 캐시 동시 접근 보호 (재진입 가능)
|
||||
CACHE_TTL = 300 # 5분
|
||||
@@ -73,14 +75,24 @@ def fetch_ohlcv(
|
||||
_clean_expired_cache()
|
||||
|
||||
_buf("debug", f"[CACHE MISS] OHLCV 수집 시작: {symbol} {timeframe}")
|
||||
|
||||
# ⚠️ CRITICAL: main.py의 minutes_to_timeframe()이 반환하는 형식과 일치해야 함
|
||||
# minutes_to_timeframe()은 "1m", "3m", "10m", "60m", "240m" 등을 반환
|
||||
# pyupbit는 "minute1", "minute240" 형식을 필요로 함
|
||||
tf_map = {
|
||||
# 분봉 (Upbit 지원: 1, 3, 5, 10, 15, 30, 60, 240분)
|
||||
"1m": "minute1",
|
||||
"3m": "minute3",
|
||||
"5m": "minute5",
|
||||
"10m": "minute10", # main.py에서 사용 가능
|
||||
"15m": "minute15",
|
||||
"30m": "minute30",
|
||||
"60m": "minute60", # main.py에서 사용 (1시간봉)
|
||||
"240m": "minute240", # ⚠️ 핵심 수정: main.py에서 4시간봉으로 사용
|
||||
# 시간 단위 별칭 (호환성)
|
||||
"1h": "minute60",
|
||||
"4h": "minute240",
|
||||
# 일봉/주봉
|
||||
"1d": "day",
|
||||
"1w": "week",
|
||||
}
|
||||
|
||||
157
tests/test_timeframe_mapping.py
Normal file
157
tests/test_timeframe_mapping.py
Normal file
@@ -0,0 +1,157 @@
|
||||
"""
|
||||
Timeframe 매핑 및 OHLCV 데이터 무결성 테스트.
|
||||
|
||||
이 테스트는 다음 버그를 사전에 발견하기 위해 작성됨:
|
||||
- main.py의 minutes_to_timeframe()과 indicators.py의 tf_map 불일치
|
||||
- candle_count 설정으로 인한 SMA 계산 데이터 부족
|
||||
|
||||
버그 배경:
|
||||
- minutes_to_timeframe()이 "240m"을 반환했으나 tf_map에 매핑이 없어
|
||||
pyupbit에 그대로 전달되어 일봉 데이터가 반환됨 (2025-12-17 발견)
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
class TestTimeframeMappingIntegrity:
|
||||
"""main.py와 indicators.py 간 timeframe 매핑 일관성 테스트."""
|
||||
|
||||
def test_all_minutes_to_timeframe_outputs_are_in_tf_map(self):
|
||||
"""minutes_to_timeframe()이 반환 가능한 모든 값이 tf_map에 매핑되어 있어야 함.
|
||||
|
||||
이 테스트가 실패하면:
|
||||
- indicators.py의 tf_map에 누락된 timeframe 매핑이 있음
|
||||
- 해당 timeframe 사용 시 잘못된 데이터가 반환될 수 있음
|
||||
"""
|
||||
from main import minutes_to_timeframe
|
||||
|
||||
# indicators.py에서 tf_map 추출
|
||||
# (함수 내부에 있으므로 직접 정의 - 동기화 필요)
|
||||
tf_map = {
|
||||
"1m": "minute1",
|
||||
"3m": "minute3",
|
||||
"5m": "minute5",
|
||||
"10m": "minute10",
|
||||
"15m": "minute15",
|
||||
"30m": "minute30",
|
||||
"60m": "minute60",
|
||||
"240m": "minute240",
|
||||
"1h": "minute60",
|
||||
"4h": "minute240",
|
||||
"1d": "day",
|
||||
"1w": "week",
|
||||
}
|
||||
|
||||
# Upbit이 지원하는 분봉 + 일봉 입력값
|
||||
test_minutes = [1, 3, 5, 10, 15, 30, 60, 240, 1440]
|
||||
|
||||
missing_mappings = []
|
||||
for minutes in test_minutes:
|
||||
timeframe = minutes_to_timeframe(minutes)
|
||||
if timeframe not in tf_map:
|
||||
missing_mappings.append((minutes, timeframe))
|
||||
|
||||
if missing_mappings:
|
||||
error_msg = f"tf_map에 누락된 매핑: {missing_mappings}"
|
||||
pytest.fail(error_msg)
|
||||
|
||||
def test_edge_case_timeframes_are_mapped(self):
|
||||
"""비표준 분봉 입력도 tf_map에 매핑된 값으로 변환되어야 함.
|
||||
|
||||
예: 120분 → 60m (근사값), 300분 → 240m
|
||||
"""
|
||||
from main import minutes_to_timeframe
|
||||
|
||||
tf_map = {
|
||||
"1m": "minute1",
|
||||
"3m": "minute3",
|
||||
"5m": "minute5",
|
||||
"10m": "minute10",
|
||||
"15m": "minute15",
|
||||
"30m": "minute30",
|
||||
"60m": "minute60",
|
||||
"240m": "minute240",
|
||||
"1h": "minute60",
|
||||
"4h": "minute240",
|
||||
"1d": "day",
|
||||
"1w": "week",
|
||||
}
|
||||
|
||||
# 비표준 분봉 → 근사 변환 → tf_map에 있어야 함
|
||||
edge_cases = [2, 7, 45, 90, 120, 180, 300, 500]
|
||||
|
||||
for minutes in edge_cases:
|
||||
timeframe = minutes_to_timeframe(minutes)
|
||||
if timeframe not in tf_map:
|
||||
pytest.fail(f"minutes_to_timeframe({minutes}) = '{timeframe}'가 tf_map에 없음")
|
||||
|
||||
|
||||
class TestCandleCountConfiguration:
|
||||
"""candle_count 설정 관련 테스트."""
|
||||
|
||||
def test_candle_count_sufficient_for_sma200(self):
|
||||
"""candle_count가 SMA200 계산에 충분해야 함.
|
||||
|
||||
signals.py에서 미완성 봉 1개를 제외하므로,
|
||||
SMA200 계산에는 최소 201개가 필요함.
|
||||
"""
|
||||
from src.config import load_config
|
||||
|
||||
config = load_config()
|
||||
candle_count = config.get("candle_count", 200)
|
||||
sma_long = config.get("sma_long", 200)
|
||||
|
||||
# 미완성 봉 1개 제외 후에도 SMA 계산 가능해야 함
|
||||
effective_candles = candle_count - 1
|
||||
|
||||
if effective_candles < sma_long:
|
||||
error_msg = (
|
||||
f"candle_count={candle_count}, effective={effective_candles}, "
|
||||
f"sma_long={sma_long}. config.json의 candle_count를 {sma_long + 1}로 설정하세요."
|
||||
)
|
||||
pytest.fail(error_msg)
|
||||
|
||||
|
||||
class TestOHLCVDataInterval:
|
||||
"""OHLCV 데이터 간격 무결성 테스트."""
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"timeframe,expected_minutes",
|
||||
[
|
||||
("1m", 1),
|
||||
("5m", 5),
|
||||
("15m", 15),
|
||||
("60m", 60),
|
||||
("240m", 240), # 핵심: 4시간 = 240분
|
||||
("1h", 60),
|
||||
("4h", 240),
|
||||
],
|
||||
)
|
||||
def test_tf_map_produces_correct_pyupbit_interval(self, timeframe, expected_minutes):
|
||||
"""tf_map 변환 결과가 pyupbit 형식에 맞고, 올바른 간격을 의미해야 함."""
|
||||
tf_map = {
|
||||
"1m": "minute1",
|
||||
"3m": "minute3",
|
||||
"5m": "minute5",
|
||||
"10m": "minute10",
|
||||
"15m": "minute15",
|
||||
"30m": "minute30",
|
||||
"60m": "minute60",
|
||||
"240m": "minute240",
|
||||
"1h": "minute60",
|
||||
"4h": "minute240",
|
||||
"1d": "day",
|
||||
"1w": "week",
|
||||
}
|
||||
|
||||
pyupbit_interval = tf_map.get(timeframe)
|
||||
assert pyupbit_interval is not None, f"tf_map에 '{timeframe}' 매핑 누락"
|
||||
|
||||
# pyupbit 형식 검증: "minuteN" 또는 "day"/"week"
|
||||
if timeframe not in ("1d", "1w"):
|
||||
if not pyupbit_interval.startswith("minute"):
|
||||
pytest.fail(f"분봉 timeframe '{timeframe}'의 pyupbit 형식 오류: {pyupbit_interval}")
|
||||
|
||||
actual_minutes = int(pyupbit_interval.replace("minute", ""))
|
||||
if actual_minutes != expected_minutes:
|
||||
pytest.fail(f"'{timeframe}' 간격 불일치: 예상={expected_minutes}분, 실제={actual_minutes}분")
|
||||
Reference in New Issue
Block a user