Files
easy_tdx_max/tests/unit/test_sentiment.py
T

201 lines
6.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""市场情绪采样(store / sampler / 端点)与涨停历史回补单测。
sentiment_store 用 EASY_TDX_CONFIG_DIR 指向临时目录;limitup 历史复用
合成 .day 文件;端点侧验证 DictResponse 包装与缓存命中。
"""
from __future__ import annotations
import asyncio
import pytest
@pytest.fixture
def store(tmp_path, monkeypatch):
"""独立配置目录 + 全新单例的 SentimentStore。"""
from easy_tdx.web import sentiment_store as ss
monkeypatch.setenv("EASY_TDX_CONFIG_DIR", str(tmp_path / "cfg"))
ss._store = None
s = ss.get_sentiment_store()
yield s
ss._store = None
def _sample(date: int, minute: int, up=2000, down=2000, limit_up=50, limit_down=10, amount=8e11):
from datetime import datetime
return {
"date": date,
"minute": minute,
"ts": int(datetime(2026, 9, 4).timestamp()),
"up_count": up,
"down_count": down,
"neutral_count": 100,
"total_count": up + down + 100,
"limit_up_count": limit_up,
"limit_down_count": limit_down,
"total_amount": amount,
}
def test_store_day_samples_and_idempotent(store):
store.insert(_sample(20260904, 935))
store.insert(_sample(20260904, 930))
# 同 (date, minute) 覆盖不累积
store.insert(_sample(20260904, 930, limit_up=77))
rows = store.day_samples(20260904)
assert [r["minute"] for r in rows] == [930, 935] # 升序
assert rows[0]["limit_up_count"] == 77 # 覆盖生效
assert store.latest_date() == 20260904
def test_store_daily_history_close_snapshot_and_peak(store):
# 收盘快照 = 当日最后一条采样;峰值 = 当日涨停最大值
store.insert(_sample(20260903, 930, up=1500, limit_up=30, limit_down=40, amount=7e11))
store.insert(
_sample(20260903, 1500, up=2500, down=1500, limit_up=90, limit_down=5, amount=9e11)
)
store.insert(
_sample(20260904, 930, up=1800, down=2200, limit_up=20, limit_down=60, amount=6e11)
)
days = store.daily_history(10)
assert [d["date"] for d in days] == [20260903, 20260904] # 升序
d3 = days[0]
assert d3["limit_up_peak"] == 90 # 日内峰值(930 点只有 301500 点 90
assert d3["limit_up_close"] == 90 # 收盘快照取当日最后一条
assert d3["up_count"] == 2500
assert d3["up_ratio"] == 62.5 # 2500 / (2500+1500)
d4 = days[1]
assert d4["limit_up_peak"] == 20
assert d4["up_ratio"] == 45.0 # 1800 / 4000
def test_sampler_inserts_store_rows(store):
import pandas as pd
from easy_tdx.web.sentiment_sampler import SentimentSampler
df = pd.DataFrame(
[
{
"up_count": 2100,
"down_count": 2300,
"neutral_count": 120,
"total_count": 4520,
"limit_up_count": 44,
"limit_down_count": 9,
"total_amount": 8.5e11,
}
]
)
class FakeClient:
async def get_market_stat(self):
return df
sampler = SentimentSampler(FakeClient().get_market_stat, store=store, interval=1.0)
asyncio.run(sampler._sample_once())
rows = store.day_samples(store.latest_date())
assert len(rows) == 1
assert rows[0]["limit_up_count"] == 44
assert rows[0]["total_amount"] == 8.5e11
@pytest.fixture
def vipdoc_factory(tmp_path):
"""按 {文件名: {dates, closes}} 合成 vipdoc 目录的工厂。"""
from easy_tdx.offline.daily_bar import _DAILY_FMT
def _day(date: int, close: float) -> bytes:
return _DAILY_FMT.pack(
date,
round((close - 0.05) * 100),
round(close * 100),
round((close - 0.10) * 100),
round(close * 100),
5_000_000.0,
1_000_000,
0,
)
def factory(specs: dict[str, dict]) -> object:
for filename, spec in specs.items():
exchange = filename[:2]
lday = tmp_path / exchange / "lday"
lday.mkdir(parents=True, exist_ok=True)
data = b"".join(_day(d, c) for d, c in zip(spec["dates"], spec["closes"]))
(lday / f"{filename}.day").write_bytes(data)
return tmp_path
return factory
def test_limitup_history_counts(vipdoc_factory):
from easy_tdx.screen.limitup import compute_limitup_history
v = vipdoc_factory(
# A 股票:0802、0803 连续两日涨停
{
"sh600100": {
"dates": [20260801, 20260802, 20260803, 20260804],
"closes": [10.00, 11.00, 12.10, 12.50],
},
# B 股票:0804 跌停
"sz000200": {
"dates": [20260801, 20260802, 20260803, 20260804],
"closes": [10.00, 10.00, 10.00, 9.00],
},
}
)
rows = compute_limitup_history(v, days=10)
by_date = {r["date"]: r for r in rows}
assert by_date[20260802]["limit_up"] == 1
assert by_date[20260803]["limit_up"] == 1
assert by_date[20260804]["limit_down"] == 1
assert by_date[20260804]["limit_up"] == 0
# 升序
dates = [r["date"] for r in rows]
assert dates == sorted(dates)
def test_limitup_history_endpoint_cache(vipdoc_factory, monkeypatch):
pytest.importorskip("fastapi")
from fastapi import FastAPI
from fastapi.testclient import TestClient
from easy_tdx.screen import limitup as limitup_mod
from easy_tdx.web.errors import register_exception_handlers
from easy_tdx.web.routers import market as market_mod
v = vipdoc_factory({"sh600100": {"dates": [20260801, 20260802], "closes": [10.0, 11.0]}})
calls = {"n": 0}
real = limitup_mod.compute_limitup_history
def counting(*a, **kw):
calls["n"] += 1
return real(*a, **kw)
monkeypatch.setattr(limitup_mod, "compute_limitup_history", counting)
app = FastAPI()
register_exception_handlers(app)
app.include_router(market_mod.router, prefix="/api/v1")
app.state.tdx_client = object()
with TestClient(app) as client:
r1 = client.get("/api/v1/market/limitup-history", params={"days": 10, "vipdoc": str(v)})
assert r1.status_code == 200
body = r1.json()["data"]
# 仅 0802 有一天涨停(0801 无前收不计数)
assert body["count"] == 1
assert body["days"][0] == {"date": 20260802, "limit_up": 1, "limit_down": 0}
client.get("/api/v1/market/limitup-history", params={"days": 10, "vipdoc": str(v)})
assert calls["n"] == 1 # 缓存命中