수정본
Some checks failed
CI/CD Pipeline / test-backend (push) Has been cancelled
CI/CD Pipeline / test-frontend (push) Has been cancelled
CI/CD Pipeline / build-and-push (push) Has been cancelled

This commit is contained in:
2026-03-27 14:47:49 +09:00
parent 844ee8944f
commit c818810667
25 changed files with 2708 additions and 266 deletions

View File

@@ -30,7 +30,7 @@ from app.core.database import AsyncSessionLocal
from app.core.market_schedule import is_market_open
from app.services.mdd_manager import mdd_manager
from app.services.swing.exit_service import check_and_execute_exit_v2
from app.services.swing.entry_service import execute_swing_entries
from app.services.swing.entry_service import execute_swing_entries, scan_dual_strategy_signals
logger = logging.getLogger(__name__)
_KST = pytz.timezone("Asia/Seoul")
@@ -57,6 +57,25 @@ def _normalize_recommendations(rec: dict) -> dict:
return rec
def _filter_unsuitable_stocks(rec: dict, unsuitable_codes: set) -> None:
"""장 전 AI 분석에서 부적합 판정된 종목을 추천 목록에서 제거."""
groups = rec.get("recommendations_by_rule", [])
if isinstance(groups, list):
for group in groups:
if isinstance(group, dict) and "stocks" in group:
group["stocks"] = [
s for s in group["stocks"]
if isinstance(s, dict) and s.get("stock_code") not in unsuitable_codes
]
elif isinstance(groups, dict):
for key, stocks in groups.items():
if isinstance(stocks, list):
groups[key] = [
s for s in stocks
if isinstance(s, dict) and s.get("stock_code") not in unsuitable_codes
]
async def swing_scheduler() -> None:
"""60초 주기 스윙 트레이딩 스케줄러.
@@ -149,7 +168,25 @@ async def swing_scheduler() -> None:
await asyncio.sleep(60)
continue
# 5. MCP RISK_ASSESSMENT
# 5. 장 전 AI 분석 결과 활용 + MCP 리스크 평가 (최적화)
# 장 전 Sonnet 배치 분석 결과가 Redis에 있으면 부적합 종목을 사전 필터링
# MCP RISK_ASSESSMENT는 실제 매수 직전에만 호출 (비용 절감)
_ai_filtered_codes = set()
try:
_ai_pre = await get_json("ai:pre_market:latest")
if _ai_pre and isinstance(_ai_pre, dict):
for s in _ai_pre.get("suitable_stocks", []):
if isinstance(s, dict) and not s.get("suitable", True):
_ai_filtered_codes.add(s.get("stock_code", ""))
if _ai_filtered_codes:
logger.info(f"[Swing] AI 장 전 분석 필터: {len(_ai_filtered_codes)}종목 제외")
except Exception:
pass
# 추천 목록에서 AI 부적합 종목 제거
if _ai_filtered_codes:
_filter_unsuitable_stocks(rec, _ai_filtered_codes)
recommendations = rec.get("recommendations_by_rule", [])
if isinstance(recommendations, dict):
rec_count = sum(len(v) for v in recommendations.values())
@@ -169,6 +206,12 @@ async def swing_scheduler() -> None:
for s in g.get("stocks", [])[:3]
][:10]
if not top_stocks:
logger.info("[Swing] 필터링 후 추천 종목 없음 — 스킵")
await asyncio.sleep(60)
continue
# MCP 리스크 평가 (Haiku — 실제 진입 직전 1회)
trade_plan = {
"action": "BUY",
"strategy": "swing",
@@ -196,7 +239,17 @@ async def swing_scheduler() -> None:
async with AsyncSessionLocal() as entry_session:
logger.info(f"[Swing] 진입 실행 시작: {top_stocks[:5]}")
await execute_swing_entries(rec, entry_session)
logger.info("[Swing] ── 사이클 완료 ──")
# 7. 듀얼 전략 시그널 스캔 (모멘텀 돌파 + 눌림목 매수, CLAUDE.md 8.4)
try:
async with AsyncSessionLocal() as dual_session:
dual_results = await scan_dual_strategy_signals(dual_session)
if dual_results:
logger.info(f"[Swing] 듀얼 전략 진입: {len(dual_results)}건")
except Exception as e:
logger.debug(f"[Swing] 듀얼 전략 스캔 오류: {e}")
logger.info("[Swing] ── 사이클 완료 ──")
except Exception as e:
logger.error(f"[Swing] 스케줄러 사이클 오류: {e}", exc_info=True)