feat(stock): webai report — edge diff→agent-office push→상태/이력(전송성공시만)

This commit is contained in:
2026-07-02 19:56:58 +09:00
parent 134b9e5d07
commit 2906a2ae3e
3 changed files with 112 additions and 1 deletions

View File

@@ -22,6 +22,7 @@ from .db import (
upsert_asset_snapshot, get_asset_snapshots,
add_sell_history, get_sell_history, update_sell_history, delete_sell_history,
add_watchlist, remove_watchlist, get_watchlist, get_alert_history,
get_alert_state_firing, set_alert_firing, touch_alert_seen, add_alert_history,
)
from .scraper import fetch_market_news, fetch_major_indices
from .price_fetcher import get_current_prices, get_current_prices_detail
@@ -29,8 +30,9 @@ from .ai_summarizer import summarize_news, OllamaError
from .auth import verify_webai_key
from . import webai_cache
from . import holdings_intel
from . import trade_alerts
from .trade_alerts import (
build_monitor_set, current_session, DEFAULT_EXIT_PARAMS, DEFAULT_BUY_PARAMS,
build_monitor_set, current_session, diff_firing, DEFAULT_EXIT_PARAMS, DEFAULT_BUY_PARAMS,
)
app = FastAPI()
@@ -532,6 +534,41 @@ def get_trade_alert_monitor_set():
conn.close()
class TradeAlertReport(BaseModel):
as_of: str | None = None
firing: list[dict] = []
@app.post("/api/webai/trade-alert/report", dependencies=[Depends(verify_webai_key)])
def post_trade_alert_report(req: TradeAlertReport):
"""web-ai(Windows 워커) 전용 — 발화 보고 수신 (계약 §5.2).
직전 발화상태 대비 edge diff(diff_firing) 후, 신규 alert는
agent-office 전송 성공 시에만 상태(firing=True)+이력 반영한다.
전송 실패 시 상태를 채택하지 않아 다음 사이클에 동일 alert가 다시
"신규"로 잡혀 재시도된다(멱등). 해제(cleared)는 전송과 무관하게 firing=False.
"""
prev = get_alert_state_firing()
d = diff_firing(req.firing, prev)
new_count = 0
for a in d["new"]:
if trade_alerts.notify_agent_office([a]):
set_alert_firing(a["ticker"], a["kind"], a["condition"], firing=True, at_iso=req.as_of)
add_alert_history(
a["ticker"], a.get("name"), a["kind"], a["condition"],
a.get("price"), a.get("detail") or {},
)
new_count += 1
for ticker, kind, condition in d["cleared"]:
set_alert_firing(ticker, kind, condition, firing=False)
touch_alert_seen(d["seen"], req.as_of or "")
return {"new_alerts": new_count, "cleared": len(d["cleared"])}
@app.post("/api/portfolio", status_code=201)
def create_portfolio_item(req: PortfolioItemRequest):
"""포트폴리오 종목 추가"""

View File

@@ -5,6 +5,9 @@ Windows 워커가 GET /api/webai/trade-alert/monitor-set 로 받는 응답을
NAS는 watchlist screener 최신 성공 run 후보를 buy_targets로, 보유 종목을
sell_targets로 병합해 넘긴다. TA/조건판정은 워커 쪽 책임.
"""
import os
import httpx
from datetime import datetime, timedelta, timezone, time as _time
from typing import Optional
@@ -117,3 +120,18 @@ def diff_firing(reported: list, prev: set) -> dict:
"cleared": cleared,
"seen": sorted(cur_keys),
}
def notify_agent_office(alerts: list) -> bool:
"""신규 alert들을 agent-office로 push (계약 §5.2). 전송 성공 시 True.
실패(네트워크 오류/비-200)는 False — 호출부가 상태/이력 미채택 후 다음
사이클에 동일 alert를 재시도하도록 한다(멱등, at-least-once).
"""
url = os.getenv("AGENT_OFFICE_URL", "http://agent-office:8000") + "/api/agent-office/stock/trade-alert"
try:
with httpx.Client(timeout=10) as c:
resp = c.post(url, json={"alerts": alerts})
return resp.status_code == 200
except httpx.HTTPError:
return False