feat(stock-webai): /api/webai/news-sentiment daily dump
JOINs news_sentiment with krx_master for name fallback. Sorted by
score DESC. Date param defaults to latest. Empty table returns
{date: null, count: 0, items: []}. 4 integration tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -87,3 +87,87 @@ def test_webai_portfolio_missing_key_returns_401(client):
|
||||
r = client.get("/api/webai/portfolio")
|
||||
assert r.status_code == 401
|
||||
assert "X-WebAI-Key" in r.json()["detail"]
|
||||
|
||||
|
||||
def _seed_news_sentiment(date_str: str, rows: list[tuple]):
|
||||
"""rows: list of (ticker, score_raw, reason, news_count)."""
|
||||
db_path = os.environ["STOCK_DB_PATH"]
|
||||
c = sqlite3.connect(db_path)
|
||||
for ticker, score, reason, news_count in rows:
|
||||
c.execute(
|
||||
"INSERT OR REPLACE INTO news_sentiment "
|
||||
"(ticker, date, score_raw, reason, news_count, source) "
|
||||
"VALUES (?, ?, ?, ?, ?, 'articles')",
|
||||
(ticker, date_str, score, reason, news_count)
|
||||
)
|
||||
c.commit()
|
||||
c.close()
|
||||
|
||||
|
||||
def _seed_krx_master(rows: list[tuple]):
|
||||
"""rows: list of (ticker, name)."""
|
||||
db_path = os.environ["STOCK_DB_PATH"]
|
||||
c = sqlite3.connect(db_path)
|
||||
import datetime as dt
|
||||
now = dt.datetime.utcnow().isoformat()
|
||||
for ticker, name in rows:
|
||||
c.execute(
|
||||
"INSERT OR REPLACE INTO krx_master "
|
||||
"(ticker, name, market, market_cap, updated_at) VALUES (?, ?, 'KOSPI', 0, ?)",
|
||||
(ticker, name, now)
|
||||
)
|
||||
c.commit()
|
||||
c.close()
|
||||
|
||||
|
||||
def test_webai_news_sentiment_returns_latest_date_when_no_param(client):
|
||||
_seed_krx_master([("005930", "삼성전자"), ("000660", "SK하이닉스")])
|
||||
_seed_news_sentiment("2026-05-14", [("005930", 5.0, "old", 5)])
|
||||
_seed_news_sentiment("2026-05-15", [
|
||||
("005930", 6.2, "HBM 양산 가시화", 12),
|
||||
("000660", 5.5, "PPI 우려에도 강세", 8),
|
||||
])
|
||||
|
||||
r = client.get("/api/webai/news-sentiment", headers=HEADERS_OK)
|
||||
assert r.status_code == 200
|
||||
body = r.json()
|
||||
assert body["date"] == "2026-05-15"
|
||||
assert body["count"] == 2
|
||||
# sorted by score DESC
|
||||
assert body["items"][0]["ticker"] == "005930"
|
||||
assert body["items"][0]["score"] == 6.2
|
||||
assert body["items"][0]["name"] == "삼성전자"
|
||||
assert body["items"][0]["reason"] == "HBM 양산 가시화"
|
||||
|
||||
|
||||
def test_webai_news_sentiment_filters_by_date_param(client):
|
||||
_seed_krx_master([("005930", "삼성전자")])
|
||||
_seed_news_sentiment("2026-05-14", [("005930", 5.0, "yesterday", 5)])
|
||||
_seed_news_sentiment("2026-05-15", [("005930", 6.2, "today", 12)])
|
||||
|
||||
r = client.get("/api/webai/news-sentiment?date=2026-05-14", headers=HEADERS_OK)
|
||||
body = r.json()
|
||||
assert body["date"] == "2026-05-14"
|
||||
assert body["count"] == 1
|
||||
assert body["items"][0]["reason"] == "yesterday"
|
||||
|
||||
|
||||
def test_webai_news_sentiment_empty_table_returns_count_zero(client):
|
||||
r = client.get("/api/webai/news-sentiment", headers=HEADERS_OK)
|
||||
body = r.json()
|
||||
assert body["date"] is None
|
||||
assert body["count"] == 0
|
||||
assert body["items"] == []
|
||||
|
||||
|
||||
def test_webai_news_sentiment_items_sorted_by_score_desc(client):
|
||||
_seed_krx_master([("A", "A주"), ("B", "B주"), ("C", "C주")])
|
||||
_seed_news_sentiment("2026-05-15", [
|
||||
("A", 1.0, "low", 1),
|
||||
("B", 9.0, "high", 1),
|
||||
("C", 5.0, "mid", 1),
|
||||
])
|
||||
|
||||
r = client.get("/api/webai/news-sentiment", headers=HEADERS_OK)
|
||||
items = r.json()["items"]
|
||||
assert [i["score"] for i in items] == [9.0, 5.0, 1.0]
|
||||
|
||||
Reference in New Issue
Block a user