astock-agent/backend/app/data/eastmoney_client.py
2026-04-15 23:32:22 +08:00

201 lines
6.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""东方财富分钟K线数据客户端
通过 push2his.eastmoney.com 获取 A 股分钟级 K 线数据,
用于盘中量能分布分析和短线进场时机判断。
免费接口,无需认证,注意限频。
"""
import logging
import httpx
import pandas as pd
from datetime import datetime
from app.data.cache import cache
from app.config import settings
logger = logging.getLogger(__name__)
# 东方财富分钟K线接口
EASTMONEY_KLINE_URL = "http://push2his.eastmoney.com/api/qt/stock/kline/get"
HEADERS = {
"Referer": "http://finance.eastmoney.com",
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36",
}
def _ts_code_to_eastmoney(ts_code: str) -> str:
"""600519.SH -> 1.600519 (上海=1, 深圳=0)"""
code, market = ts_code.split(".")
prefix = "1" if market == "SH" else "0"
return f"{prefix}.{code}"
async def get_min_kline(
ts_code: str,
period: str = "5",
count: int = 48,
) -> pd.DataFrame:
"""获取分钟级 K 线数据
Args:
ts_code: Tushare 格式代码,如 600519.SH
period: 分钟周期 ("1", "5", "15", "30", "60")
count: 请求条数5分钟×48=4小时≈一个交易日
Returns:
DataFrame with columns: time, open, close, high, low, volume, amount
空 DataFrame if failed
"""
cache_key = f"min_kline:{ts_code}:{period}"
cached = cache.get(cache_key)
if cached is not None:
return cached
secid = _ts_code_to_eastmoney(ts_code)
params = {
"secid": secid,
"fields1": "f1,f2,f3,f4,f5,f6",
"fields2": "f51,f52,f53,f54,f55,f56,f57,f58,f59,f60,f61",
"klt": period, # 分钟周期
"fqt": "1", # 前复权
"beg": "0",
"end": "20500101",
"lmt": str(count), # 返回条数限制
}
try:
async with httpx.AsyncClient() as client:
resp = await client.get(
EASTMONEY_KLINE_URL,
params=params,
headers=HEADERS,
timeout=10,
)
data = resp.json()
klines = data.get("data", {}).get("klines", [])
if not klines:
logger.debug(f"东方财富分钟K线 {ts_code} 无数据")
return pd.DataFrame()
# 解析:格式 "2026-04-15 09:30,5.71,5.73,5.74,5.70,12345,70000.00"
rows = []
for line in klines:
parts = line.split(",")
if len(parts) < 7:
continue
rows.append({
"time": parts[0],
"open": float(parts[1]),
"close": float(parts[2]),
"high": float(parts[3]),
"low": float(parts[4]),
"volume": float(parts[5]),
"amount": float(parts[6]),
})
df = pd.DataFrame(rows)
if df.empty:
return df
# 缓存分钟数据盘中3分钟过期盘后30分钟过期
ttl = 180 if _is_trading_hours() else 1800
cache.set(cache_key, df, ttl)
return df
except Exception as e:
logger.error(f"东方财富分钟K线获取失败 {ts_code}: {e}")
return pd.DataFrame()
def analyze_intraday_volume_distribution(min_df: pd.DataFrame) -> dict:
"""分析盘中量能分布基于5分钟K线
Returns:
{
"morning_volume_ratio": float, # 上午成交额占比
"afternoon_volume_ratio": float, # 下午成交额占比
"opening_strength": float, # 开盘30分钟成交额/全天占比
"closing_strength": float, # 尾盘30分钟成交额/全天占比
"volume_trend": str, # "morning_dominant" / "afternoon_dominant" / "balanced"
"key_periods": list[str], # 关键放量时段描述
}
"""
if min_df.empty or len(min_df) < 4:
return {
"morning_volume_ratio": 0.5,
"afternoon_volume_ratio": 0.5,
"opening_strength": 0.25,
"closing_strength": 0.25,
"volume_trend": "balanced",
"key_periods": [],
}
total_amount = min_df["amount"].sum()
if total_amount == 0:
return {
"morning_volume_ratio": 0.5,
"afternoon_volume_ratio": 0.5,
"opening_strength": 0.25,
"closing_strength": 0.25,
"volume_trend": "balanced",
"key_periods": [],
}
# 按时间段分割
morning = min_df[min_df["time"].str.contains("09:|10:|11:0", na=False)]
afternoon = min_df[min_df["time"].str.contains("13:|14:0|14:1|14:2|14:3|14:4|14:5", na=False)]
morning_ratio = morning["amount"].sum() / total_amount if not morning.empty else 0.5
afternoon_ratio = afternoon["amount"].sum() / total_amount if not afternoon.empty else 0.5
# 开盘30分钟9:30-10:00
opening_30 = min_df[min_df["time"].str.contains("09:3|09:4|09:5|10:0", na=False)]
opening_strength = opening_30["amount"].sum() / total_amount if not opening_30.empty else 0.25
# 尾盘30分钟14:30-15:00
closing_30 = min_df[min_df["time"].str.contains("14:3|14:4|14:5|15:0", na=False)]
closing_strength = closing_30["amount"].sum() / total_amount if not closing_30.empty else 0.25
# 量能趋势判断
if morning_ratio > 0.6:
volume_trend = "morning_dominant"
elif afternoon_ratio > 0.6:
volume_trend = "afternoon_dominant"
else:
volume_trend = "balanced"
# 关键放量时段
key_periods = []
avg_amount = min_df["amount"].mean()
hot_bars = min_df[min_df["amount"] > avg_amount * 2]
for _, bar in hot_bars.iterrows():
time_str = bar["time"].split(" ")[1] if " " in bar["time"] else bar["time"]
pct = bar["amount"] / total_amount * 100
key_periods.append(f"{time_str}放量(占比{pct:.0f}%)")
return {
"morning_volume_ratio": round(morning_ratio, 2),
"afternoon_volume_ratio": round(afternoon_ratio, 2),
"opening_strength": round(opening_strength, 2),
"closing_strength": round(closing_strength, 2),
"volume_trend": volume_trend,
"key_periods": key_periods[:3],
}
def _is_trading_hours() -> bool:
"""简单判断是否在交易时间"""
now = datetime.now()
if now.weekday() >= 5: # 周六周日
return False
hour = now.hour
minute = now.minute
# 9:30 - 11:30 或 13:00 - 15:00
if (hour == 9 and minute >= 30) or (hour == 10) or (hour == 11 and minute <= 30):
return True
if (hour == 13) or (hour == 14) or (hour == 15 and minute == 0):
return True
return False