mirror of
https://ghfast.top/https://github.com/aeroxw/easy-tdx.git
synced 2026-09-12 15:44:15 +08:00
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:
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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):
|
||||
"""简单计数响应。"""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user