K线数据首选mootdx通达信TCP源,板块数据增加mootdx降级

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Sakurasan
2026-07-08 02:09:57 +08:00
co-authored by Claude Opus 4.7
parent 015aeb989a
commit 86535f5de2
4 changed files with 151 additions and 7 deletions
+1
View File
@@ -3,3 +3,4 @@ uvicorn==0.30.0
httpx==0.27.0
python-dotenv==1.0.1
akshare==1.18.64
mootdx
+9 -1
View File
@@ -1,7 +1,7 @@
"""板块数据路由:行业板块、概念板块"""
from fastapi import APIRouter, Query, HTTPException
from services import eastmoney
from services import eastmoney, mootdx
router = APIRouter()
@@ -14,4 +14,12 @@ async def sector_list(
raise HTTPException(status_code=400, detail="板块类型错误,仅支持 industry/concept")
data = await eastmoney.fetch_sector_list(type)
if data:
return {"data": data, "count": len(data), "type": type}
# 降级:通达信 mootdx(不含实时资金流数据)
md_data = await mootdx.fetch_sector_list(type)
if md_data:
return {"data": md_data, "count": len(md_data), "type": type, "source": "mootdx"}
return {"data": [], "count": 0, "type": type}
+11 -4
View File
@@ -3,7 +3,7 @@
from fastapi import APIRouter, Query, HTTPException
import re
from services import tencent, sina, eastmoney
from services import tencent, sina, eastmoney, mootdx
from models import StockSearchResult, StockQuote, KLineData, FundFlowData, FundFlowSummary, CompanyProfile, FinancialReportItem, FinancialDataResponse
router = APIRouter()
@@ -54,21 +54,28 @@ async def stock_history(
if not re.match(r"^\d{6}$", code):
raise HTTPException(status_code=400, detail="股票代码格式错误,需为6位数字")
# 主数据源:东方财富 push2his(含成交额/涨跌幅/振幅/换手率,可能被限流
# 主数据源:通达信 mootdx(TCP直连,不被限流,稳定可靠
md_klines = await mootdx.fetch_kline_history(code, days)
if md_klines and len(md_klines) >= 2:
return {"data": md_klines, "count": len(md_klines), "source": "mootdx"}
# 降级1:东方财富 push2his(含成交额/涨跌幅/振幅/换手率)
em_klines = await eastmoney.fetch_kline_history(code, days)
if em_klines and len(em_klines) >= 2:
return {"data": em_klines, "count": len(em_klines), "source": "eastmoney"}
# 降级1:腾讯(含涨跌幅)
# 降级2:腾讯(含涨跌幅)
tencent_klines = await tencent.fetch_history(code, days)
if tencent_klines and len(tencent_klines) >= 2:
return {"data": tencent_klines, "count": len(tencent_klines), "source": "tencent"}
# 降级2:新浪
# 降级3:新浪
sina_klines = await sina.fetch_history(code, days)
if sina_klines:
return {"data": sina_klines, "count": len(sina_klines), "source": "sina"}
if md_klines:
return {"data": md_klines, "count": len(md_klines), "source": "mootdx"}
if em_klines:
return {"data": em_klines, "count": len(em_klines), "source": "eastmoney"}
if tencent_klines:
+128
View File
@@ -0,0 +1,128 @@
"""通达信数据源(mootdx TCP直连通达信服务器)
通过 TCP 协议直连通达信行情服务器,不走 HTTP,不会被限流。
主要用途:
- K线数据:主数据源(稳定可靠)
- 板块数据:东方财富 push2 的降级方案
"""
import asyncio
from typing import Optional, List
def _get_market(code: str) -> str:
return "sh" if code.startswith("6") else "sz"
def _create_client():
from mootdx.quotes import Quotes
return Quotes.factory(market="std", multithread=True, heartbeat=True)
def _sync_fetch_kline(code: str, days: int) -> Optional[list]:
client = _create_client()
klines = client.bars(symbol=code, frequency=9, offset=min(days, 800))
if klines is None or len(klines) == 0:
return None
result = []
prev_close = 0.0
for bar in reversed(klines):
close = float(bar.close)
change_pct = 0
if prev_close > 0:
change_pct = (close - prev_close) / prev_close * 100
result.append(
{
"date": (
bar.datetime.strftime("%Y-%m-%d")
if hasattr(bar.datetime, "strftime")
else str(bar.datetime)[:10]
),
"open": float(bar.open),
"close": close,
"high": float(bar.high),
"low": float(bar.low),
"volume": int(bar.vol) if hasattr(bar, "vol") else 0,
"turnover": (
float(bar.amount) if hasattr(bar, "amount") and bar.amount else 0
),
"changePercent": round(change_pct, 2),
}
)
prev_close = close
result.sort(key=lambda x: x["date"])
return result
async def fetch_kline_history(code: str, days: int = 90) -> Optional[List[dict]]:
"""获取日K线(TCP直连通达信,主数据源)"""
try:
loop = asyncio.get_event_loop()
return await loop.run_in_executor(None, _sync_fetch_kline, code, days)
except ImportError:
return None
except Exception as e:
print(f"[mootdx] fetch_kline error: {e}")
return None
# ---- 板块数据(东方财富降级方案)----
def _sync_fetch_sectors(sector_type: str) -> Optional[list]:
from mootdx.consts import MARKET_SH, MARKET_SZ
client = _create_client()
# block() 返回 DataFrame,列:code, name 等
# 按板块类型过滤
block_df = client.block()
if block_df is None or block_df.empty:
return None
items = []
for _, row in block_df.iterrows():
name = str(row.get("name", "") or row.get("blockname", ""))
code = str(row.get("code", "") or row.get("blockcode", ""))
if not code or not name:
continue
items.append(
{
"code": code,
"name": name,
"level": None,
"changePercent": None,
"changeAmount": None,
"mainNetInflow": 0,
"mainNetInflowPercent": None,
"superLargeInflow": None,
"superLargeInflowPercent": None,
"largeInflow": None,
"largeInflowPercent": None,
"mediumInflow": None,
"mediumInflowPercent": None,
"smallInflow": None,
"smallInflowPercent": None,
"turnover": 0,
}
)
return items if items else None
async def fetch_sector_list(sector_type: str) -> Optional[List[dict]]:
"""获取板块列表(东方财富的降级方案,仅含代码和名称)"""
try:
loop = asyncio.get_event_loop()
return await loop.run_in_executor(None, _sync_fetch_sectors, sector_type)
except ImportError:
return None
except Exception as e:
print(f"[mootdx] fetch_sectors error: {e}")
return None