mirror of
https://ghfast.top/https://github.com/aeroxw/easy-tdx.git
synced 2026-09-12 21:34:16 +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:
+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