182 lines
6.3 KiB
Python
182 lines
6.3 KiB
Python
"""
|
|
Day Trading Strategy (Refactored)
|
|
|
|
기존 day_trading_strategy.py를 BaseStrategy 구조로 변환
|
|
|
|
전략:
|
|
- ORB (Opening Range Breakout)
|
|
- VWAP Bounce
|
|
- RSI+BB 과매도 반등
|
|
"""
|
|
import logging
|
|
from typing import Optional, Dict, Any
|
|
from datetime import datetime, time
|
|
import pytz
|
|
|
|
from app.strategy.base_strategy import BaseStrategy
|
|
from app.strategy.signal import Signal, SignalType
|
|
from app.core.config import settings
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class DayTradingStrategy(BaseStrategy):
|
|
"""
|
|
데이트레이딩 전략 (단기 스칼핑)
|
|
|
|
09:00~15:20 간 급등/급락 모멘텀 포착
|
|
"""
|
|
|
|
DEFAULT_CONFIG = {
|
|
"max_positions": 3,
|
|
"max_capital_ratio": 0.1, # 종목당 10%
|
|
"orb_breakout_margin": 0.002, # 0.2% 돌파
|
|
"stop_loss_pct": 0.025, # -2.5%
|
|
"entry_cutoff": "14:30", # 신규 진입 마감
|
|
"eod_flat": "15:20", # EOD 청산
|
|
}
|
|
|
|
def __init__(self, name: str = "DayTrading", config: Optional[Dict[str, Any]] = None):
|
|
merged_config = {**self.DEFAULT_CONFIG, **(config or {})}
|
|
super().__init__(name, merged_config)
|
|
|
|
self.kst = pytz.timezone("Asia/Seoul")
|
|
self.entry_cutoff = self._parse_time(self.config["entry_cutoff"])
|
|
self.eod_flat = self._parse_time(self.config["eod_flat"])
|
|
|
|
def _parse_time(self, time_str: str) -> time:
|
|
"""문자열 "HH:MM" → time 객체"""
|
|
h, m = map(int, time_str.split(":"))
|
|
return time(h, m)
|
|
|
|
async def analyze(
|
|
self,
|
|
stock_code: str,
|
|
market_data: Dict[str, Any],
|
|
portfolio_data: Optional[Dict[str, Any]] = None
|
|
) -> Optional[Signal]:
|
|
"""
|
|
시장 데이터 분석
|
|
|
|
Args:
|
|
market_data: {
|
|
"current_price": float,
|
|
"minute_chart": List[dict], # 1분봉
|
|
"orderbook": dict,
|
|
"stock_name": str,
|
|
}
|
|
portfolio_data: {
|
|
"holding_quantity": int,
|
|
"avg_price": float,
|
|
}
|
|
"""
|
|
current_time = datetime.now(self.kst).time()
|
|
|
|
# 1. 시간 체크
|
|
if current_time >= self.eod_flat:
|
|
# EOD 청산 신호
|
|
if portfolio_data and portfolio_data.get("holding_quantity", 0) > 0:
|
|
return Signal(
|
|
stock_code=stock_code,
|
|
signal_type=SignalType.SELL,
|
|
strategy_name=self.name,
|
|
timestamp=datetime.now(self.kst),
|
|
quantity=portfolio_data["holding_quantity"],
|
|
price=None, # 시장가
|
|
confidence=1.0,
|
|
reason="EOD 강제 청산"
|
|
)
|
|
return None
|
|
|
|
if current_time >= self.entry_cutoff:
|
|
# 신규 진입 차단
|
|
return None
|
|
|
|
# 2. 현재 보유 중이면 청산 조건 체크
|
|
if portfolio_data and portfolio_data.get("holding_quantity", 0) > 0:
|
|
exit_signal = self._check_exit(
|
|
stock_code,
|
|
market_data,
|
|
portfolio_data
|
|
)
|
|
if exit_signal:
|
|
return exit_signal
|
|
return None # 보유 유지
|
|
|
|
# 3. 신규 진입 검토
|
|
return self._check_entry(stock_code, market_data)
|
|
|
|
def _check_entry(self, stock_code: str, market_data: Dict[str, Any]) -> Optional[Signal]:
|
|
"""신규 진입 신호 체크"""
|
|
current_price = market_data.get("current_price")
|
|
minute_chart = market_data.get("minute_chart", [])
|
|
|
|
if not current_price or not minute_chart:
|
|
return None
|
|
|
|
# 예시: ORB 전략 (간단화)
|
|
# 실제는 기존 check_orb_signal() 등을 호출
|
|
|
|
# 임시 구현: 단순 예시
|
|
if len(minute_chart) >= 30: # 09:00~09:30 데이터 있음
|
|
orb_high = max(c.get("high", 0) for c in minute_chart[:30])
|
|
|
|
if current_price > orb_high * (1 + self.config["orb_breakout_margin"]):
|
|
stop_loss = current_price * (1 - self.config["stop_loss_pct"])
|
|
target = current_price * 1.03 # +3% 목표
|
|
|
|
return Signal(
|
|
stock_code=stock_code,
|
|
signal_type=SignalType.BUY,
|
|
strategy_name=self.name,
|
|
timestamp=datetime.now(self.kst),
|
|
quantity=None, # OrderManager가 계산
|
|
price=current_price,
|
|
confidence=0.75,
|
|
stop_loss_price=stop_loss,
|
|
take_profit_price=target,
|
|
reason=f"ORB 돌파: {orb_high:.0f} → {current_price:.0f}",
|
|
metadata={"strategy_type": "ORB"}
|
|
)
|
|
|
|
return None
|
|
|
|
def _check_exit(self, stock_code: str, market_data: Dict[str, Any], portfolio_data: Dict[str, Any]) -> Optional[Signal]:
|
|
"""청산 신호 체크"""
|
|
current_price = market_data.get("current_price")
|
|
avg_price = portfolio_data.get("avg_price")
|
|
quantity = portfolio_data.get("holding_quantity")
|
|
|
|
if not current_price or not avg_price or not quantity:
|
|
return None
|
|
|
|
# 손절 체크
|
|
stop_loss = avg_price * (1 - self.config["stop_loss_pct"])
|
|
if current_price <= stop_loss:
|
|
return Signal(
|
|
stock_code=stock_code,
|
|
signal_type=SignalType.STOP_LOSS,
|
|
strategy_name=self.name,
|
|
timestamp=datetime.now(self.kst),
|
|
quantity=quantity,
|
|
price=None, # 시장가
|
|
confidence=1.0,
|
|
reason=f"손절: {current_price:.0f} ≤ {stop_loss:.0f}"
|
|
)
|
|
|
|
# 익절 체크
|
|
target = avg_price * 1.03
|
|
if current_price >= target:
|
|
return Signal(
|
|
stock_code=stock_code,
|
|
signal_type=SignalType.TAKE_PROFIT,
|
|
strategy_name=self.name,
|
|
timestamp=datetime.now(self.kst),
|
|
quantity=quantity,
|
|
price=None,
|
|
confidence=1.0,
|
|
reason=f"목표가 도달: {current_price:.0f} ≥ {target:.0f}"
|
|
)
|
|
|
|
return None
|