feat(web): full Web API parity with CLI — 18 new endpoints (v1.10.2)

- Board analysis: list/members/belong/summary/ranking/change-ranking (6)
- Capital flow, symbol info, server info (3)
- Quote list, auction, unusual (3)
- Extended market: bars/quote/minute/transaction (4)
- Technical indicators: list + compute (2)
- Multi-client DI: AsyncMacClient + AsyncExTdxClient lifecycle
- 6 MAC enum converters, DictResponse, ComputeIndicatorsRequest schemas
- Web API endpoints: 22 → 40

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
GitHub
2026-06-12 16:46:48 +08:00
co-authored by Claude
parent 06ce712d15
commit d17895e92b
11 changed files with 672 additions and 13 deletions
+77 -6
View File
@@ -773,6 +773,7 @@ easy-tdx serve --reload
### REST API 示例
```bash
# ── 基础行情 ──
# 获取深圳市场证券数量
curl "http://localhost:8000/api/v1/security/count?market=SZ"
@@ -784,16 +785,71 @@ curl -X POST "http://localhost:8000/api/v1/quotes" \
-H "Content-Type: application/json" \
-d '{"stocks": [{"market": "SZ", "code": "000001"}, {"market": "SH", "code": "600000"}]}'
# 缠论分析
curl -X POST "http://localhost:8000/api/v1/chanlun/analyze" \
-H "Content-Type: application/json" \
-d '{"market": "SZ", "code": "000001", "category": "DAY", "count": 200}'
# 市场统计
curl "http://localhost:8000/api/v1/market/stat"
# 板块信息
# 板块信息(标准协议)
curl "http://localhost:8000/api/v1/block?filename=block_gn.dat"
# ── 板块分析(MAC 协议)──
# 行业板块列表
curl "http://localhost:8000/api/v1/board-mac/list?board_type=HY&count=50"
# 板块成分股(按涨幅排序)
curl "http://localhost:8000/api/v1/board-mac/members?board_symbol=881001&count=20"
# 个股所属板块
curl "http://localhost:8000/api/v1/board-mac/belong?market=SZ&code=000001"
# 板块摘要(含主力净流入、涨跌家数)
curl "http://localhost:8000/api/v1/board-mac/summary?board_symbol=881001"
# 行业板块涨幅排名 Top 10
curl "http://localhost:8000/api/v1/board-mac/ranking?board_type=HY&top_n=10"
# 板块 20 日涨幅排行
curl "http://localhost:8000/api/v1/board-mac/change-ranking?board_type=HY&days=20&top_n=10"
# ── 资金 / 信息 ──
# 个股资金流向(主力/散户净流入)
curl "http://localhost:8000/api/v1/mac/capital-flow?market=SH&code=600519"
# 个股基本信息快照
curl "http://localhost:8000/api/v1/mac/symbol-info?market=SZ&code=000001"
# 服务器交易时段信息
curl "http://localhost:8000/api/v1/mac/server-info"
# ── 排行 / 竞价 / 异动 ──
# 全 A 涨幅排行前 20
curl "http://localhost:8000/api/v1/mac/quote-list?category=A&count=20&sort_type=CHANGE_PCT"
# 集合竞价数据
curl "http://localhost:8000/api/v1/mac/auction?market=SZ&code=000001"
# 市场异动行情
curl "http://localhost:8000/api/v1/mac/unusual?market=SH&count=50"
# ── 扩展市场(期货/港股/美股)──
# 港股 K 线
curl "http://localhost:8000/api/v1/ex/bars?market=HK_MAIN_BOARD&code=00700&category=DAY&count=30"
# 美股实时报价
curl "http://localhost:8000/api/v1/ex/quote?market=US_STOCK&code=AAPL"
# ── 技术指标 ──
# 列出所有可用指标
curl "http://localhost:8000/api/v1/indicator/list"
# 计算 MACD + KDJ 指标
curl -X POST "http://localhost:8000/api/v1/indicator/compute" \
-H "Content-Type: application/json" \
-d '{"data": [{"open":10,"close":10.5,"high":11,"low":9.5,"vol":1000}], "indicators": ["MACD", "KDJ"]}'
# ── 缠论分析 ──
curl -X POST "http://localhost:8000/api/v1/chanlun/analyze" \
-H "Content-Type: application/json" \
-d '{"market": "SZ", "code": "000001", "category": "DAY", "count": 200}'
```
### WebSocket 实时行情
@@ -1415,6 +1471,21 @@ ruff format --check src/ tests/ # format check
## Changelog
### 1.10.2 (2026-06-12)
**Web API 全面补齐** — 新增 18 个 REST 端点,Web API 与 CLI 接口覆盖对齐。
- **板块分析(6 端点)**:板块列表、成分股、所属板块、板块摘要、涨幅排名、N日涨幅排行
- **资金/信息(3 端点)**:个股资金流向、个股信息快照、服务器交易时段
- **排行/竞价/异动(3 端点)**:分类排序行情列表、集合竞价、市场异动
- **扩展市场(4 端点)**:港股/美股/期货的 K 线、报价、分时、逐笔成交
- **技术指标(2 端点)**:指标列表、指标计算(POST)
- 新增 `AsyncMacClient` 依赖注入(`get_mac_client`),Web 层同时管理 TDX + MAC 双客户端连接
- 新增 `AsyncExTdxClient` 依赖注入(`get_ex_client`),可选启用扩展市场端点
- 新增 6 个 MAC 枚举转换器(BoardType/SortType/SortOrder/Category/ExMarket/FilterType
- 新增 `DictResponse``ComputeIndicatorsRequest` schemas
- Web API 端点总数从 22 增至 40
### 1.10.0 (2026-06-12)
**Web API 层** — 新增 FastAPI REST + WebSocket 服务,一键将 easy-tdx 暴露为 HTTP API。
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "easy-tdx"
version = "1.10.0"
version = "1.10.2"
description = "通达信 TCP 协议行情数据客户端,支持在线行情、离线数据读取与写入同步"
readme = "README.md"
requires-python = ">=3.10"
+64 -5
View File
@@ -19,6 +19,7 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
"""管理 TDX 连接生命周期:启动时连接,关闭时断开。"""
from easy_tdx.client import AsyncTdxClient
# --- 标准 TDX 客户端 ---
host = app.state.tdx_host
port = app.state.tdx_port
timeout = app.state.tdx_timeout
@@ -31,19 +32,60 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
logger.warning("TDX client connection failed — endpoints will return 503")
app.state.tdx_client = client
# --- MAC 协议客户端 ---
mac_client = None
enable_mac = getattr(app.state, "enable_mac", True)
if enable_mac:
try:
from easy_tdx.mac.client import AsyncMacClient
mac_client = AsyncMacClient.from_best_host()
await mac_client.connect()
logger.info("MAC client connected")
except Exception:
logger.warning("MAC client connection failed — MAC endpoints will return 503")
mac_client = None
app.state.mac_client = mac_client
# --- 扩展市场客户端(可选) ---
ex_client = None
enable_ex = getattr(app.state, "enable_ex", False)
if enable_ex:
try:
from easy_tdx.ex.client import AsyncExTdxClient
ex_client = AsyncExTdxClient.from_best_host()
await ex_client.connect()
logger.info("Ex market client connected")
except Exception:
logger.warning("Ex market client connection failed — Ex endpoints will return 503")
ex_client = None
app.state.ex_client = ex_client
yield
try:
await client.close()
logger.info("TDX client disconnected")
except Exception:
pass
# --- 依次关闭 ---
for name, cli in [
("Ex market client", ex_client),
("MAC client", mac_client),
("TDX client", client),
]:
if cli is not None:
try:
await cli.close()
logger.info("%s disconnected", name)
except Exception:
pass
def _create_app(
host: str | None = None,
port: int | None = None,
timeout: float | None = None,
*,
enable_mac: bool = True,
enable_ex: bool = False,
) -> FastAPI:
"""创建并配置 FastAPI 应用实例。"""
from easy_tdx.config import get_best_host, get_port, get_timeout
@@ -67,6 +109,10 @@ def _create_app(
app.state.tdx_port = port
app.state.tdx_timeout = timeout
app.state.tdx_client = None # will be set in lifespan
app.state.mac_client = None
app.state.ex_client = None
app.state.enable_mac = enable_mac
app.state.enable_ex = enable_ex
# CORS middleware (permissive for development)
app.add_middleware(
@@ -83,8 +129,13 @@ def _create_app(
# Mount routers
from easy_tdx.web.routers.bars import router as bars_router
from easy_tdx.web.routers.block import router as block_router
from easy_tdx.web.routers.board_mac import router as board_mac_router
from easy_tdx.web.routers.chanlun import router as chanlun_router
from easy_tdx.web.routers.ex_market import router as ex_market_router
from easy_tdx.web.routers.finance import router as finance_router
from easy_tdx.web.routers.indicator import router as indicator_router
from easy_tdx.web.routers.mac_data import router as mac_data_router
from easy_tdx.web.routers.mac_quotes import router as mac_quotes_router
from easy_tdx.web.routers.market import router as market_router
from easy_tdx.web.routers.realtime import router as realtime_router
@@ -94,5 +145,13 @@ def _create_app(
app.include_router(block_router, prefix="/api/v1")
app.include_router(chanlun_router, prefix="/api/v1")
app.include_router(realtime_router, prefix="/api/v1")
# MAC 协议路由
app.include_router(board_mac_router, prefix="/api/v1")
app.include_router(mac_data_router, prefix="/api/v1")
app.include_router(mac_quotes_router, prefix="/api/v1")
# 扩展市场路由
app.include_router(ex_market_router, prefix="/api/v1")
# 技术指标路由
app.include_router(indicator_router, prefix="/api/v1")
return app
+115 -1
View File
@@ -1,4 +1,4 @@
"""共享参数转换工具(market/category 字符串 → 枚举)。"""
"""共享参数转换工具(字符串 → 枚举)。"""
from __future__ import annotations
@@ -24,6 +24,11 @@ def market_from_str(s: str) -> Any:
raise ValueError(f"无效市场代码 '{s}',可选值: {valid}") from None
def market_value_from_str(s: str) -> int:
"""将市场字符串转为 int 值(MAC 客户端使用 int 而非枚举)。"""
return int(market_from_str(s).value)
def category_from_str(s: str) -> Any:
"""将字符串转为 KlineCategory 枚举,支持大小写和数字字符串。"""
from easy_tdx.models.enums import KlineCategory
@@ -39,3 +44,112 @@ def category_from_str(s: str) -> Any:
except KeyError:
valid = ", ".join(c.name for c in KlineCategoryEnum)
raise ValueError(f"无效K线周期 '{s}',可选值: {valid}") from None
# ---------------------------------------------------------------------------
# MAC 枚举转换器
# ---------------------------------------------------------------------------
def board_type_from_str(s: str) -> Any:
"""将字符串转为 BoardType 枚举(ALL/HY/HY2/GN/FG/DQ/...)。"""
from easy_tdx.mac.enums import BoardType
key = s.upper()
try:
return BoardType[key]
except KeyError:
pass
try:
return BoardType(int(key))
except (ValueError, TypeError):
pass
valid = ", ".join(m.name for m in BoardType)
raise ValueError(f"无效板块类型 '{s}',可选值: {valid}") from None
def sort_type_from_str(s: str) -> Any:
"""将字符串转为 SortType 枚举(CHANGE_PCT/VOLUME/... 或 hex 数字)。"""
from easy_tdx.mac.enums import SortType
key = s.upper()
try:
return SortType[key]
except KeyError:
pass
try:
return SortType(int(key, 0)) # 支持 hex 如 "0x0E"
except (ValueError, TypeError):
pass
valid = ", ".join(m.name for m in SortType)
raise ValueError(f"无效排序字段 '{s}',可选值: {valid}") from None
def sort_order_from_str(s: str) -> Any:
"""将字符串转为 SortOrder 枚举(ASC/DESC)。"""
from easy_tdx.mac.enums import SortOrder
key = s.upper()
try:
return SortOrder[key]
except KeyError:
pass
try:
return SortOrder(int(key))
except (ValueError, TypeError):
pass
valid = ", ".join(m.name for m in SortOrder)
raise ValueError(f"无效排序方向 '{s}',可选值: {valid}") from None
def category_mac_from_str(s: str) -> Any:
"""将字符串转为 MAC Category 枚举(A/SH/SZ/KCB/BJ/CYB/...)。"""
from easy_tdx.mac.enums import Category
key = s.upper()
try:
return Category[key]
except KeyError:
pass
try:
return Category(int(key))
except (ValueError, TypeError):
pass
valid = ", ".join(m.name for m in Category if m < 10000)
raise ValueError(f"无效市场分类 '{s}',可选值: {valid}") from None
def ex_market_from_str(s: str) -> int:
"""将字符串转为 ExMarket 整数值(HK_MAIN_BOARD/COMEX_FUTURES/... 或数字)。"""
from easy_tdx.mac.enums import ExMarket
try:
return int(ExMarket[s.upper()])
except KeyError:
pass
try:
return int(s)
except (ValueError, TypeError):
pass
valid = ", ".join(m.name for m in ExMarket)
raise ValueError(f"无效扩展市场代码 '{s}',可选值: {valid}") from None
def filter_types_from_str(s: str) -> list[Any]:
"""将逗号分隔字符串转为 FilterType 列表(ST,KC,BJ,...)。"""
from easy_tdx.mac.enums import FilterType
if not s:
return []
result: list[Any] = []
for part in s.split(","):
key = part.strip().upper()
try:
result.append(FilterType[key])
except KeyError:
try:
result.append(FilterType(int(key)))
except (ValueError, TypeError):
valid = ", ".join(m.name for m in FilterType)
raise ValueError(f"无效过滤标志 '{part}',可选值: {valid}") from None
return result
+18
View File
@@ -2,6 +2,8 @@
from __future__ import annotations
from typing import Any
from fastapi import Request
from easy_tdx.client import AsyncTdxClient
@@ -11,3 +13,19 @@ def get_client(request: Request) -> AsyncTdxClient:
"""从 app.state 获取共享的 AsyncTdxClient 实例。"""
client: AsyncTdxClient = request.app.state.tdx_client
return client
def get_mac_client(request: Request) -> Any:
"""从 app.state 获取共享的 AsyncMacClient 实例。"""
client: Any = request.app.state.mac_client
return client
def get_ex_client(request: Request) -> Any:
"""从 app.state 获取共享的 AsyncExTdxClient 实例(可选)。"""
client: Any | None = request.app.state.ex_client
if client is None:
from easy_tdx.exceptions import TdxConnectionError
raise TdxConnectionError("扩展市场客户端未启用")
return client
+116
View File
@@ -0,0 +1,116 @@
"""板块分析路由:板块列表、成分、归属、摘要、涨幅排名、N日涨幅。"""
from __future__ import annotations
from typing import Any
from fastapi import APIRouter, Depends, Query
from easy_tdx.web.convert import (
board_type_from_str,
market_value_from_str,
sort_order_from_str,
sort_type_from_str,
)
from easy_tdx.web.deps import get_mac_client
from easy_tdx.web.schemas import DataFrameResponse, DictResponse
router = APIRouter(tags=["board-mac"])
def _df_resp(df: Any) -> DataFrameResponse:
return DataFrameResponse.from_dataframe(df)
@router.get("/board-mac/list", response_model=DataFrameResponse)
async def board_list(
board_type: str = Query("ALL", description="板块类型: ALL/HY/HY2/GN/FG/DQ"),
count: int = Query(500, ge=1, le=50000),
client: Any = Depends(get_mac_client),
) -> DataFrameResponse:
"""获取板块列表。"""
df = await client.get_board_list(board_type=board_type_from_str(board_type), count=count)
return _df_resp(df)
@router.get("/board-mac/members", response_model=DataFrameResponse)
async def board_members(
board_symbol: str = Query(..., description="板块代码,如 881001"),
count: int = Query(100, ge=1, le=100000),
sort_type: str = Query("CHANGE_PCT", description="排序字段"),
sort_order: str = Query("DESC", description="排序方向: ASC/DESC"),
client: Any = Depends(get_mac_client),
) -> DataFrameResponse:
"""获取板块成分股。"""
df = await client.get_board_members(
board_symbol=board_symbol,
count=count,
sort_type=sort_type_from_str(sort_type),
sort_order=sort_order_from_str(sort_order),
)
return _df_resp(df)
@router.get("/board-mac/belong", response_model=DataFrameResponse)
async def board_belong(
market: str = Query(..., description="市场: SZ, SH"),
code: str = Query(..., min_length=6, max_length=6, description="6位股票代码"),
client: Any = Depends(get_mac_client),
) -> DataFrameResponse:
"""获取股票所属板块列表。"""
df = await client.get_belong_board(market=market_value_from_str(market), code=code)
return _df_resp(df)
@router.get("/board-mac/summary", response_model=DictResponse)
async def board_summary(
board_symbol: str = Query(..., description="板块代码,如 881001"),
sort_type: str = Query("CHANGE_PCT", description="排序字段"),
sort_order: str = Query("DESC", description="排序方向: ASC/DESC"),
client: Any = Depends(get_mac_client),
) -> DictResponse:
"""获取板块摘要信息(含成分股资金流向)。"""
result = await client.get_board_summary(
board_symbol=board_symbol,
sort_type=sort_type_from_str(sort_type),
sort_order=sort_order_from_str(sort_order),
)
return DictResponse.from_dict(result)
@router.get("/board-mac/ranking", response_model=DataFrameResponse)
async def board_ranking(
board_type: str = Query("HY", description="板块类型: HY/HY2/GN/FG/DQ"),
top_n: int = Query(10, ge=1, le=200),
sort_by: str = Query("change_pct", description="排序字段名"),
ascending: bool = Query(False, description="是否升序"),
client: Any = Depends(get_mac_client),
) -> DataFrameResponse:
"""获取板块涨幅排名。"""
df = await client.get_board_ranking(
board_type=board_type_from_str(board_type),
top_n=top_n,
sort_by=sort_by,
ascending=ascending,
)
return _df_resp(df)
@router.get("/board-mac/change-ranking", response_model=DataFrameResponse)
async def board_change_ranking(
board_type: str = Query("HY", description="板块类型: HY/HY2/GN/FG/DQ"),
days: int = Query(20, ge=1, le=250, description="统计天数"),
top_n: int = Query(10, ge=1, le=200),
target_date: int | None = Query(None, description="目标日期,如 20250101"),
ascending: bool = Query(False, description="是否升序"),
client: Any = Depends(get_mac_client),
) -> DataFrameResponse:
"""获取板块 N 日涨幅排名。"""
df = await client.get_board_change_ranking(
board_type=board_type_from_str(board_type),
target_date=target_date,
days=days,
top_n=top_n,
ascending=ascending,
)
return _df_resp(df)
+83
View File
@@ -0,0 +1,83 @@
"""扩展市场路由:期货、港股、美股等扩展市场行情数据。"""
from __future__ import annotations
from dataclasses import asdict
from typing import Any
from fastapi import APIRouter, Depends, Query
from easy_tdx.web.convert import category_from_str, ex_market_from_str
from easy_tdx.web.deps import get_ex_client
from easy_tdx.web.schemas import DataFrameResponse
router = APIRouter(tags=["ex-market"])
def _records_to_df_resp(records: list[Any]) -> DataFrameResponse:
"""将 dataclass 列表转为 DataFrameResponse。"""
import pandas as pd
if not records:
return DataFrameResponse(data=[], count=0)
df = pd.DataFrame([asdict(r) for r in records])
return DataFrameResponse.from_dataframe(df)
@router.get("/ex/bars", response_model=DataFrameResponse)
async def ex_bars(
market: str = Query(..., description="扩展市场代码,如 HK_MAIN_BOARD 或数字"),
code: str = Query(..., description="合约/证券代码"),
category: str = Query("DAY", description="K线周期: MIN_1/MIN_5/.../DAY/WEEK/MONTH"),
start: int = Query(0, ge=0),
count: int = Query(700, ge=1, le=700),
client: Any = Depends(get_ex_client),
) -> DataFrameResponse:
"""获取扩展市场 K 线数据。"""
records = await client.get_instrument_bars(
category=int(category_from_str(category)),
market=ex_market_from_str(market),
code=code,
start=start,
count=count,
)
return _records_to_df_resp(records)
@router.get("/ex/quote", response_model=DataFrameResponse)
async def ex_quote(
market: str = Query(..., description="扩展市场代码"),
code: str = Query(..., description="合约/证券代码"),
client: Any = Depends(get_ex_client),
) -> DataFrameResponse:
"""获取扩展市场实时报价。"""
result = await client.get_instrument_quote(market=ex_market_from_str(market), code=code)
if result is None:
return DataFrameResponse(data=[], count=0)
return _records_to_df_resp([result])
@router.get("/ex/minute", response_model=DataFrameResponse)
async def ex_minute(
market: str = Query(..., description="扩展市场代码"),
code: str = Query(..., description="合约/证券代码"),
client: Any = Depends(get_ex_client),
) -> DataFrameResponse:
"""获取扩展市场分时数据。"""
records = await client.get_minute_time_data(market=ex_market_from_str(market), code=code)
return _records_to_df_resp(records)
@router.get("/ex/transaction", response_model=DataFrameResponse)
async def ex_transaction(
market: str = Query(..., description="扩展市场代码"),
code: str = Query(..., description="合约/证券代码"),
start: int = Query(0, ge=0),
count: int = Query(1800, ge=1, le=3000),
client: Any = Depends(get_ex_client),
) -> DataFrameResponse:
"""获取扩展市场逐笔成交数据。"""
records = await client.get_transaction_data(
market=ex_market_from_str(market), code=code, start=start, count=count
)
return _records_to_df_resp(records)
+45
View File
@@ -0,0 +1,45 @@
"""技术指标路由:指标列表、指标计算。"""
from __future__ import annotations
from typing import Any
import pandas as pd
from fastapi import APIRouter
from easy_tdx.web.schemas import ComputeIndicatorsRequest, DataFrameResponse
router = APIRouter(tags=["indicator"])
def _df_resp(df: Any) -> DataFrameResponse:
return DataFrameResponse.from_dataframe(df)
@router.get("/indicator/list")
async def indicator_list() -> list[dict[str, Any]]:
"""获取所有可用技术指标列表。"""
from easy_tdx.indicator import list_indicators
return list_indicators()
@router.post("/indicator/compute", response_model=DataFrameResponse)
async def indicator_compute(
req: ComputeIndicatorsRequest,
) -> DataFrameResponse:
"""在 OHLCV 数据上计算技术指标。
请求体包含 K 线 records 和指标名称列表,返回计算后的 DataFrame。
"""
from easy_tdx.indicator import compute_indicators
df = pd.DataFrame(req.data)
result = compute_indicators(
df,
indicators=req.indicators,
params=req.params,
keep_ohlcv=req.keep_ohlcv,
tail=req.tail,
)
return _df_resp(result)
+48
View File
@@ -0,0 +1,48 @@
"""MAC 数据路由:资金流向、个股信息、服务器信息。"""
from __future__ import annotations
from typing import Any
from fastapi import APIRouter, Depends, Query
from easy_tdx.web.convert import market_value_from_str
from easy_tdx.web.deps import get_mac_client
from easy_tdx.web.schemas import DataFrameResponse
router = APIRouter(tags=["mac-data"])
def _df_resp(df: Any) -> DataFrameResponse:
return DataFrameResponse.from_dataframe(df)
@router.get("/mac/capital-flow", response_model=DataFrameResponse)
async def capital_flow(
market: str = Query(..., description="市场: SZ, SH"),
code: str = Query(..., min_length=6, max_length=6, description="6位股票代码"),
client: Any = Depends(get_mac_client),
) -> DataFrameResponse:
"""获取个股资金流向(主力/散户净流入)。"""
df = await client.get_capital_flow(market=market_value_from_str(market), code=code)
return _df_resp(df)
@router.get("/mac/symbol-info", response_model=DataFrameResponse)
async def symbol_info(
market: str = Query(..., description="市场: SZ, SH"),
code: str = Query(..., min_length=6, max_length=6, description="6位股票代码"),
client: Any = Depends(get_mac_client),
) -> DataFrameResponse:
"""获取个股基本信息快照。"""
df = await client.get_symbol_info(market=market_value_from_str(market), code=code)
return _df_resp(df)
@router.get("/mac/server-info", response_model=DataFrameResponse)
async def server_info(
client: Any = Depends(get_mac_client),
) -> DataFrameResponse:
"""获取服务器交易时段信息。"""
df = await client.get_server_info()
return _df_resp(df)
+69
View File
@@ -0,0 +1,69 @@
"""MAC 行情路由:排行行情列表、竞价数据、异动行情。"""
from __future__ import annotations
from typing import Any
from fastapi import APIRouter, Depends, Query
from easy_tdx.web.convert import (
category_mac_from_str,
filter_types_from_str,
market_value_from_str,
sort_order_from_str,
sort_type_from_str,
)
from easy_tdx.web.deps import get_mac_client
from easy_tdx.web.schemas import DataFrameResponse
router = APIRouter(tags=["mac-quotes"])
def _df_resp(df: Any) -> DataFrameResponse:
return DataFrameResponse.from_dataframe(df)
@router.get("/mac/quote-list", response_model=DataFrameResponse)
async def quote_list(
category: str = Query("A", description="市场分类: A/SH/SZ/KCB/BJ/CYB"),
start: int = Query(0, ge=0, description="分页起始位置"),
count: int = Query(80, ge=1, le=5000, description="返回数量"),
sort_type: str = Query("CHANGE_PCT", description="排序字段"),
sort_order: str = Query("DESC", description="排序方向: ASC/DESC"),
exclude: str | None = Query(None, description="过滤标志(逗号分隔): ST,KC,BJ,..."),
client: Any = Depends(get_mac_client),
) -> DataFrameResponse:
"""获取分排行行情列表(涨幅/成交量/换手等排序)。"""
exclude_flags = filter_types_from_str(exclude) if exclude else None
df = await client.get_stock_quotes_list(
category=category_mac_from_str(category),
start=start,
count=count,
sort_type=sort_type_from_str(sort_type),
sort_order=sort_order_from_str(sort_order),
exclude_flags=exclude_flags,
)
return _df_resp(df)
@router.get("/mac/auction", response_model=DataFrameResponse)
async def auction(
market: str = Query(..., description="市场: SZ, SH"),
code: str = Query(..., min_length=6, max_length=6, description="6位股票代码"),
client: Any = Depends(get_mac_client),
) -> DataFrameResponse:
"""获取集合竞价数据。"""
df = await client.get_auction(market=market_value_from_str(market), code=code)
return _df_resp(df)
@router.get("/mac/unusual", response_model=DataFrameResponse)
async def unusual(
market: str = Query(..., description="市场: SZ, SH"),
start: int = Query(0, ge=0, description="分页起始位置"),
count: int = Query(50, ge=1, le=500, description="返回数量"),
client: Any = Depends(get_mac_client),
) -> DataFrameResponse:
"""获取市场异动行情数据。"""
df = await client.get_unusual(market=market_value_from_str(market), start=start, count=count)
return _df_resp(df)
+36
View File
@@ -65,6 +65,18 @@ class ChanlunRequest(BaseModel):
start: int = Field(default=0, ge=0)
class ComputeIndicatorsRequest(BaseModel):
"""技术指标计算请求。"""
data: list[dict[str, Any]] = Field(..., description="OHLCV records")
indicators: list[str] = Field(..., min_length=1, description="指标名称列表")
params: dict[str, dict[str, int | float]] | None = Field(
default=None, description="指标参数(可选)"
)
keep_ohlcv: bool = Field(default=True, description="保留原始 OHLCV 列")
tail: int | None = Field(default=None, ge=1, description="仅返回末尾 N 行")
# ---------------------------------------------------------------------------
# Response models
# ---------------------------------------------------------------------------
@@ -87,6 +99,7 @@ class DataFrameResponse(BaseModel):
for row in records:
clean_row: dict[str, Any] = {}
for k, v in row.items():
assert isinstance(k, str)
if hasattr(v, "isoformat"):
clean_row[k] = v.isoformat()
elif hasattr(v, "item"):
@@ -99,6 +112,29 @@ class DataFrameResponse(BaseModel):
return cls(data=[], count=0)
class DictResponse(BaseModel):
"""通用 dict 响应(用于非 DataFrame 返回值)。"""
data: dict[str, Any]
@classmethod
def from_dict(cls, d: dict[str, Any]) -> DictResponse:
"""序列化 dict,将其中的 DataFrame 转为 records 格式。"""
import pandas as pd
cleaned: dict[str, Any] = {}
for k, v in d.items():
if isinstance(v, pd.DataFrame):
cleaned[k] = DataFrameResponse.from_dataframe(v).data
elif hasattr(v, "isoformat"):
cleaned[k] = v.isoformat()
elif hasattr(v, "item"):
cleaned[k] = v.item()
else:
cleaned[k] = v
return cls(data=cleaned)
class CountResponse(BaseModel):
"""简单计数响应。"""