레디스 및 구조 수정중

This commit is contained in:
2026-02-25 16:56:34 +09:00
parent 89f54bae55
commit ec02f27200
10 changed files with 428 additions and 131 deletions

View File

@@ -11,34 +11,43 @@
- 일일 손실 한도: 전체 자본 -3% 초과 시 신규 진입 중단
- 동시 최대 3종목, 종목당 최대 10% 자본
- 전체 데이트레이딩 자본: 총자산의 최대 25% (CLAUDE.md 8.2)
- 수수료+슬리피지 포함 기준: 0.5% (매수 0.015% + 매도 0.3% + 슬리피지 0.2%)
[.env 설정]
DAY_TRADE_MAX_CAPITAL_RATIO
DAY_TRADE_MAX_RATIO_DT
DAY_TRADE_MAX_POSITIONS (== MAX_DAY_TRADE_POSITIONS)
DAY_TRADE_DAILY_LOSS_LIMIT
DAY_TRADE_ORB_STOP_LOSS_PCT
DAY_TRADE_REVERSION_STOP_LOSS_PCT
DAY_TRADE_TRAILING_TRIGGER
DAY_TRADE_TRAILING_ATR_MULT
DAY_TRADE_ORB_BREAKOUT_MARGIN
DAY_TRADE_COST_PCT
DAY_TRADE_ENTRY_CUTOFF_HOUR, DAY_TRADE_ENTRY_CUTOFF_MIN
DAY_TRADE_EOD_HOUR, DAY_TRADE_EOD_MIN
"""
import logging
from typing import Optional, Dict, List, Tuple
from dataclasses import dataclass
from datetime import time
from app.core.config import settings
logger = logging.getLogger(__name__)
# ── 장 시간 상수 (KST) ──────────────────────────────────────────────────────
MARKET_OPEN = time(9, 0)
ORB_END = time(9, 30) # Opening Range 집계 종료
ENTRY_CUTOFF = time(15, 0) # 신규 진입 마감 (이후는 청산만)
EOD_FLAT = time(15, 20) # EOD 강제 청산 시작
MARKET_CLOSE = time(15, 30)
# ── 리스크 파라미터 ───────────────────────────────────────────────────────────
MAX_CAPITAL_RATIO = 0.10 # 종목당 최대 10%
MAX_DAY_TRADE_RATIO = 0.25 # 전체 데이트레이딩 최대 25%
MAX_POSITIONS = 3 # 동시 최대 3종목
DAILY_LOSS_LIMIT = -0.03 # 일일 손실 한도 -3%
STOP_LOSS_PCT = 0.02 # ORB 손절 -2%
STOP_LOSS_PCT_REVERSION = 0.025 # VWAP/RSI_BB 평균회귀 전략 손절 -2.5%
# ↑ ORB(-2%)보다 약간 넓게 잡되, 일일 손실 한도(-3%) 이내에서 단일 포지션이 소화 가능하도록 설정
TRAILING_TRIGGER = 0.015 # 트레일링 발동 +1.5%
TRAILING_ATR_MULT = 1.0 # 트레일링 ATR 배수
ORB_BREAKOUT_MARGIN = 0.002 # ORB 돌파 최소 여유 +0.2%
COST_PCT = 0.005 # 수수료+슬리피지 합산 (매수+매도)
# 동적 시간 설정 (settings에서 읽음)
def _get_entry_cutoff() -> time:
"""신규 진입 마감 시간 (.env: DAY_TRADE_ENTRY_CUTOFF_HOUR, DAY_TRADE_ENTRY_CUTOFF_MIN)"""
return time(settings.DAY_TRADE_ENTRY_CUTOFF_HOUR, settings.DAY_TRADE_ENTRY_CUTOFF_MIN)
def _get_eod_flat() -> time:
"""EOD 강제 청산 시작 시간 (.env: DAY_TRADE_EOD_HOUR, DAY_TRADE_EOD_MIN)"""
return time(settings.DAY_TRADE_EOD_HOUR, settings.DAY_TRADE_EOD_MIN)
MARKET_CLOSE = time(15, 30)
# ── 데이터 클래스 ─────────────────────────────────────────────────────────────
@@ -226,7 +235,7 @@ def check_orb_signal(
if not orb or current_price <= 0:
return None
if current_price < orb.high * (1 + ORB_BREAKOUT_MARGIN):
if current_price < orb.high * (1 + settings.DAY_TRADE_ORB_BREAKOUT_MARGIN):
return None
# 최근 1분봉 거래량 체크
@@ -243,7 +252,7 @@ def check_orb_signal(
if candles_5m[-2].close < candles_5m[-2].open:
return None # 직전 5분봉 음봉 → 스킵
stop_loss = max(orb.low, current_price * (1 - STOP_LOSS_PCT))
stop_loss = max(orb.low, current_price * (1 - settings.DAY_TRADE_ORB_STOP_LOSS_PCT))
risk = current_price - stop_loss
# 수수료 고려: 목표 = 진입가 + 2R + 수수료 보전
target = current_price + max(risk * 2.0, current_price * 0.03)
@@ -302,8 +311,8 @@ def check_vwap_bounce_signal(
return None
atr = _atr(candles_5min, 14)
raw_stop = (current_price - 1.5 * atr) if atr else current_price * (1 - STOP_LOSS_PCT)
stop_loss = max(raw_stop, current_price * (1 - STOP_LOSS_PCT_REVERSION), 1.0) # 하드 스탑 -2.5% (일일 손실 한도 -3% 이내)
raw_stop = (current_price - 1.5 * atr) if atr else current_price * (1 - settings.DAY_TRADE_ORB_STOP_LOSS_PCT)
stop_loss = max(raw_stop, current_price * (1 - settings.DAY_TRADE_REVERSION_STOP_LOSS_PCT), 1.0) # 하드 스탑 -2.5% (일일 손실 한도 -3% 이내)
target = (current_price + 2.0 * atr) if atr else vwap * 1.03
return DayTradeSignal(
@@ -360,8 +369,8 @@ def check_rsi_bb_signal(
return None
atr = _atr(candles_5min, 14)
raw_stop = (current_price - 1.5 * atr) if atr else current_price * (1 - STOP_LOSS_PCT)
stop_loss = max(raw_stop, current_price * (1 - STOP_LOSS_PCT_REVERSION), 1.0) # 하드 스탑 -2.5% (일일 손실 한도 -3% 이내)
raw_stop = (current_price - 1.5 * atr) if atr else current_price * (1 - settings.DAY_TRADE_ORB_STOP_LOSS_PCT)
stop_loss = max(raw_stop, current_price * (1 - settings.DAY_TRADE_REVERSION_STOP_LOSS_PCT), 1.0) # 하드 스탑 -2.5% (일일 손실 한도 -3% 이내)
# 최소 1.5R 보장: bb_middle이 너무 가까우면 R:R 확보를 위해 목표 상향
risk = current_price - stop_loss
target = max(bb_middle, current_price + risk * 1.5)
@@ -403,10 +412,10 @@ def check_day_trade_exit(
5. 5분봉 RSI ≥ 70 (과매수 청산)
"""
# 1. EOD 강제 청산
if current_time >= EOD_FLAT:
if current_time >= _get_eod_flat():
return DayTradeExit(
action="SELL_ALL",
reason=f"EOD 강제 청산 ({current_time.strftime('%H:%M')} ≥ 15:20)",
reason=f"EOD 강제 청산 ({current_time.strftime('%H:%M')} ≥ {_get_eod_flat().strftime('%H:%M')})",
)
# 2. 손절
@@ -427,10 +436,10 @@ def check_day_trade_exit(
# 4. 트레일링 스탑 (1.5% 이상 수익 시 발동)
profit_pct = (highest_since_entry - entry_price) / entry_price
if profit_pct >= TRAILING_TRIGGER:
if profit_pct >= settings.DAY_TRADE_TRAILING_TRIGGER:
atr = _atr(candles_5min, 14)
if atr:
trailing_stop = highest_since_entry - TRAILING_ATR_MULT * atr
trailing_stop = highest_since_entry - settings.DAY_TRADE_TRAILING_ATR_MULT * atr
else:
trailing_stop = highest_since_entry * (1 - 0.02) # fallback: 최고가 -2% (5분봉 데이터 부족 시)
if current_price <= trailing_stop:
@@ -446,7 +455,7 @@ def check_day_trade_exit(
rsi = _rsi(candles_5min, 14)
if rsi is not None and rsi >= 70:
gain_pct = (current_price - entry_price) / entry_price * 100
if gain_pct > COST_PCT * 100: # 수수료 이상 수익인 경우만
if gain_pct > settings.DAY_TRADE_COST_PCT * 100: # 수수료 이상 수익인 경우만
return DayTradeExit(
action="SELL_ALL",
reason=f"과매수 청산: RSI={rsi:.1f}≥70, 수익={gain_pct:.1f}%",