feat(stock): webai report — edge diff→agent-office push→상태/이력(전송성공시만)
This commit is contained in:
@@ -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):
|
||||
"""포트폴리오 종목 추가"""
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user