[2026-05-19] 세션 분리 + L3→B안 전환 + /midday 장중 분석 추가
- L3 하드 중단 제거 → B안(연속 손절별 포지션 축소) 적용 0회×1.0 / 1회×0.7 / 2회×0.5 / 3+회×0.3, 익절 시 한 단계 회복 - 아침·점심 세션 분리: 11:00 이후 midday_context.json 감지 시 점심 세션 자동 시작 (12:00 고정 시작 제거 → 이벤트 기반) - app/ai/midday.py: 장중 데이터 수집 스크립트 신규 작성 - .claude/commands/midday.md: /midday 슬래시 커맨드 신규 작성 - scripts/run_midday.ps1: 11:20 스케줄러 스크립트 신규 작성 - setup_scheduler.ps1: StockBot_Midday 태스크 추가 - CLAUDE.md: 전체 문서 업데이트 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+80
-9
@@ -12,6 +12,7 @@ main.py
|
||||
DRY_RUN=true → 신호만 확인, 주문 전송 안 함
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import asyncio
|
||||
@@ -86,10 +87,58 @@ class StockBot:
|
||||
self.risk = None # RiskManager (잔고 확인 후 초기화)
|
||||
self.running = False
|
||||
|
||||
# 장중 컨텍스트 (midday_context.json 갱신 감지용)
|
||||
self._midday_ctx_mtime : float = 0.0
|
||||
self._midday_pos_mult : float = 1.0 # midday position_size_multiplier
|
||||
self._midday_loaded : bool = False
|
||||
|
||||
mode = "모의투자" if self.kis.is_mock else "실거래"
|
||||
dry = " [DRY_RUN]" if os.getenv("DRY_RUN","true")=="true" else ""
|
||||
logger.info(f"StockBot 시작 [{mode}]{dry}")
|
||||
|
||||
# ─────────────────────────────────────────
|
||||
# 장중 컨텍스트 감시
|
||||
# ─────────────────────────────────────────
|
||||
|
||||
def _check_midday_context(self):
|
||||
"""midday_context.json 갱신 감지 → 즉시 점심 세션 파라미터 반영"""
|
||||
path = Path("data/midday_context.json")
|
||||
if not path.exists():
|
||||
return
|
||||
try:
|
||||
mtime = path.stat().st_mtime
|
||||
except OSError:
|
||||
return
|
||||
if mtime <= self._midday_ctx_mtime:
|
||||
return
|
||||
try:
|
||||
ctx = json.loads(path.read_text(encoding="utf-8"))
|
||||
if ctx.get("date") != datetime.now().strftime("%Y-%m-%d"):
|
||||
return
|
||||
if not ctx.get("lunch_trade_allowed", True):
|
||||
logger.warning("midday_context: 점심 세션 진입 중단 설정")
|
||||
self._midday_pos_mult = float(ctx.get("position_size_multiplier", 1.0))
|
||||
# 섹터·블랙리스트 업데이트
|
||||
if "hot_sectors" in ctx:
|
||||
self.strategy.context["hot_sectors"] = ctx["hot_sectors"]
|
||||
if "avoid_sectors" in ctx:
|
||||
self.strategy.context["avoid_sectors"] = ctx["avoid_sectors"]
|
||||
for t in ctx.get("blacklist_tickers", []):
|
||||
bl = self.strategy.context.setdefault("blacklist_tickers", [])
|
||||
if t not in bl:
|
||||
bl.append(t)
|
||||
# lunch_trade_allowed=false이면 진입 자체를 막는 플래그 저장
|
||||
self.strategy.context["lunch_trade_allowed"] = ctx.get("lunch_trade_allowed", True)
|
||||
self._midday_ctx_mtime = mtime
|
||||
self._midday_loaded = True
|
||||
logger.info(
|
||||
f"midday_context 로드 완료 — 점심 세션 시작 "
|
||||
f"(포지션 배율: ×{self._midday_pos_mult}, "
|
||||
f"진입허용: {ctx.get('lunch_trade_allowed', True)})"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"midday_context 로드 실패: {e}")
|
||||
|
||||
# ─────────────────────────────────────────
|
||||
# 초기화
|
||||
# ─────────────────────────────────────────
|
||||
@@ -276,7 +325,7 @@ class StockBot:
|
||||
self.running = False
|
||||
break
|
||||
|
||||
# 14:00 이후 신규 진입 중단 (강제청산 50분 전)
|
||||
# 14:00 이후 신규 진입 중단
|
||||
if now_str > "14:00":
|
||||
await asyncio.sleep(1)
|
||||
continue
|
||||
@@ -286,12 +335,10 @@ class StockBot:
|
||||
await asyncio.sleep(1)
|
||||
continue
|
||||
|
||||
# 점심 (11:30~13:00) 신규 진입 중단
|
||||
if "11:30" <= now_str < "13:00":
|
||||
await asyncio.sleep(1)
|
||||
continue
|
||||
# midday_context.json 갱신 감지 (점심 세션 이벤트 기반 시작)
|
||||
self._check_midday_context()
|
||||
|
||||
# 리스크 체크
|
||||
# 리스크 체크 (L2/L4/L5 하드 중단)
|
||||
if not self.risk.can_trade():
|
||||
await asyncio.sleep(5)
|
||||
continue
|
||||
@@ -311,8 +358,16 @@ class StockBot:
|
||||
|
||||
async def check_entries(self):
|
||||
"""유니버스 전체 진입 신호 확인"""
|
||||
# check_exits 처리 중 14:00을 넘었을 경우 진입 차단
|
||||
if datetime.now().strftime("%H:%M") > "14:00":
|
||||
now_str = datetime.now().strftime("%H:%M")
|
||||
# 14:00 이후 진입 차단
|
||||
if now_str > "14:00":
|
||||
return
|
||||
# midday_context 로드 전(11:20~) 11:00 이후 신규 진입 일시 중단
|
||||
# — midday_context.json이 생성되면 _check_midday_context()가 자동 해제
|
||||
if now_str >= "11:00" and not self._midday_loaded:
|
||||
return
|
||||
# lunch_trade_allowed=false이면 점심 세션 진입 차단
|
||||
if self._midday_loaded and not self.strategy.context.get("lunch_trade_allowed", True):
|
||||
return
|
||||
for ticker in self.universe:
|
||||
if ticker in self.positions:
|
||||
@@ -341,7 +396,13 @@ class StockBot:
|
||||
|
||||
balance = await self.kis.get_balance()
|
||||
cash = balance["cash"]
|
||||
invest = self.risk.get_pos_size(cash, signal.get("multiplier", 1.0))
|
||||
# AI 신호 배율 × B안(연속 손절) 배율 × midday 배율
|
||||
combined_mult = (
|
||||
signal.get("multiplier", 1.0)
|
||||
* self.risk.get_consec_multiplier()
|
||||
* self._midday_pos_mult
|
||||
)
|
||||
invest = self.risk.get_pos_size(cash, combined_mult)
|
||||
qty = max(1, int(invest // current))
|
||||
|
||||
result = await self.executor.buy(
|
||||
@@ -435,6 +496,16 @@ class StockBot:
|
||||
|
||||
self.risk.record_trade(pnl)
|
||||
|
||||
# B안: 연속 손절 2회·3회 도달 시 Discord 알림
|
||||
if reason == "SL":
|
||||
consec = self.risk.consec_loss
|
||||
if consec in (2, 3):
|
||||
mult = self.risk.get_consec_multiplier()
|
||||
await notify_risk(
|
||||
"L3-B",
|
||||
f"{consec}연속 손절 — 포지션 크기 {int(mult * 100)}%로 축소"
|
||||
)
|
||||
|
||||
if reason == "TP1":
|
||||
pos["tp1_done"] = True
|
||||
pos["qty"] -= qty
|
||||
|
||||
Reference in New Issue
Block a user