K线数据首选mootdx通达信TCP源,板块数据增加mootdx降级
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
015aeb989a
commit
86535f5de2
@@ -2,4 +2,5 @@ fastapi==0.115.0
|
|||||||
uvicorn==0.30.0
|
uvicorn==0.30.0
|
||||||
httpx==0.27.0
|
httpx==0.27.0
|
||||||
python-dotenv==1.0.1
|
python-dotenv==1.0.1
|
||||||
akshare==1.18.64
|
akshare==1.18.64
|
||||||
|
mootdx
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
"""板块数据路由:行业板块、概念板块"""
|
"""板块数据路由:行业板块、概念板块"""
|
||||||
|
|
||||||
from fastapi import APIRouter, Query, HTTPException
|
from fastapi import APIRouter, Query, HTTPException
|
||||||
from services import eastmoney
|
from services import eastmoney, mootdx
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@@ -14,4 +14,12 @@ async def sector_list(
|
|||||||
raise HTTPException(status_code=400, detail="板块类型错误,仅支持 industry/concept")
|
raise HTTPException(status_code=400, detail="板块类型错误,仅支持 industry/concept")
|
||||||
|
|
||||||
data = await eastmoney.fetch_sector_list(type)
|
data = await eastmoney.fetch_sector_list(type)
|
||||||
return {"data": data, "count": len(data), "type": 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
@@ -3,7 +3,7 @@
|
|||||||
from fastapi import APIRouter, Query, HTTPException
|
from fastapi import APIRouter, Query, HTTPException
|
||||||
import re
|
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
|
from models import StockSearchResult, StockQuote, KLineData, FundFlowData, FundFlowSummary, CompanyProfile, FinancialReportItem, FinancialDataResponse
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
@@ -54,21 +54,28 @@ async def stock_history(
|
|||||||
if not re.match(r"^\d{6}$", code):
|
if not re.match(r"^\d{6}$", code):
|
||||||
raise HTTPException(status_code=400, detail="股票代码格式错误,需为6位数字")
|
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)
|
em_klines = await eastmoney.fetch_kline_history(code, days)
|
||||||
if em_klines and len(em_klines) >= 2:
|
if em_klines and len(em_klines) >= 2:
|
||||||
return {"data": em_klines, "count": len(em_klines), "source": "eastmoney"}
|
return {"data": em_klines, "count": len(em_klines), "source": "eastmoney"}
|
||||||
|
|
||||||
# 降级1:腾讯(含涨跌幅)
|
# 降级2:腾讯(含涨跌幅)
|
||||||
tencent_klines = await tencent.fetch_history(code, days)
|
tencent_klines = await tencent.fetch_history(code, days)
|
||||||
if tencent_klines and len(tencent_klines) >= 2:
|
if tencent_klines and len(tencent_klines) >= 2:
|
||||||
return {"data": tencent_klines, "count": len(tencent_klines), "source": "tencent"}
|
return {"data": tencent_klines, "count": len(tencent_klines), "source": "tencent"}
|
||||||
|
|
||||||
# 降级2:新浪
|
# 降级3:新浪
|
||||||
sina_klines = await sina.fetch_history(code, days)
|
sina_klines = await sina.fetch_history(code, days)
|
||||||
if sina_klines:
|
if sina_klines:
|
||||||
return {"data": sina_klines, "count": len(sina_klines), "source": "sina"}
|
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:
|
if em_klines:
|
||||||
return {"data": em_klines, "count": len(em_klines), "source": "eastmoney"}
|
return {"data": em_klines, "count": len(em_klines), "source": "eastmoney"}
|
||||||
if tencent_klines:
|
if tencent_klines:
|
||||||
|
|||||||
@@ -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
|
||||||
Reference in New Issue
Block a user