diff --git a/src/easy_tdx/commands/security_quotes.py b/src/easy_tdx/commands/security_quotes.py index 1bf8fa4..b2c00eb 100644 --- a/src/easy_tdx/commands/security_quotes.py +++ b/src/easy_tdx/commands/security_quotes.py @@ -201,7 +201,8 @@ class GetSecurityQuotesCmd(BaseCommand[list[SecurityQuote]]): raise TdxDecodeError(f"security_quotes 非法 market 值: {market_b}") from e code = code_b.decode("utf-8").rstrip("\x00") - # 价格按品种有效小数位解析:股票/大盘指数 ÷100,ETF/基金/债券/880 统计指数 ÷1000(Issue #8) + # 价格按品种有效小数位解析:股票/大盘指数 ÷100; + # ETF/基金/债券/880 统计指数 ÷1000(Issue #8) divisor = 10 ** _price_decimal_digits(market, code) p = price_raw / divisor diff --git a/src/easy_tdx/web/app.py b/src/easy_tdx/web/app.py index 5aed2fc..0a8525b 100644 --- a/src/easy_tdx/web/app.py +++ b/src/easy_tdx/web/app.py @@ -88,7 +88,7 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]: store = get_watchlist_store() - async def _watch_symbols(): + async def _watch_symbols() -> list[tuple[Market, str]]: # SQLite 存 "SH"/"SZ"/"BJ" 字符串,轮询器需要 Market 枚举 return [(Market[mkt], code) for mkt, code in store.symbols()] @@ -132,10 +132,10 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]: yield # --- 关闭实时行情推送器 --- - streamer = getattr(app.state, "quote_streamer", None) - if streamer is not None: + streamer_svc = getattr(app.state, "quote_streamer", None) + if streamer_svc is not None: try: - await streamer.stop() + await streamer_svc.stop() except Exception: logger.warning("QuoteStreamer stop failed", exc_info=True) diff --git a/src/easy_tdx/web/quote_streamer.py b/src/easy_tdx/web/quote_streamer.py index d2f887b..a960712 100644 --- a/src/easy_tdx/web/quote_streamer.py +++ b/src/easy_tdx/web/quote_streamer.py @@ -19,7 +19,8 @@ import asyncio import itertools import logging from collections.abc import Awaitable, Callable -from datetime import datetime, timedelta, timezone as dt_timezone +from datetime import datetime, timedelta +from datetime import timezone as dt_timezone from typing import Any import pandas as pd diff --git a/src/easy_tdx/web/routers/stream.py b/src/easy_tdx/web/routers/stream.py index 4425661..2a556a5 100644 --- a/src/easy_tdx/web/routers/stream.py +++ b/src/easy_tdx/web/routers/stream.py @@ -15,6 +15,7 @@ from __future__ import annotations import asyncio import json import logging +from collections.abc import AsyncGenerator from fastapi import APIRouter, Request from fastapi.responses import StreamingResponse @@ -37,7 +38,7 @@ async def stream_quotes(request: Request) -> StreamingResponse: qid, queue = streamer.subscribe() - async def event_gen(): # type: ignore[no-untyped-def] + async def event_gen() -> AsyncGenerator[str, None]: try: # 首帧 hello:告诉前端连接可用 + 当前订阅规模 hello = {"type": "hello", "subscribers": streamer.subscriber_count} diff --git a/src/easy_tdx/web/routers/watchlist.py b/src/easy_tdx/web/routers/watchlist.py index 8b41541..3d2ece3 100644 --- a/src/easy_tdx/web/routers/watchlist.py +++ b/src/easy_tdx/web/routers/watchlist.py @@ -20,26 +20,28 @@ class WatchItemAdd(BaseModel): class WatchlistResponse(BaseModel): - items: list[dict] + items: list[dict[str, object]] count: int @router.get("/watchlist", response_model=WatchlistResponse) -async def list_watchlist(group: str | None = Query(None, description="按分组过滤")) -> WatchlistResponse: +async def list_watchlist( + group: str | None = Query(None, description="按分组过滤"), +) -> WatchlistResponse: """列出全部自选(按加入顺序)。""" items = get_watchlist_store().list_all(group=group) return WatchlistResponse(items=[i.to_dict() for i in items], count=len(items)) -@router.post("/watchlist", response_model=dict) -async def add_watch_item(req: WatchItemAdd) -> dict: +@router.post("/watchlist", response_model=dict[str, object]) +async def add_watch_item(req: WatchItemAdd) -> dict[str, object]: """加入自选(幂等:重复加入仅刷新名称)。""" item = get_watchlist_store().add(req.market, req.code, name=req.name, group=req.group) return {"ok": True, "item": item.to_dict()} -@router.delete("/watchlist/{market}/{code}", response_model=dict) -async def remove_watch_item(market: str, code: str) -> dict: +@router.delete("/watchlist/{market}/{code}", response_model=dict[str, object]) +async def remove_watch_item(market: str, code: str) -> dict[str, object]: """移除自选。""" if market.upper() not in {"SZ", "SH", "BJ"}: raise HTTPException(status_code=400, detail=f"非法市场: {market}") diff --git a/src/easy_tdx/web/watchlist_store.py b/src/easy_tdx/web/watchlist_store.py index e310aa8..28872b3 100644 --- a/src/easy_tdx/web/watchlist_store.py +++ b/src/easy_tdx/web/watchlist_store.py @@ -56,7 +56,7 @@ class WatchItem: """前端统一标识:SH600000 形式。""" return f"{self.market}{self.code}" - def to_dict(self) -> dict: + def to_dict(self) -> dict[str, object]: return { "market": self.market, "code": self.code, @@ -125,7 +125,8 @@ class WatchlistStore: market = market.upper() with _write_lock, self._connect() as conn: row = conn.execute( - "SELECT * FROM watchlist WHERE market = ? AND code = ?", (market, code) + "SELECT * FROM watchlist WHERE market = ? AND code = ?", + (market, code), ).fetchone() if row is not None: if name and name != row["name"]: @@ -141,19 +142,26 @@ class WatchlistStore: created_at=row["created_at"], sort_order=row["sort_order"], ) - next_order = conn.execute("SELECT COALESCE(MAX(sort_order), 0) + 1 FROM watchlist").fetchone()[0] + next_order = conn.execute( + "SELECT COALESCE(MAX(sort_order), 0) + 1 FROM watchlist" + ).fetchone()[0] conn.execute( "INSERT INTO watchlist (market, code, name, group_name, created_at, sort_order)" " VALUES (?, ?, ?, ?, ?, ?)", (market, code, name, group, _now_iso(), next_order), ) - return WatchItem(market=market, code=code, name=name, group_name=group, created_at=_now_iso(), sort_order=next_order) + return WatchItem( + market=market, code=code, name=name, group_name=group, + created_at=_now_iso(), sort_order=next_order, + ) def remove(self, market: str, code: str) -> bool: """移除自选;返回是否确实删除了一条。""" market = market.upper() with _write_lock, self._connect() as conn: - cur = conn.execute("DELETE FROM watchlist WHERE market = ? AND code = ?", (market, code)) + cur = conn.execute( + "DELETE FROM watchlist WHERE market = ? AND code = ?", (market, code) + ) return cur.rowcount > 0 diff --git a/tests/unit/test_watchlist_and_streamer.py b/tests/unit/test_watchlist_and_streamer.py index f458d89..1de2510 100644 --- a/tests/unit/test_watchlist_and_streamer.py +++ b/tests/unit/test_watchlist_and_streamer.py @@ -12,7 +12,6 @@ from easy_tdx.models.enums import Market from easy_tdx.web.quote_streamer import INDEX_SYMBOLS, QuoteStreamer, _is_trading_hours from easy_tdx.web.watchlist_store import WatchlistStore - # ── WatchlistStore ────────────────────────────────────────────────────────── @@ -147,7 +146,8 @@ def test_streamer_backpressure_drops_oldest() -> None: def test_is_trading_hours() -> None: - from datetime import datetime, timedelta, timezone as dt_timezone + from datetime import datetime, timedelta + from datetime import timezone as dt_timezone tz = dt_timezone(timedelta(hours=8)) assert _is_trading_hours(datetime(2026, 9, 1, 10, 0, tzinfo=tz)) is True # 周二盘中