refactor: rename stock-lab → stock (graduation)
- git mv stock-lab/ → stock/ - docker-compose.yml: 서비스 키 + container_name + build.context + frontend.depends_on + agent-office STOCK_LAB_URL → STOCK_URL - agent-office/app: config.py, service_proxy.py, agents/stock.py, tests/ STOCK_LAB_URL → STOCK_URL - nginx/default.conf: proxy_pass http://stock-lab → http://stock (3 lines) - CLAUDE.md / README.md / STATUS.md / scripts/ 문구 갱신 - stock/ 내부 자기 참조 갱신 lab 네이밍 정책 (feedback_lab_naming.md) graduation. API URL / Python import / DB 파일명 변경 없음.
This commit is contained in:
129
stock/app/test_screener_snapshot.py
Normal file
129
stock/app/test_screener_snapshot.py
Normal file
@@ -0,0 +1,129 @@
|
||||
import datetime as dt
|
||||
import sqlite3
|
||||
|
||||
import pandas as pd
|
||||
import pytest
|
||||
|
||||
from app.screener import snapshot as snap
|
||||
from app.screener.schema import ensure_screener_schema
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def conn(tmp_path):
|
||||
db_path = tmp_path / "snap.db"
|
||||
c = sqlite3.connect(db_path)
|
||||
ensure_screener_schema(c)
|
||||
yield c
|
||||
c.close()
|
||||
|
||||
|
||||
def _stub_listing(monkeypatch):
|
||||
df = pd.DataFrame([
|
||||
{"Code": "005930", "Name": "삼성전자", "Market": "KOSPI",
|
||||
"Marcap": 420_000_000_000_000,
|
||||
"Open": 70000, "High": 72000, "Low": 69500, "Close": 71000,
|
||||
"Volume": 12_000_000, "Amount": 840_000_000_000},
|
||||
{"Code": "035420", "Name": "NAVER", "Market": "KOSPI",
|
||||
"Marcap": 30_000_000_000_000,
|
||||
"Open": 215000, "High": 220000, "Low": 213000, "Close": 218000,
|
||||
"Volume": 1_000_000, "Amount": 218_000_000_000},
|
||||
{"Code": "091990", "Name": "셀트리온헬스케어우", "Market": "KOSDAQ",
|
||||
"Marcap": 10_000_000_000_000,
|
||||
"Open": 60000, "High": 61000, "Low": 59500, "Close": 60500,
|
||||
"Volume": 500_000, "Amount": 30_250_000_000},
|
||||
])
|
||||
monkeypatch.setattr(snap, "fetch_master_listing", lambda: df)
|
||||
|
||||
|
||||
def _stub_flow(monkeypatch, mapping):
|
||||
def fake_flow(ticker, *, client):
|
||||
if mapping is None:
|
||||
return None
|
||||
v = mapping.get(ticker)
|
||||
if v is None:
|
||||
return None
|
||||
return {
|
||||
"date": dt.date(2026, 5, 12).isoformat(),
|
||||
"foreign_net": v["foreign_net"],
|
||||
"institution_net": v["institution_net"],
|
||||
}
|
||||
monkeypatch.setattr(snap, "fetch_flow_naver", fake_flow)
|
||||
|
||||
|
||||
def test_refresh_daily_writes_master_and_prices(conn, monkeypatch):
|
||||
_stub_listing(monkeypatch)
|
||||
_stub_flow(monkeypatch, None)
|
||||
summary = snap.refresh_daily(conn, dt.date(2026, 5, 12),
|
||||
flow_top_n=10, rate_limit_sec=0)
|
||||
assert summary["master_count"] == 3
|
||||
assert summary["prices_count"] == 3
|
||||
assert summary["flow_count"] == 0
|
||||
|
||||
row = conn.execute(
|
||||
"SELECT close FROM krx_daily_prices WHERE ticker='005930' AND date='2026-05-12'"
|
||||
).fetchone()
|
||||
assert row[0] == 71000
|
||||
|
||||
|
||||
def test_refresh_daily_writes_flow_for_top_n(conn, monkeypatch):
|
||||
_stub_listing(monkeypatch)
|
||||
_stub_flow(monkeypatch, {
|
||||
"005930": {"foreign_net": 12_000_000_000, "institution_net": 4_000_000_000},
|
||||
"035420": {"foreign_net": -3_000_000_000, "institution_net": 8_000_000_000},
|
||||
})
|
||||
summary = snap.refresh_daily(conn, dt.date(2026, 5, 12),
|
||||
flow_top_n=2, rate_limit_sec=0)
|
||||
assert summary["flow_count"] == 2
|
||||
row = conn.execute(
|
||||
"SELECT foreign_net FROM krx_flow WHERE ticker='005930'"
|
||||
).fetchone()
|
||||
assert row[0] == 12_000_000_000
|
||||
|
||||
|
||||
def test_master_flags_preferred(conn, monkeypatch):
|
||||
_stub_listing(monkeypatch)
|
||||
_stub_flow(monkeypatch, None)
|
||||
snap.refresh_daily(conn, dt.date(2026, 5, 12), flow_top_n=0, rate_limit_sec=0)
|
||||
pref = conn.execute(
|
||||
"SELECT is_preferred FROM krx_master WHERE ticker='091990'"
|
||||
).fetchone()
|
||||
assert pref[0] == 1
|
||||
|
||||
|
||||
def test_refresh_daily_is_idempotent(conn, monkeypatch):
|
||||
_stub_listing(monkeypatch)
|
||||
_stub_flow(monkeypatch, None)
|
||||
snap.refresh_daily(conn, dt.date(2026, 5, 12), flow_top_n=0, rate_limit_sec=0)
|
||||
snap.refresh_daily(conn, dt.date(2026, 5, 12), flow_top_n=0, rate_limit_sec=0)
|
||||
cnt = conn.execute(
|
||||
"SELECT count(*) FROM krx_daily_prices WHERE date='2026-05-12'"
|
||||
).fetchone()[0]
|
||||
assert cnt == 3
|
||||
|
||||
|
||||
def test_fetch_flow_naver_parses_html():
|
||||
"""Real HTML structure parse with synthetic naver-like markup."""
|
||||
html = """
|
||||
<html><body>
|
||||
<table class="type2">
|
||||
<tr><th>날짜</th></tr>
|
||||
<tr><td>2026.05.12</td><td>71,000</td><td>500</td><td>0.71%</td>
|
||||
<td>12,000,000</td><td>4,000,000,000</td><td>12,000,000,000</td>
|
||||
<td>1</td><td>53.0</td></tr>
|
||||
<tr><td>2026.05.09</td><td>70,500</td><td>-200</td><td>-0.28%</td>
|
||||
<td>10,000,000</td><td>2,000,000,000</td><td>5,000,000,000</td>
|
||||
<td>1</td><td>52.8</td></tr>
|
||||
</table>
|
||||
</body></html>
|
||||
"""
|
||||
class FakeResp:
|
||||
status_code = 200
|
||||
text = html
|
||||
class FakeClient:
|
||||
def get(self, url, params): return FakeResp()
|
||||
out = snap.fetch_flow_naver("005930", client=FakeClient())
|
||||
assert out == {
|
||||
"date": "2026-05-12",
|
||||
"foreign_net": 12_000_000_000,
|
||||
"institution_net": 4_000_000_000,
|
||||
}
|
||||
Reference in New Issue
Block a user