Files
auto-stock/backend/app/services/short_term_strategy.py

209 lines
6.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
15분봉 단기 전략 (CLAUDE.md 섹션 8.2)
전체 자본의 최대 25% 이내에서 운용.
일봉 RSI ≤ 40 관심 종목에 한해 15분봉 신호를 감지한다.
⚠️ 슬리피지 주의: 수수료 0.3% + 슬리피지 0.2% 포함 백테스트 필수
"""
import logging
from dataclasses import dataclass
from enum import Enum
from typing import List, Optional
from .mean_reversion_strategy import calculate_rsi, calculate_bollinger
logger = logging.getLogger(__name__)
# 자본 비율 상한 (CLAUDE.md 섹션 8.2 / 8.3)
MAX_CAPITAL_RATIO = 0.25
# 비용 가정 — 백테스트 시 왕복으로 적용
COMMISSION_RATE = 0.003 # 수수료 0.3%
SLIPPAGE_RATE = 0.002 # 슬리피지 0.2%
TOTAL_COST_RATE = COMMISSION_RATE + SLIPPAGE_RATE
class TimeFrame(str, Enum):
M15 = "15m"
H1 = "60m"
@dataclass
class EntrySignal:
ticker: str
timeframe: TimeFrame
reason: str
rsi: float
close: float
@dataclass
class ExitSignal:
action: str # "SELL_ALL"
reason: str
sell_pct: float # 1.0 = 전량
class ShortTermStrategy:
"""15분봉 단기 평균회귀 전략
진입 조건 (모두 충족 필요):
1. 일봉 RSI(14) ≤ 40 — 관심 종목 사전 필터
2. 15분봉 RSI(14) ≤ 30
3. 15분봉 종가 ≤ BB 하단 × 1.01
4. 15분봉 종가 > MA5 — Fake-out 방지
5. 15분봉 거래량 > 거래량 MA20 × 1.3
청산 조건 (둘 중 하나 충족):
- 15분봉 RSI(14) ≥ 55
- 수익률 ≥ 1.5 × ATR_15m_pct
자본 제한:
- 이 전략 배분 총액이 전체 자본 × 25% 초과 시 신규 진입 차단
- MDD -10% 돌파 시 즉시 중단 (호출 측에서 판단)
"""
TIMEFRAME = TimeFrame.M15
DAILY_RSI_THRESHOLD = 40.0 # 일봉 사전 필터
M15_RSI_ENTRY = 30.0 # 15분봉 진입 RSI
M15_RSI_EXIT = 55.0 # 15분봉 청산 RSI
VOLUME_MULTIPLIER = 1.3 # 거래량 배수
def __init__(self, max_capital_ratio: float = MAX_CAPITAL_RATIO):
self.max_capital_ratio = max_capital_ratio
# ------------------------------------------------------------------
# Public API
# ------------------------------------------------------------------
def entry_signal(
self,
ticker: str,
daily_rsi: float,
m15_closes: List[float],
m15_volumes: List[int],
current_volume: int,
) -> Optional[EntrySignal]:
"""15분봉 진입 신호 감지
Args:
ticker: 종목코드
daily_rsi: 일봉 RSI(14)
m15_closes: 15분봉 종가 리스트 (최소 25개)
m15_volumes: 15분봉 거래량 리스트 (최소 20개)
current_volume: 현재 봉 거래량
Returns:
EntrySignal if 모든 조건 충족, else None
"""
# 조건 1: 일봉 사전 필터
if daily_rsi > self.DAILY_RSI_THRESHOLD:
return None
if len(m15_closes) < 25 or len(m15_volumes) < 20:
return None
close = m15_closes[-1]
# 조건 2: 15분봉 RSI
rsi_15m = calculate_rsi(m15_closes, 14)
if rsi_15m is None or rsi_15m > self.M15_RSI_ENTRY:
return None
# 조건 3: BB 하단 근처
bb = calculate_bollinger(m15_closes, 20)
if bb is None:
return None
_, _, bb_lower = bb
if close > bb_lower * 1.01:
return None
# 조건 4: MA5 상향 (Fake-out 방지)
ma5 = sum(m15_closes[-5:]) / 5
if close <= ma5:
return None
# 조건 5: 거래량 급증
vol_ma20 = sum(m15_volumes[-20:]) / 20
if vol_ma20 == 0 or current_volume <= vol_ma20 * self.VOLUME_MULTIPLIER:
return None
return EntrySignal(
ticker=ticker,
timeframe=self.TIMEFRAME,
reason=(
f"15분봉 RSI({rsi_15m:.1f})≤30 + BB하단 + MA5돌파 "
f"+ 거래량({current_volume / vol_ma20:.1f}x)"
),
rsi=round(rsi_15m, 2),
close=close,
)
def exit_signal(
self,
ticker: str,
m15_closes: List[float],
entry_price: float,
atr_15m: Optional[float] = None,
) -> Optional[ExitSignal]:
"""15분봉 청산 신호
Args:
ticker: 종목코드
m15_closes: 15분봉 종가 리스트
entry_price: 진입 가격
atr_15m: 15분봉 ATR (None 이면 ATR 조건 스킵)
Returns:
ExitSignal if 청산 조건 충족, else None
"""
if len(m15_closes) < 15:
return None
close = m15_closes[-1]
rsi_15m = calculate_rsi(m15_closes, 14)
# 청산 조건 1: RSI ≥ 55 회복
if rsi_15m is not None and rsi_15m >= self.M15_RSI_EXIT:
return ExitSignal(
action="SELL_ALL",
reason=f"15분봉 RSI({rsi_15m:.1f})≥55 회복",
sell_pct=1.0,
)
# 청산 조건 2: 수익률 ≥ 1.5 × ATR_15m_pct
if atr_15m and entry_price > 0:
profit_pct = (close - entry_price) / entry_price
atr_pct = atr_15m / entry_price
if profit_pct >= 1.5 * atr_pct:
return ExitSignal(
action="SELL_ALL",
reason=(
f"익절 수익률({profit_pct * 100:.1f}%) "
f"≥ 1.5×ATR({atr_pct * 100:.1f}%)"
),
sell_pct=1.0,
)
return None
def is_capital_available(
self, total_assets: float, current_short_term_allocation: float
) -> bool:
"""단기 전략 자본 한도 초과 여부 확인
CLAUDE.md 섹션 8.3: 단기 전략에 최대 25% 배분.
MDD -10% 돌파 시에는 호출 측에서 별도로 차단해야 한다.
"""
limit = total_assets * self.max_capital_ratio
return current_short_term_allocation < limit
@staticmethod
def net_profit_pct(gross_profit_pct: float) -> float:
"""수수료 + 슬리피지 차감 후 순수익률 (왕복 기준)
백테스트 결과 검증 시 이 함수로 실질 수익률을 계산하라.
"""
return gross_profit_pct - TOTAL_COST_RATE * 2