Files
auv/backend/services/eastmoney.py
T
2026-07-06 19:44:40 +08:00

774 lines
26 KiB
Python

"""东方财富 API 客户端(资金流向数据)"""
import asyncio
import httpx
import json
import os
import re
from datetime import datetime
from typing import Optional, List
from services.cache import get_cache, set_cache
# ---- API Key 轮询 ----
_api_keys: List[str] = []
_key_index: int = -1
def _load_api_keys() -> List[str]:
"""收集所有可用 MX API key(支持逗号分隔和 MX_APIKEY_1 编号后缀)"""
keys: List[str] = []
seen: set = set()
def add(k: str):
k = k.strip()
if k and k not in seen:
seen.add(k)
keys.append(k)
raw = os.environ.get("MX_APIKEY", "")
if raw:
for k in raw.split(","):
add(k)
for i in range(1, 10):
raw = os.environ.get(f"MX_APIKEY_{i}", "")
if raw:
for k in raw.split(","):
add(k)
return keys
def _ensure_keys() -> bool:
global _api_keys, _key_index
if not _api_keys:
_api_keys = _load_api_keys()
if _key_index == -1:
val = get_cache("_mx_key_index")
_key_index = int(val) if val else 0
return bool(_api_keys)
def _get_key() -> str:
"""获取当前使用的 API key"""
if not _ensure_keys():
return ""
return _api_keys[_key_index % len(_api_keys)]
def _rotate_key():
"""切换到下一个 key 并持久化当前索引"""
global _key_index
_key_index += 1
set_cache("_mx_key_index", str(_key_index), ttl_hours=24)
# ---- 工具函数 ----
def get_eastmoney_market(code: str) -> str:
"""获取东方财富格式的市场标识"""
if code.startswith("688"):
return "6"
if code.startswith("60"):
return "1"
return "0"
def parse_amount(text) -> float:
"""解析金额文本(支持 1.432亿元, -9915万元, 0元)"""
if isinstance(text, (int, float)):
return float(text)
if not text or not isinstance(text, str):
return 0
text = text.strip()
if not text:
return 0
sign = -1 if text.startswith("-") else 1
clean = text.lstrip("+-").strip()
m = re.match(r"^([\d.]+)\s*(亿|万|元)?$", clean)
if not m:
return 0
value = float(m.group(1)) if m.group(1) else 0
unit = m.group(2) or "元"
if unit == "亿":
return sign * value * 100000000
elif unit == "万":
return sign * value * 10000
return sign * value
# ---- MX API ----
async def _call_mx_api(api_key: str, name: str, days: int) -> Optional[dict]:
"""执行一次 MX API 调用,返回原始 JSON"""
url = "https://mkapi2.dfcfs.com/finskillshub/api/claw/query"
payload = {"toolQuery": f"{name}最近{days}天主力资金流向和成交额"}
async with httpx.AsyncClient() as client:
try:
resp = await client.post(
url,
json=payload,
headers={
"Content-Type": "application/json",
"apikey": api_key,
},
timeout=15,
)
if resp.status_code != 200:
return None
return resp.json()
except Exception as e:
print(f"[eastmoney] MX API error: {e}")
return None
def _parse_mx_response(result: dict) -> Optional[List[dict]]:
"""解析 MX API 返回,提取资金流向数据列表"""
if result.get("status") != 0:
return None
dto_list = result.get("data", {}).get("data", {}).get("searchDataResultDTO", {}).get("dataTableDTOList", [])
if not dto_list:
return None
dto = dto_list[0]
name_map = dto.get("nameMap", {})
raw_table = dto.get("rawTable", {})
table = dto.get("table", {})
dates = table.get("headName", [])
if not dates:
return None
name_to_id = {}
for kid, v in name_map.items():
if isinstance(v, str):
name_to_id[v] = kid
main_net_col = name_to_id.get("(区间)主力净流入资金")
amount_col = name_to_id.get("区间成交额")
data_list = []
for i, date_str in enumerate(dates):
date_str = str(date_str).strip()
m1 = re.match(r"^(\d{4})[/-](\d{1,2})[/-](\d{1,2})", date_str)
if m1:
date_str = f"{m1.group(1)}-{int(m1.group(2)):02d}-{int(m1.group(3)):02d}"
else:
m2 = re.match(r"^(\d{1,2})[/-](\d{1,2})", date_str)
if m2:
year = datetime.now().year
date_str = f"{year}-{int(m2.group(1)):02d}-{int(m2.group(2)):02d}"
else:
continue
main_net = 0
if main_net_col and raw_table.get(main_net_col) and i < len(raw_table[main_net_col]):
main_net = parse_amount(raw_table[main_net_col][i])
amount = 0
if amount_col and raw_table.get(amount_col) and i < len(raw_table[amount_col]):
amount = parse_amount(raw_table[amount_col][i])
data_list.append({"date": date_str, "mainNetInflow": main_net, "amount": amount})
return data_list
async def fetch_mx_api(name: str, days: int) -> Optional[List[dict]]:
"""调用东方财富妙想MX API获取资金流向(缓存6小时,多key轮询)"""
cache_key = f"mx_fund_flow:{name}:{days}"
# 缓存命中
cached = get_cache(cache_key)
if cached is not None:
return json.loads(cached)
# 多 key 轮询:按序尝试,遇到 113(超限) 自动切下一个 key
if not _ensure_keys():
return None
for attempt in range(len(_api_keys)):
key = _get_key()
result = await _call_mx_api(key, name, days)
if result is None:
_rotate_key()
continue
status = result.get("status", -1)
if status == 113:
print(f"[eastmoney] key {_key_index % len(_api_keys)} 已达每日上限,切换到下一个")
_rotate_key()
continue
if status == 114:
print(f"[eastmoney] key {_key_index % len(_api_keys)} 无效(114),跳过")
_rotate_key()
continue
if status != 0:
_rotate_key()
continue
# 成功
data_list = _parse_mx_response(result)
if data_list:
set_cache(cache_key, json.dumps(data_list, ensure_ascii=False))
return data_list
print(f"[eastmoney] 所有 {len(_api_keys)} 个 MX API key 均已耗尽")
return None
# ---- 板块数据 ----
# 东方财富板块类型映射
SECTOR_TYPE_MAP = {
"industry": "m:90+t:2", # 行业板块
"concept": "m:90+t:3", # 概念板块
}
# 板块列表字段:f12=代码, f14=名称, f3=涨跌幅%, f62=主力净流入, f184=主力净流入占比
# f66=超大单净流入, f69=超大单净流入占比, f70=成交额, f78=小单净流入
SECTOR_FIELDS = "f12,f14,f2,f3,f4,f62,f184,f66,f69,f70,f78"
# ---- 板块数据(通过 akshare 调用同花顺数据源)----
import akshare as ak
import pandas as pd
async def fetch_sector_list(sector_type: str) -> list[dict]:
"""
获取板块资金流向数据
sector_type: "industry" 或 "concept"
返回按主力净流入降序排列的板块列表
编码优先东方财富(BKxxxx),降级同花顺(6位数字)
"""
loop = asyncio.get_event_loop()
def _get_data():
# 1) 板块编码映射:东方财富 BK 编码 → 降级同花顺编码
code_map = {}
try:
if sector_type == "industry":
code_df = ak.stock_board_industry_name_em()
else:
code_df = ak.stock_board_concept_name_em()
if code_df is not None and not code_df.empty:
for _, r in code_df.iterrows():
code_map[str(r.get("f14", ""))] = str(r.get("f12", ""))
except Exception:
try:
if sector_type == "industry":
code_df = ak.stock_board_industry_name_ths()
else:
code_df = ak.stock_board_concept_name_ths()
if code_df is not None and not code_df.empty:
for _, r in code_df.iterrows():
code_map[str(r.get("name", ""))] = str(r.get("code", ""))
except Exception:
pass
# 2) 资金流向(10jqka 同花顺数据源)
if sector_type == "industry":
df = ak.stock_fund_flow_industry()
else:
df = ak.stock_fund_flow_concept()
return code_map, df
try:
code_map, df = await loop.run_in_executor(None, _get_data)
if df is None or df.empty:
return []
df = df.sort_values("净额", ascending=False)
items = []
for _, row in df.iterrows():
name = str(row.get("行业", "")).strip()
items.append({
"code": code_map.get(name, ""),
"name": name,
"level": _safe_float(row.get("行业指数")),
"changePercent": _safe_float(row.get("行业-涨跌幅")),
"changeAmount": None,
"mainNetInflow": _safe_float(row.get("净额", 0)) * 100000000, # 亿→元
"mainNetInflowPercent": None,
"superLargeInflow": None,
"superLargeInflowPercent": None,
"turnover": _safe_float(row.get("流入资金", 0)) * 100000000 + _safe_float(row.get("流出资金", 0)) * 100000000,
"smallNetInflow": None,
})
return items
except Exception as e:
print(f"[eastmoney] 获取{sector_type}板块失败: {e}")
return []
def _safe_float(val) -> float:
if val is None:
return 0.0
try:
return float(val)
except (ValueError, TypeError):
return 0.0
async def fetch_sector_list_direct(sector_type: str) -> list[dict]:
"""
直接从东方财富 push2 API 获取板块列表(备用,当 akshare 不可用时)
"""
fs = SECTOR_TYPE_MAP.get(sector_type)
if not fs:
return []
url = (
f"https://push2.eastmoney.com/api/qt/clist/get"
f"?fs={fs}&fields={SECTOR_FIELDS}"
f"&fid=f62&po=1&pz=500&pn=1&np=1&fltt=2"
)
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
"Referer": "https://quote.eastmoney.com/",
}
async with httpx.AsyncClient() as client:
try:
resp = await client.get(url, headers=headers, timeout=10)
if resp.status_code != 200:
return []
result = resp.json()
if result.get("rc") != 0:
return []
diff = result.get("data", {}).get("diff", [])
items = []
for item in diff:
items.append({
"code": item.get("f12", ""),
"name": item.get("f14", ""),
"level": item.get("f2"),
"changePercent": item.get("f3"),
"changeAmount": item.get("f4"),
"mainNetInflow": item.get("f62", 0),
"mainNetInflowPercent": item.get("f184", 0),
"superLargeInflow": item.get("f66", 0),
"superLargeInflowPercent": item.get("f69", 0),
"turnover": item.get("f70", 0),
"smallNetInflow": item.get("f78", 0),
})
return items
except Exception as e:
print(f"[eastmoney] 获取{sector_type}板块失败: {e}")
return []
# ---- 公司概况 ----
_F10_MARKET_MAP = {"6": "SH", "0": "SZ", "3": "SZ"}
_DATACENTER_MARKET_MAP = {"6": "SH", "0": "SZ", "3": "SZ"}
def _code_to_f10(code: str) -> str:
"""6位代码 → 东方财富 F10 格式 (SH600519 / SZ000001)"""
prefix = code[0]
mkt = _F10_MARKET_MAP.get(prefix, "SH")
return f"{mkt}{code}"
def _code_to_secucode(code: str) -> str:
"""6位代码 → 数据中心格式 (600519.SH / 000001.SZ)"""
prefix = code[0]
mkt = _DATACENTER_MARKET_MAP.get(prefix, "SH")
return f"{code}.{mkt}"
async def fetch_company_profile(code: str) -> Optional[dict]:
"""东方财富 F10 - 公司概况(缓存24小时)"""
cache_key = f"company_profile:{code}"
cached = get_cache(cache_key)
if cached is not None:
return json.loads(cached)
em_code = _code_to_f10(code)
url = (
f"https://emweb.securities.eastmoney.com/PC_HSF10"
f"/CompanySurvey/CompanySurveyAjax?code={em_code}"
)
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
"Referer": (
f"https://emweb.securities.eastmoney.com/PC_HSF10"
f"/CompanySurvey/Index?type=web&code={em_code}"
),
}
async with httpx.AsyncClient() as client:
try:
resp = await client.get(url, headers=headers, timeout=15)
if resp.status_code != 200:
return None
raw = resp.json()
except Exception as e:
print(f"[eastmoney] fetch_company_profile error: {e}")
return None
jbzl = raw.get("jbzl", {})
fxxg = raw.get("fxxg", {})
profile = {
"code": code,
"name": jbzl.get("agjc", ""),
"fullName": jbzl.get("gsmc", ""),
"englishName": jbzl.get("ywmc", ""),
"boardType": jbzl.get("zqlb", ""),
"industry": jbzl.get("sshy", ""),
"exchange": jbzl.get("ssjys", ""),
"industryDetail": jbzl.get("sszjhhy", ""),
# 高管
"chairman": jbzl.get("dsz", ""),
"generalManager": jbzl.get("zjl", ""),
"secretary": jbzl.get("dm", ""),
# 联系方式
"phone": jbzl.get("lxdh", ""),
"email": jbzl.get("dzxx", ""),
"fax": jbzl.get("cz", ""),
"website": jbzl.get("gswz", ""),
# 地址
"officeAddress": jbzl.get("bgdz", ""),
"registeredAddress": jbzl.get("zcdz", ""),
"region": jbzl.get("qy", ""),
"postalCode": jbzl.get("yzbm", ""),
# 注册信息
"registeredCapital": jbzl.get("zczb", ""),
"unifiedCreditCode": jbzl.get("gsdj", ""),
"employees": jbzl.get("gyrs", 0),
"managementCount": jbzl.get("glryrs", 0),
# 中介机构
"lawFirm": jbzl.get("lssws", ""),
"auditor": jbzl.get("kjssws", ""),
# 公司简介&业务
"businessScope": jbzl.get("jyfw", ""),
"companyProfile": jbzl.get("gsjj", ""),
# 发行信息
"establishmentDate": fxxg.get("clrq", ""),
"listingDate": fxxg.get("ssrq", ""),
"listingPrice": fxxg.get("mgfxj", ""),
"issuePriceEarningsRatio": fxxg.get("fxsyl", ""),
"issueAmount": fxxg.get("fxl", ""),
"netProceeds": fxxg.get("mjzjje", ""),
"issueMethod": fxxg.get("fxfs", ""),
}
set_cache(cache_key, json.dumps(profile, ensure_ascii=False), ttl_hours=24)
return profile
# ---- 财务数据 ----
# 财务指标关键字段映射(英文 → 中文)
FINANCIAL_FIELD_LABELS: dict[str, str] = {
"EPSJB": "基本每股收益",
"EPSKCJB": "扣非每股收益",
"BPS": "每股净资产",
"MGWFPLR": "每股未分配利润",
"MGJYXJJE": "每股经营现金流",
"TOTALOPERATEREVE": "营业总收入",
"MLR": "毛利润",
"PARENTNETPROFIT": "归母净利润",
"KCFJCXSYJLR": "扣非净利润",
"TOTALOPERATEREVETZ": "营收同比增长",
"PARENTNETPROFITTZ": "净利润同比增长",
"KCFJCXSYJLRTZ": "扣非净利润同比增长",
"ROEJQ": "加权净资产收益率",
"ROEKCJQ": "扣非ROE",
"ZZCJLL": "总资产净利率",
"XSJLL": "销售净利率",
"XSMLL": "销售毛利率",
"LD": "流动比率",
"SD": "速动比率",
"ZCFZL": "资产负债率",
"XJLLB": "现金流比率",
"YSZKZZTS": "应收账款周转天数",
"CHZZTS": "存货周转天数",
"ZZCZZTS": "总资产周转天数",
"TOAZZL": "总资产周转率",
"CHZZL": "存货周转率",
"ROIC": "投入资本回报率",
"QYCS": "权益乘数",
"CQBL": "产权比率",
"TAXRATE": "实际税率",
"TOTAL_SHARE": "总股本",
"A_FREE_SHARE": "流通A股",
"DJD_TOI_YOY": "单季营收同比",
"DJD_DPNP_YOY": "单季净利润同比",
"EPSJBTZ": "每股收益同比增长",
"BPSTZ": "每股净资产同比增长",
"ROEJQTZ": "ROE同比增长",
# 利润表补充
"OPERATE_PROFIT_PK": "营业利润",
"PER_TOI": "每股营业收入",
"PER_EBIT": "每股EBIT",
# 资产负债表补充
"TOTAL_ASSETS_PK": "总资产",
"LIABILITY": "总负债",
"TOTAL_EQUITY_PK": "所有者权益",
"MGZBGJ": "每股资本公积",
"CA_TA": "流动资产占比",
"NCA_TA": "非流动资产占比",
"CL_TL": "流动负债占比",
# 现金流量表补充
"NETCASH_OPERATE_PK": "经营活动现金流净额",
"NETCASH_INVEST_PK": "投资活动现金流净额",
"NETCASH_FINANCE_PK": "筹资活动现金流净额",
"JYXJLYYSR": "经营现金流占营收比",
"XSJXLYYSR": "销售现金流占营收比",
"FCFF_FORWARD": "企业自由现金流",
# 营运能力补充
"OPERATE_CYCLE": "营业周期",
"CURRENT_ASSET_TR": "流动资产周转率",
"FIXED_ASSET_TR": "固定资产周转率",
"ACCOUNTS_PAYABLE_TR": "应付账款周转率",
"PREPAID_ACCOUNTS_RATIO": "预收账款占营收比",
"PAYABLE_TDAYS": "应付账款周转天数",
}
# 小数比率字段(后端返回小数,前端需显示为百分比)
_DECIMAL_RATIO_FIELDS = {
"JYXJLYYSR", "XSJXLYYSR", "PREPAID_ACCOUNTS_RATIO",
"NCO_OP", "NCO_FIXED", "SALE_ER_PK",
}
def _parse_financial_record(record: dict) -> dict:
"""将原始财务记录转为整洁的输出"""
result = {}
for field, label in FINANCIAL_FIELD_LABELS.items():
val = record.get(field)
if val is not None:
if isinstance(val, (int, float)):
# 小数比率字段转为百分比
if field in _DECIMAL_RATIO_FIELDS and abs(val) < 1:
result[label] = round(val * 100, 2)
result[label + "(%)"] = True
elif abs(val) > 1_0000_0000:
result[label] = round(val / 1_0000_0000, 2)
result[label + "(亿)"] = True
elif abs(val) > 1_0000:
result[label] = round(val / 1_0000, 2)
result[label + "(万)"] = True
else:
result[label] = round(val, 2)
else:
result[label] = val
return result
async def fetch_financial_data_main(code: str, years: int = 5) -> Optional[list[dict]]:
"""东方财富数据中心 - 主要财务指标(缓存6小时)"""
cache_key = f"financial_main:{code}:{years}"
cached = get_cache(cache_key)
if cached is not None:
return json.loads(cached)
secucode = _code_to_secucode(code)
url = "https://datacenter.eastmoney.com/securities/api/data/v1/get"
params = {
"reportName": "RPT_F10_FINANCE_MAINFINADATA",
"columns": "ALL",
"filter": f'(SECUCODE="{secucode}")',
"pageNumber": 1,
"pageSize": max(years * 4, 4), # 每年最多4份报告
"sortTypes": -1,
"sortColumns": "REPORT_DATE",
}
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
"Referer": "https://datacenter.eastmoney.com/",
}
async with httpx.AsyncClient() as client:
try:
resp = await client.get(url, params=params, headers=headers, timeout=15)
if resp.status_code != 200:
return None
body = resp.json()
if not body.get("result") or not body["result"].get("data"):
return None
records = body["result"]["data"]
except Exception as e:
print(f"[eastmoney] fetch_financial_data error: {e}")
return None
items = []
for r in records:
items.append({
"reportDate": r.get("REPORT_DATE", ""),
"reportType": r.get("REPORT_TYPE", ""),
"reportDateName": r.get("REPORT_DATE_NAME", ""),
"noticeDate": r.get("NOTICE_DATE", ""),
"indicators": _parse_financial_record(r),
})
set_cache(cache_key, json.dumps(items, ensure_ascii=False), ttl_hours=6)
return items
# ---- 经营分析(主营构成/收入构成)----
_SEGMENT_TYPE_MAP = {
"industry": "1", # 按行业
"product": "2", # 按产品
"region": "3", # 按地区
}
async def fetch_business_segments(code: str, by_type: str = "product", years: int = 3) -> Optional[list[dict]]:
"""东方财富经营分析 - 主营构成(按产品/行业/地区),缓存6小时
Args:
code: 6位股票代码
by_type: "product"|"industry"|"region"
years: 返回最近N年的数据
"""
seg_type = _SEGMENT_TYPE_MAP.get(by_type, "2")
cache_key = f"business_segments:{code}:{by_type}:{years}"
cached = get_cache(cache_key)
if cached is not None:
return json.loads(cached)
secucode = _code_to_secucode(code)
url = "https://datacenter.eastmoney.com/securities/api/data/v1/get"
params = {
"reportName": "RPT_F10_FN_SEGMENTSV",
"columns": (
"SECUCODE,SECURITY_CODE,SECURITY_NAME_ABBR,MAINOP_TYPE,ITEM_NAME,"
"ITEM_CODE,ITEM_PARENT_CODE,MAIN_BUSINESS_INCOME,MAIN_BUSINESS_RPOFIT,"
"GROSS_RPOFIT_RATIO,MBI_RATIO,MBR_RATIO,REPORT_DATE,REPORT_NAME,"
"REPORT_DATE_CODE,MAIN_BUSINESS_COST,MBC_RATIO"
),
"filter": f'(SECUCODE="{secucode}")(MAINOP_TYPE="{seg_type}")(ITEM_PARENT_CODE="999999")',
"pageNumber": 1,
"pageSize": 200,
"sortTypes": "1,1",
"sortColumns": "MAINOP_TYPE,RANK3",
}
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
"Referer": "https://datacenter.eastmoney.com/",
}
async with httpx.AsyncClient() as client:
try:
resp = await client.get(url, params=params, headers=headers, timeout=15)
if resp.status_code != 200:
return None
body = resp.json()
if not body.get("result") or not body["result"].get("data"):
return None
records = body["result"]["data"]
except Exception as e:
print(f"[eastmoney] fetch_business_segments error: {e}")
return None
# 按报告期分组
from collections import OrderedDict
period_map: dict[str, list[dict]] = OrderedDict()
for r in records:
report_name = r.get("REPORT_NAME", "")
report_date = r.get("REPORT_DATE", "")
item_name = r.get("ITEM_NAME", "")
income = r.get("MAIN_BUSINESS_INCOME")
cost = r.get("MAIN_BUSINESS_COST")
profit = r.get("MAIN_BUSINESS_RPOFIT")
gross_ratio = r.get("GROSS_RPOFIT_RATIO")
mbi_ratio = r.get("MBI_RATIO")
mbr_ratio = r.get("MBR_RATIO")
mbc_ratio = r.get("MBC_RATIO")
entry = {
"itemName": item_name,
"income": income,
"cost": cost,
"profit": profit,
"grossRatio": round(gross_ratio * 100, 2) if gross_ratio is not None else None,
"incomeRatio": round(mbi_ratio * 100, 2) if mbi_ratio is not None else None,
"profitRatio": round(mbr_ratio * 100, 2) if mbr_ratio is not None else None,
"costRatio": round(mbc_ratio * 100, 2) if mbc_ratio is not None else None,
}
if report_name not in period_map:
period_map[report_name] = []
period_map[report_name].append(entry)
# 补充 profitRatio:若 API 未返回,用该期总利润推算各项目利润占比
for pname, items in period_map.items():
total_profit = sum(it["profit"] for it in items if it["profit"] is not None)
if total_profit != 0:
for it in items:
if it["profitRatio"] is None and it["profit"] is not None:
it["profitRatio"] = round(it["profit"] / total_profit * 100, 2)
# 取最近N年
all_periods = list(period_map.keys())
all_periods.sort(reverse=True)
selected = all_periods[:max(years * 4, 4)]
result = []
for p in selected:
result.append({
"reportName": p,
"items": period_map[p],
})
set_cache(cache_key, json.dumps(result, ensure_ascii=False), ttl_hours=6)
return result
async def fetch_push2his(code: str, days: int) -> Optional[list]:
"""回退到东方财富 push2his 接口(自动重试一次)"""
market = get_eastmoney_market(code)
secid = f"{market}.{code}"
url = (
f"https://push2his.eastmoney.com/api/qt/stock/fflow/daykline/get"
f"?lmt={days}&fields1=f1,f2,f3,f7"
f"&fields2=f51,f52,f53,f54,f55,f56,f57,f58,f59,f60,f61,f62,f63,f64,f65,f66,f67,f68,f69"
f"&ut=b2884a393a59ad64002292a3e90d46a5&secid={secid}"
)
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
"Referer": "https://quote.eastmoney.com/",
}
for attempt in range(2):
async with httpx.AsyncClient() as client:
try:
resp = await client.get(url, headers=headers, timeout=10)
if resp.status_code != 200:
if attempt == 0:
continue
print(f"[eastmoney] push2his HTTP {resp.status_code}")
return None
result = resp.json()
klines = result.get("data", {}).get("klines", [])
if not klines:
if attempt == 0:
continue
print(f"[eastmoney] push2his no klines in response")
return None
return klines
except Exception as e:
if attempt == 0:
continue
print(f"[eastmoney] push2his error: {e}")
return None
return None