박재오 결정 2026-05-19 — V2를 정식 명칭 ai_trade로 graduation, V1은 deprecated 마킹 (legacy 디렉토리 이동은 file lock 풀린 후 후속). 변경 사항: - signal_v2/ → ai_trade/ (git mv, import 일괄 sed: signal_v2.x → ai_trade.x) - root start.bat → legacy/start_v1.bat (V1 자동 시작 차단) - ai_trade/start.bat 내부 uvicorn target signal_v2.main → ai_trade.main - signal_v1/DEPRECATED.md 추가 (사용 금지 명시) - CLAUDE.md 디렉토리 표·서버 시작 방식 갱신 - services/ 디렉토리 미래 예정 (Plan-B-Insta 작업 시 신설) ai_trade tests 59/59 PASS 확인. signal_v1/ 디렉토리 자체 이동(legacy/signal_v1/)은 telegram_bot.log + data/news_snapshots.db file lock으로 보류. lock 해제 후 후속 커밋. 후속 작업: Plan-B-Insta (services/insta-render + NAS insta 분할) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
70 lines
2.3 KiB
Python
70 lines
2.3 KiB
Python
"""분봉 OHLCV → 5-level 모멘텀 분류."""
|
|
from __future__ import annotations
|
|
from collections import deque
|
|
|
|
# 분류 카테고리
|
|
STRONG_UP = "strong_up"
|
|
WEAK_UP = "weak_up"
|
|
NEUTRAL = "neutral"
|
|
WEAK_DOWN = "weak_down"
|
|
STRONG_DOWN = "strong_down"
|
|
|
|
_BARS_PER_5MIN = 5
|
|
_LOOKBACK_5MIN_BARS = 5
|
|
_VOLUME_AVG_WINDOW = 12 # 60분 = 5분봉 12개
|
|
|
|
|
|
def aggregate_1min_to_5min(minute_bars: list[dict]) -> list[dict]:
|
|
"""1분봉 N개 → 5분봉 floor(N/5) 개. 시간 오름차순.
|
|
|
|
각 5분봉: open=첫 1분봉 open, high=max, low=min, close=마지막 close, volume=sum.
|
|
"""
|
|
bars_5min = []
|
|
chunks = len(minute_bars) // _BARS_PER_5MIN
|
|
for i in range(chunks):
|
|
chunk = minute_bars[i * _BARS_PER_5MIN : (i + 1) * _BARS_PER_5MIN]
|
|
bars_5min.append({
|
|
"datetime": chunk[0]["datetime"],
|
|
"open": chunk[0]["open"],
|
|
"high": max(b["high"] for b in chunk),
|
|
"low": min(b["low"] for b in chunk),
|
|
"close": chunk[-1]["close"],
|
|
"volume": sum(b["volume"] for b in chunk),
|
|
})
|
|
return bars_5min
|
|
|
|
|
|
def classify_minute_momentum(minute_bars: deque) -> str:
|
|
"""1분봉 deque → 5-level 모멘텀 분류.
|
|
|
|
Returns: STRONG_UP / WEAK_UP / NEUTRAL / WEAK_DOWN / STRONG_DOWN
|
|
"""
|
|
minute_list = list(minute_bars)
|
|
if len(minute_list) < _BARS_PER_5MIN * _LOOKBACK_5MIN_BARS:
|
|
return NEUTRAL # 데이터 부족
|
|
|
|
bars_5min = aggregate_1min_to_5min(minute_list)
|
|
if len(bars_5min) < _LOOKBACK_5MIN_BARS:
|
|
return NEUTRAL
|
|
|
|
recent = bars_5min[-_LOOKBACK_5MIN_BARS:]
|
|
up_count = sum(1 for b in recent if b["close"] > b["open"])
|
|
|
|
# 거래량 multiplier: recent 5 avg vs 60분 avg
|
|
recent_vol_avg = sum(b["volume"] for b in recent) / len(recent)
|
|
long_window = bars_5min[-_VOLUME_AVG_WINDOW:]
|
|
long_vol_avg = sum(b["volume"] for b in long_window) / len(long_window)
|
|
vol_mult = recent_vol_avg / long_vol_avg if long_vol_avg > 0 else 1.0
|
|
|
|
# 5-level 분류
|
|
if up_count == 5 and vol_mult >= 1.5:
|
|
return STRONG_UP
|
|
elif up_count >= 3 and vol_mult >= 1.0:
|
|
return WEAK_UP
|
|
elif up_count == 0 and vol_mult >= 1.5:
|
|
return STRONG_DOWN
|
|
elif up_count <= 2 and vol_mult < 1.0:
|
|
return WEAK_DOWN
|
|
else:
|
|
return NEUTRAL
|