Compare commits
2 Commits
9c63d04eac
...
a7fd6e8564
| Author | SHA1 | Date | |
|---|---|---|---|
| a7fd6e8564 | |||
| 9fd02932fa |
@@ -88,6 +88,19 @@ def seconds_until_market_open() -> float | None:
|
||||
return None
|
||||
|
||||
|
||||
def is_pre_market_window(minutes_before: int = 30) -> bool:
|
||||
"""개장 직전 준비 구간(기본 08:30~09:00, 거래일만).
|
||||
|
||||
이 구간에 배치 분석을 미리 돌려 stock_analysis·추천 캐시를 채워두면
|
||||
09:00 개장과 동시에 추천 종목이 보인다. (2026-09-23)
|
||||
"""
|
||||
now = datetime.now(KST)
|
||||
if now.weekday() >= 5 or _is_holiday(now.date()):
|
||||
return False
|
||||
open_dt = now.replace(hour=9, minute=0, second=0, microsecond=0)
|
||||
return (open_dt - timedelta(minutes=minutes_before)) <= now < open_dt
|
||||
|
||||
|
||||
def is_day_trade_entry_window() -> bool:
|
||||
"""데이트레이딩 신규 진입 가능 시간 (09:30~14:59)"""
|
||||
now = datetime.now(KST)
|
||||
|
||||
@@ -13,7 +13,9 @@ from datetime import datetime as _dt, date as _date, timedelta as _td
|
||||
|
||||
from app.core.config import settings
|
||||
from app.core.database import AsyncSessionLocal
|
||||
from app.core.market_schedule import is_market_open
|
||||
from app.core.market_schedule import (
|
||||
is_market_open, seconds_until_market_open, is_pre_market_window,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -102,8 +104,12 @@ async def batch_analysis_scheduler():
|
||||
await asyncio.sleep(sleep_sec)
|
||||
continue
|
||||
|
||||
if not is_market_open():
|
||||
await asyncio.sleep(1800)
|
||||
# [FIX 2026-09-23] 개장 전 준비 구간(08:30~09:00)에는 분석을 미리 돌려
|
||||
# stock_analysis·Redis 추천 캐시를 채워둔다 → 09:00에 추천 종목 즉시 표시.
|
||||
# 그 외 마감 시간에는 개장을 넘겨 자지 않도록 sleep 상한을 둔다.
|
||||
if not is_market_open() and not is_pre_market_window():
|
||||
_until_open = seconds_until_market_open()
|
||||
await asyncio.sleep(1800.0 if _until_open is None else max(15.0, min(1800.0, _until_open)))
|
||||
continue
|
||||
|
||||
# ── exponential backoff 재시도 (최대 3회) ────────────────────────
|
||||
@@ -411,8 +417,11 @@ async def auto_trade_scheduler():
|
||||
while True:
|
||||
try:
|
||||
if not is_market_open():
|
||||
logger.info("💤 Market is closed. Next check in 30 minutes.")
|
||||
await asyncio.sleep(1800)
|
||||
# 개장을 넘겨 자지 않도록 상한 (09:00부터 바로 매매 판단 시작)
|
||||
_until_open = seconds_until_market_open()
|
||||
_sleep = 1800.0 if _until_open is None else max(15.0, min(1800.0, _until_open))
|
||||
logger.info(f"💤 Market is closed. Next check in {_sleep / 60:.0f} minutes.")
|
||||
await asyncio.sleep(_sleep)
|
||||
continue
|
||||
|
||||
async with AsyncSessionLocal() as session:
|
||||
|
||||
@@ -99,8 +99,11 @@ async def swing_scheduler() -> None:
|
||||
logger.info("[Swing] ── 사이클 시작 ──")
|
||||
|
||||
if not is_market_open():
|
||||
logger.info("[Swing] 장 미오픈 — 30분 대기")
|
||||
await asyncio.sleep(1800)
|
||||
logger.info("[Swing] 장 미오픈 — 개장까지 대기")
|
||||
# 개장을 넘겨 자지 않도록 상한 (2026-09-23)
|
||||
from app.core.market_schedule import seconds_until_market_open
|
||||
_until_open = seconds_until_market_open()
|
||||
await asyncio.sleep(1800.0 if _until_open is None else max(15.0, min(1800.0, _until_open)))
|
||||
continue
|
||||
|
||||
# ── Phase 1: 청산 체크 (별도 세션) ──
|
||||
|
||||
@@ -42,3 +42,15 @@ def test_sleep_never_overshoots_open(monkeypatch):
|
||||
until = ms.seconds_until_market_open()
|
||||
sleep = 1800.0 if until is None else max(15.0, min(1800.0, until))
|
||||
assert sleep <= until, f"{hh}:{mm} — sleep {sleep}s가 개장({until}s 후)을 넘김"
|
||||
|
||||
|
||||
def test_pre_market_window(monkeypatch):
|
||||
"""08:30~09:00은 개장 전 준비 구간 (배치 분석 선행 실행)."""
|
||||
_at(monkeypatch, 2026, 9, 23, 8, 45)
|
||||
assert ms.is_pre_market_window() is True
|
||||
_at(monkeypatch, 2026, 9, 23, 8, 20)
|
||||
assert ms.is_pre_market_window() is False
|
||||
_at(monkeypatch, 2026, 9, 23, 9, 0)
|
||||
assert ms.is_pre_market_window() is False # 개장 후는 정규 경로
|
||||
_at(monkeypatch, 2026, 9, 26, 8, 45) # 토요일
|
||||
assert ms.is_pre_market_window() is False
|
||||
|
||||
Reference in New Issue
Block a user