From 134b9e5d07584effdf95e89d188a761e65e82d50 Mon Sep 17 00:00:00 2001 From: gahusb Date: Thu, 2 Jul 2026 19:51:57 +0900 Subject: [PATCH] =?UTF-8?q?feat(stock):=20session=20=ED=8C=90=EC=A0=95=20+?= =?UTF-8?q?=20webai=20monitor-set=20=EC=97=94=EB=93=9C=ED=8F=AC=EC=9D=B8?= =?UTF-8?q?=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01EqCYBhvTcdeCTUDX3RhWx9 --- stock/app/main.py | 25 +++++++++++++ stock/app/trade_alerts.py | 22 +++++++++++- .../tests/test_trade_alerts_monitorset_api.py | 35 +++++++++++++++++++ 3 files changed, 81 insertions(+), 1 deletion(-) create mode 100644 stock/tests/test_trade_alerts_monitorset_api.py diff --git a/stock/app/main.py b/stock/app/main.py index ab598d1..de5c118 100644 --- a/stock/app/main.py +++ b/stock/app/main.py @@ -29,6 +29,9 @@ from .ai_summarizer import summarize_news, OllamaError from .auth import verify_webai_key from . import webai_cache from . import holdings_intel +from .trade_alerts import ( + build_monitor_set, current_session, DEFAULT_EXIT_PARAMS, DEFAULT_BUY_PARAMS, +) app = FastAPI() install_access_log(app) @@ -507,6 +510,28 @@ def get_webai_news_sentiment(date: str | None = None): return result +@app.get("/api/webai/trade-alert/monitor-set", dependencies=[Depends(verify_webai_key)]) +def get_trade_alert_monitor_set(): + """web-ai(Windows 워커) 전용 — 실시간 매매 알람 감시대상 조립 (계약 §5.1). + + session은 KST 시각으로 pre/regular/after 판정 후, 평일·휴장 여부(is_market_open)를 + 함께 게이팅해 최종 closed 여부를 결정한다. + """ + from datetime import datetime, timezone, timedelta + kst = timezone(timedelta(hours=9)) + now_kst = datetime.now(kst) + session = current_session(now_kst) + if not is_market_open(now_kst.date()): + session = "closed" + + from .db import _conn + conn = _conn() + try: + return build_monitor_set(conn, session, DEFAULT_EXIT_PARAMS, DEFAULT_BUY_PARAMS) + finally: + conn.close() + + @app.post("/api/portfolio", status_code=201) def create_portfolio_item(req: PortfolioItemRequest): """포트폴리오 종목 추가""" diff --git a/stock/app/trade_alerts.py b/stock/app/trade_alerts.py index bbb86ea..97fe3f9 100644 --- a/stock/app/trade_alerts.py +++ b/stock/app/trade_alerts.py @@ -5,13 +5,33 @@ Windows 워커가 GET /api/webai/trade-alert/monitor-set 로 받는 응답을 NAS는 watchlist ∪ screener 최신 성공 run 후보를 buy_targets로, 보유 종목을 sell_targets로 병합해 넘긴다. TA/조건판정은 워커 쪽 책임. """ -from datetime import datetime, timedelta, timezone +from datetime import datetime, timedelta, timezone, time as _time from typing import Optional from app.db import get_all_portfolio, get_watchlist _KST = timezone(timedelta(hours=9)) +# KST 세션 창(시:분) — 평일+휴장 판정은 호출부에서 is_market_open으로 별도 게이팅 +_SESSIONS = [ + ("pre", (8, 30), (9, 0)), + ("regular", (9, 0), (15, 30)), + ("after", (16, 0), (18, 0)), +] + + +def current_session(now_kst) -> str: + """now_kst의 time만으로 pre/regular/after/closed 세션 판정 (요일·휴장 무관).""" + t = now_kst.time() + for name, (sh, sm), (eh, em) in _SESSIONS: + if _time(sh, sm) <= t < _time(eh, em): + return name + return "closed" + + +DEFAULT_EXIT_PARAMS = {"stop_pct": 0.08, "take_pct": 0.25, "trailing_pct": 0.10} +DEFAULT_BUY_PARAMS = {"rsi_oversold": 30, "breakout_vol_mult": 1.5, "pullback_pct": 0.02} + def latest_screener_candidates(conn) -> list: """최신 성공(status='success') screener run의 후보 {ticker,name} 목록.""" diff --git a/stock/tests/test_trade_alerts_monitorset_api.py b/stock/tests/test_trade_alerts_monitorset_api.py new file mode 100644 index 0000000..f1ed79f --- /dev/null +++ b/stock/tests/test_trade_alerts_monitorset_api.py @@ -0,0 +1,35 @@ +import datetime as dt +import pytest +from fastapi.testclient import TestClient + + +def test_current_session_windows(): + from app.trade_alerts import current_session + d = dt.date(2026, 7, 2) + assert current_session(dt.datetime.combine(d, dt.time(8, 40))) == "pre" + assert current_session(dt.datetime.combine(d, dt.time(10, 0))) == "regular" + assert current_session(dt.datetime.combine(d, dt.time(17, 0))) == "after" + assert current_session(dt.datetime.combine(d, dt.time(20, 0))) == "closed" + + +@pytest.fixture +def client(monkeypatch, tmp_path): + from app import db as _db + monkeypatch.setattr(_db, "DB_PATH", str(tmp_path / "stock.db")) + _db.init_db() + monkeypatch.setenv("WEBAI_API_KEY", "k") + from app.main import app + return TestClient(app) + + +def test_monitor_set_requires_auth(client): + assert client.get("/api/webai/trade-alert/monitor-set").status_code == 401 + + +def test_monitor_set_ok(client): + r = client.get("/api/webai/trade-alert/monitor-set", headers={"X-WebAI-Key": "k"}) + assert r.status_code == 200 + body = r.json() + assert body["session"] in ("pre", "regular", "after", "closed") + assert "buy_targets" in body and "sell_targets" in body + assert body["exit_params"]["trailing_pct"] == 0.10