feat(stock): session 판정 + webai monitor-set 엔드포인트
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EqCYBhvTcdeCTUDX3RhWx9
This commit is contained in:
@@ -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):
|
||||
"""포트폴리오 종목 추가"""
|
||||
|
||||
@@ -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} 목록."""
|
||||
|
||||
35
stock/tests/test_trade_alerts_monitorset_api.py
Normal file
35
stock/tests/test_trade_alerts_monitorset_api.py
Normal file
@@ -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
|
||||
Reference in New Issue
Block a user