modify scripts

This commit is contained in:
2025-07-19 17:06:27 +08:00
parent ad47bcf511
commit cbd9b18ef4
9 changed files with 1171 additions and 696 deletions

View File

@ -0,0 +1,154 @@
import requests
import logging
import time
import re
import json
from typing import List, Dict, Optional
# 配置日志
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler("eastmoney_stock.log", encoding='utf-8'),
logging.StreamHandler()
]
)
logger = logging.getLogger("eastmoney_stock_fetcher")
class EastMoneyStockFetcher:
def __init__(self,
appkey: str,
cookies: str,
retry_count: int = 3,
retry_delay: int = 2):
"""
初始化东方财富自选股获取器
:param appkey: 接口认证密钥(从抓包获取)
:param cookies: 完整cookie字符串从抓包获取
:param retry_count: 重试次数
:param retry_delay: 重试间隔(秒)
"""
self.base_url = "https://myfavor.eastmoney.com/v4/webouter/gstkinfos"
self.appkey = appkey
self.cookies = cookies
self.retry_count = retry_count
self.retry_delay = retry_delay
self.headers = self._build_headers()
def _build_headers(self) -> Dict[str, str]:
"""构建请求头"""
return {
"Accept": "*/*",
"Accept-Language": "zh-CN,zh;q=0.9",
"Connection": "keep-alive",
"Referer": "https://quote.eastmoney.com/zixuan/?from=home",
"Sec-Fetch-Dest": "script",
"Sec-Fetch-Mode": "no-cors",
"Sec-Fetch-Site": "same-site",
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36",
"sec-ch-ua": "\"Not)A;Brand\";v=\"8\", \"Chromium\";v=\"138\", \"Google Chrome\";v=\"138\"",
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": "macOS",
"Cookie": self.cookies
}
def _build_params(self) -> Dict[str, str]:
"""构建请求参数"""
timestamp = int(time.time() * 1000)
# 生成唯一回调函数名(模拟前端行为)
callback = f"jQuery3710{int(time.time()*1000000)}"
return {
"appkey": self.appkey,
"cb": callback,
"g": "1", # 固定参数(抓包观察值)
"_": str(timestamp)
}
def _parse_jsonp(self, jsonp_str: str) -> Optional[Dict]:
"""解析JSONP响应为字典"""
try:
# 匹配JSONP包裹的JSON部分去除函数调用
match = re.match(r'^jQuery\d+(_\d+)?\((.*)\);$', jsonp_str)
if not match:
logger.error("JSONP格式解析失败无法提取JSON内容")
return None
json_str = match.group(2) # 取第二个分组括号内的JSON内容
return json.loads(json_str)
except json.JSONDecodeError as e:
logger.error(f"JSON解析失败{str(e)}", exc_info=True)
return None
except Exception as e:
logger.error(f"JSONP处理异常{str(e)}", exc_info=True)
return None
def get_favorites(self) -> Optional[List[Dict]]:
"""获取自选股列表(带重试机制)"""
for retry in range(self.retry_count):
try:
logger.info(f"获取自选股(第{retry+1}/{self.retry_count}次尝试)")
# 发送请求
response = requests.get(
url=self.base_url,
headers=self.headers,
params=self._build_params(),
timeout=10
)
response.raise_for_status() # 触发HTTP错误如403、500
# 解析响应
json_data = self._parse_jsonp(response.text)
if not json_data:
logger.warning("解析响应失败,将重试")
logger.warning(f"response: {response.text[:500]}")
continue
# 验证业务状态
if json_data.get("state") != 0:
error_msg = json_data.get("message", "未知错误")
logger.error(f"接口返回错误:{error_msg}")
continue
# 提取股票列表
stock_list = json_data.get("data", {}).get("stkinfolist", [])
logger.info(f"成功获取{len(stock_list)}只自选股")
return stock_list
except requests.exceptions.HTTPError as e:
logger.error(f"HTTP错误{str(e)}", exc_info=True)
except requests.exceptions.ConnectionError:
logger.error("网络连接失败,正在重试...")
except requests.exceptions.Timeout:
logger.error("请求超时,正在重试...")
except Exception as e:
logger.error(f"获取失败:{str(e)}", exc_info=True)
# 重试前等待(最后一次失败不等待)
if retry < self.retry_count - 1:
time.sleep(self.retry_delay)
logger.error(f"达到最大重试次数({self.retry_count}次),获取自选股失败")
return None
# 使用示例
if __name__ == "__main__":
# 替换为你的实际参数(从抓包获取)
USER_CONFIG = {
"appkey": "e9166c7e9cdfad3aa3fd7d93b757e9b1", # 抓包得到的appkey
"cookies": "qgqp_b_id=5107797c7296e8e7fc529ab2daa8bf8b; AUTH_FUND.EASTMONEY.COM_GSJZ=AUTH*TTJJ*TOKEN; xsb_history=831566%7C%u76DB%u5927%u5728%u7EBF%2C874086%7C%u5C0F%u5510%u79D1%u6280; mtp=1; uidal=5697316400113710%e7%ba%a2%e6%98%8coscar; sid=173318833; vtpst=|; ct=Cg2Q_hxYYLVYDcl8gpqSfxI47pA0EhhzkPjOjakoKXlgBcuGD-UqiREN2uiJtzIYD7tq7QKsj8BXMMyB2qUNG51SBbWCzy6Mvj0S3PxV67qqGiSf25tA9_HkAXbOEC26TggMsSv_G6x_CUkfAP0ElJO0ly0PDqUG6nvRx9xA074; ut=FobyicMgeV6W21ICVIh677eBIvbu__ZyQz5TBZWTQa8trNG3ATJNzZsOuWDUzrzavA0SNRBKoUwSOj-mk-kLXkuGvwj1EVY7aq3Ed-I76o9UqkhrvIjWMkjJTS8zYZ-dMiFJW3j0NtDkDLQ-T7-UvQR_zZPaow6M7iiz6-n9RVIypp03BZxsnjE3f_p3Ns2CRWNHoaJfdyW9LmWCBFOgNNmdBoyvqAVPThz_JYTzDRCuMx1tAe3l2GMuu2k8krYCjhEMmhbp1uqNtStmq8I1g__t3ajwvBQQ; pi=5697316400113710%3Bv5697316400113710%3B%E7%BA%A2%E6%98%8Coscar%3B817EV8jx6Tt0RJBA5N2FGOTBXhXXUP0r%2BYE54iCMCtD%2BXii5B7kf5bRI7srQ3L0TuJa1gpd125NW4ApaR0SL6PoeTdF6ueZi2GPFVX5rkeDCLWILf0rJHdYRL8JDKGySpCFxvzODOozSDz5w3Jb%2BOyJsvAns0Q69ea2VHHPJBRcL4ZhzOix%2FKocZlOQv7W6aawo0YhcY%3BWKiXjWmqqqz9pMgKRJpzeAwI1Be6om8gJWVcunwDO3UQK4FQ%2F8BLLGzJc8DdX%2FBcByq1faXnmy7KZa8dNxDN8J7n3Q2LDqSzswh9D1%2BZHQMxDO4AOfCouvrBk%2B2Gcnq0Pr4XdLmL6%2BzLVcCKDDiRPxEzBCfYFg%3D%3D; st_si=47023831054215; fullscreengg=1; fullscreengg2=1; HAList=ty-0-002555-%u4E09%u4E03%u4E92%u5A31%2Cty-0-159687-%u4E9A%u592A%u7CBE%u9009ETF%2Cty-116-01024-%u5FEB%u624B-W%2Cty-116-00700-%u817E%u8BAF%u63A7%u80A1%2Cty-1-601919-%u4E2D%u8FDC%u6D77%u63A7%2Cty-0-002714-%u7267%u539F%u80A1%u4EFD%2Cty-0-000423-%u4E1C%u963F%u963F%u80F6%2Cty-0-002595-%u8C6A%u8FC8%u79D1%u6280%2Cty-0-836395-%u6717%u9E3F%u79D1%u6280%2Cty-0-832982-%u9526%u6CE2%u751F%u7269; st_asi=delete; rskey=Ah3stV3Q5aVRHVVFmMFMyNTByV0hveTMwZz09IWhHQ; st_pvi=05050221710102; st_sp=2022-01-20%2014%3A22%3A55; st_inirUrl=https%3A%2F%2Fwww.baidu.com%2Flink; st_sn=35; st_psi=20250716151931682-113200301712-2597539270" # 你的完整cookie
}
# 初始化获取器并获取自选股
fetcher = EastMoneyStockFetcher(
appkey=USER_CONFIG["appkey"],
cookies=USER_CONFIG["cookies"]
)
favorites = fetcher.get_favorites()
if favorites:
print("获取到的自选股列表:")
for idx, stock in enumerate(favorites, 1):
# 解析股票代码从security字段提取格式如"0$300760$36443142646090"
code = stock["security"].split("$")[1] if "$" in stock["security"] else stock["security"]
print(f"{idx}. 代码: {code}, 当前价: {stock['price']}, 更新时间: {stock['updatetime']}")

View File

@ -0,0 +1,160 @@
import requests
import logging
import time
import re
from typing import List, Dict, Optional
# 配置日志
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler("tencent_stock.log", encoding='utf-8'),
logging.StreamHandler()
]
)
logger = logging.getLogger("tencent_stock_fetcher")
class TencentStockFetcher:
def __init__(self,
uin: str,
openid: str,
fskey: str,
cookies: str,
retry_count: int = 3,
retry_delay: int = 2):
"""
初始化腾讯自选股获取器
:param uin: 用户唯一标识(从抓包获取)
:param openid: 开放平台ID从抓包获取
:param fskey: 会话密钥(从抓包获取)
:param cookies: 完整的cookie字符串从抓包获取
:param retry_count: 重试次数
:param retry_delay: 重试间隔(秒)
"""
self.base_url = "https://webstock.finance.qq.com/stockapp/zixuanguweb/stocklist"
self.uin = uin
self.openid = openid
self.fskey = fskey
self.cookies = cookies
self.retry_count = retry_count
self.retry_delay = retry_delay
self.headers = self._build_headers()
def _build_headers(self) -> Dict[str, str]:
"""构建请求头"""
return {
"accept": "*/*",
"accept-language": "zh-CN,zh;q=0.9",
"referer": "https://gu.qq.com/",
"sec-ch-ua": '"Not)A;Brand";v="8", "Chromium";v="138", "Google Chrome";v="138"',
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": "macOS",
"sec-fetch-dest": "script",
"sec-fetch-mode": "no-cors",
"sec-fetch-site": "same-site",
"user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36",
"cookie": self.cookies
}
def _build_params(self) -> Dict[str, str]:
"""构建请求参数"""
timestamp = int(time.time() * 1000) # 生成当前时间戳
return {
"uin": self.uin,
"app": "guanwang",
"range": "group",
"check": "10",
"appid": "101481127",
"openid": self.openid,
"fskey": self.fskey,
"access_token": "undefined",
"callback": "GET_2",
"_": str(timestamp)
}
def _parse_jsonp(self, jsonp_str: str) -> Optional[Dict]:
"""解析JSONP格式响应为JSON"""
try:
# 提取JSON部分去掉前后的函数包裹
match = re.match(r'^GET_2\((.*)\)$', jsonp_str)
if not match:
logger.error("无法解析JSONP响应格式")
return None
json_str = match.group(1)
import json
return json.loads(json_str)
except Exception as e:
logger.error(f"解析JSONP失败: {str(e)}", exc_info=True)
return None
def get_favorites(self) -> Optional[List[Dict]]:
"""获取自选股列表"""
for retry in range(self.retry_count):
try:
logger.info(f"获取自选股列表(第{retry+1}/{self.retry_count}次尝试)")
# 发送请求
response = requests.get(
url=self.base_url,
headers=self.headers,
params=self._build_params(),
timeout=10
)
response.raise_for_status() # 触发HTTP错误状态码的异常
# 解析响应
json_data = self._parse_jsonp(response.text)
if not json_data:
continue
# 检查返回状态
if json_data.get("code") != 0:
logger.error(f"接口返回错误: {json_data.get('msg', '未知错误')}")
continue
# 提取股票列表
stock_list = json_data.get("data", {}).get("grouplist", {}).get("stocklist", [])
logger.info(f"成功获取{len(stock_list)}只自选股")
return stock_list
except requests.exceptions.HTTPError as e:
logger.error(f"HTTP请求错误: {str(e)}", exc_info=True)
except requests.exceptions.ConnectionError:
logger.error("网络连接错误,正在重试...")
except requests.exceptions.Timeout:
logger.error("请求超时,正在重试...")
except Exception as e:
logger.error(f"获取自选股失败: {str(e)}", exc_info=True)
# 重试间隔
if retry < self.retry_count - 1:
time.sleep(self.retry_delay)
logger.error(f"达到最大重试次数({self.retry_count}次),获取自选股失败")
return None
# 使用示例
if __name__ == "__main__":
# 以下参数需要替换为您自己抓包获取的值
USER_CONFIG = {
"uin": "57EE1D146B0F2785875C6064C8A51F06", # 替换为您的uin
"openid": "57EE1D146B0F2785875C6064C8A51F06", # 替换为您的openid
"fskey": "v0b94c9501068774c3728b5d55017473", # 替换为您的fskey
"cookies": "_qimei_q36=; _qimei_h38=cd57f0fb697333e1319643370300000d217b0b; ptcz=f00816984093b2aa10bb5df66bee2bbdbff36f0f08554e195a3f5ee581ec426d; pgv_pvid=8296401627; pac_uid=0_9ZeWrs3t9r911; tvfe_boss_uuid=e9e7fc0254dac269; _qimei_uuid42=18c04121a0610061b7c03e45cb2801033a173a6cc3; RK=pDGU2iWKVW; _qimei_fingerprint=e9c7426ab8cf575a8dcb1e5b06bcec14; suid=user_1_27315909; omgid=1_27315909; pgv_info=ssid=s1753181612165394; _clck=3006684973|1|fxk|0; check=10; appid=101481127; openid=57EE1D146B0F2785875C6064C8A51F06; fskey=v0b94c9501068774c3728b5d55017473; g_openid=oA0GbjrzijlLftzuZ7_e8BrHCaLI; qq_openid=57EE1D146B0F2785875C6064C8A51F06; headimgurl=https%3A%2F%2Fpic.finance.qq.com%2Fuser%2Fheadimg%2Fe0dc7c583728191172a2719481e49ccc; nickname=oscar; zxg_fskey=v0b94c9501068774c3728b5d55017473; zxg_openid=57EE1D146B0F2785875C6064C8A51F06; tgw_l7_route=2068d115e8b0083ed181bb3085b13d57" # 替换为您的完整cookie
}
# 初始化获取器并获取自选股
fetcher = TencentStockFetcher(
uin=USER_CONFIG["uin"],
openid=USER_CONFIG["openid"],
fskey=USER_CONFIG["fskey"],
cookies=USER_CONFIG["cookies"]
)
favorites = fetcher.get_favorites()
if favorites:
print("获取到的自选股列表:")
for idx, stock in enumerate(favorites, 1):
print(f"{idx}. 代码: {stock['symbol']}, 名称: {stock['symbol']}, 市场: {stock['market']}, 类型: {stock['type']}")

View File

@ -0,0 +1,175 @@
import requests
import logging
import time
import json
from typing import List, Dict, Optional, Tuple
logger = logging.getLogger()
class XueQiuStockFetcher:
def __init__(self,
cookies: str,
size: int = 1000,
retry_count: int = 3,
retry_delay: int = 2):
"""
初始化雪球自选股获取器
:param cookies: 完整的cookie字符串从抓包获取
:param size: 最大返回数量
:param retry_count: 重试次数
:param retry_delay: 重试间隔(秒)
"""
self.base_url = "https://stock.xueqiu.com/v5/stock/portfolio"
self.cookies = cookies
self.size = size
self.retry_count = retry_count
self.retry_delay = retry_delay
self.headers = self._build_headers()
def _build_headers(self) -> Dict[str, str]:
"""构建请求头"""
return {
"accept": "application/json, text/plain, */*",
"accept-language": "zh-CN,zh;q=0.9",
"origin": "https://xueqiu.com",
"priority": "u=1, i",
"referer": "https://xueqiu.com/",
"sec-ch-ua": "\"Not)A;Brand\";v=\"8\", \"Chromium\";v=\"138\", \"Google Chrome\";v=\"138\"",
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": "macOS",
"sec-fetch-dest": "empty",
"sec-fetch-mode": "cors",
"sec-fetch-site": "same-site",
"user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36",
"Cookie": self.cookies
}
def _request_with_retry(self, url: str, params: Dict[str, str]) -> Optional[Dict]:
"""带重试机制的通用请求方法"""
for retry in range(self.retry_count):
try:
response = requests.get(
url=url,
headers=self.headers,
params=params,
timeout=10
)
response.raise_for_status()
# 解析JSON响应
try:
return response.json()
except json.JSONDecodeError as e:
logger.error(f"JSON解析失败: {str(e)}")
logger.error(f"响应内容: {response.text[:200]}...")
continue
except requests.exceptions.HTTPError as e:
logger.error(f"HTTP请求错误: {str(e)}", exc_info=True)
except requests.exceptions.ConnectionError:
logger.error("网络连接错误,正在重试...")
except requests.exceptions.Timeout:
logger.error("请求超时,正在重试...")
except Exception as e:
logger.error(f"请求失败: {str(e)}", exc_info=True)
if retry < self.retry_count - 1:
time.sleep(self.retry_delay)
logger.error(f"达到最大重试次数({self.retry_count}次),请求失败")
return None
def get_groups(self) -> Tuple[Optional[List[Dict]], Optional[List[Dict]],
Optional[List[Dict]], Optional[List[Dict]]]:
"""
获取雪球自选股所有分组
:return: 四个分组列表元组 (cubes, funds, stocks, mutualFunds)
"""
url = f"{self.base_url}/list.json"
params = {"system": "true"}
json_data = self._request_with_retry(url, params)
if not json_data:
return None, None, None, None
# 检查错误码
if json_data.get("error_code") != 0:
error_msg = json_data.get("error_description", "未知错误")
logger.error(f"获取分组失败: {error_msg}")
return None, None, None, None
data = json_data.get("data", {})
return (
data.get("cubes"),
data.get("funds"),
data.get("stocks"),
data.get("mutualFunds")
)
def get_stocks_by_group(self, category: int = 1, pid: int = -1) -> Optional[List[Dict]]:
"""
获取指定分组的自选股列表
:param category: 分类ID从分组信息中获取
:param pid: 组合ID从分组信息中获取
:return: 股票列表
"""
url = f"{self.base_url}/stock/list.json"
params = {
"size": str(self.size),
"category": str(category),
"pid": str(pid)
}
json_data = self._request_with_retry(url, params)
if not json_data:
return None
# 检查错误码
if json_data.get("error_code") != 0:
error_msg = json_data.get("error_description", "未知错误")
logger.error(f"获取股票列表失败: {error_msg}")
if "登录" in error_msg or json_data.get("error_code") in [401, 403]:
logger.warning("可能是登录状态失效请更新cookies")
return None
return json_data.get("data", {}).get("stocks", [])
# 使用示例
if __name__ == "__main__":
# 替换为你的实际cookie
USER_COOKIES = "u=5682299253; HMACCOUNT=AA6F9D2598CE96D7; xq_is_login=1; snbim_minify=true; _c_WBKFRo=BuebJX5KAbPh1PGBVFDvQTV7x7VF8W2cvWtaC99v; _nb_ioWEgULi=; cookiesu=661740133906455; device_id=fbe0630e603f726742fec4f9a82eb5fb; s=b312165egu; bid=1f3e6ffcb97fd2d9b4ddda47551d4226_m7fv1brw; Hm_lvt_1db88642e346389874251b5a1eded6e3=1751852390; xq_a_token=a0fd17a76966314ab80c960412f08e3fffb3ec0f; xqat=a0fd17a76966314ab80c960412f08e3fffb3ec0f; xq_id_token=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJ1aWQiOjU2ODIyOTkyNTMsImlzcyI6InVjIiwiZXhwIjoxNzU0NzAzMjk5LCJjdG0iOjE3NTIxMTEyOTkyODYsImNpZCI6ImQ5ZDBuNEFadXAifQ.Vbs-LDgB4bCJI2N644DwfeptdcamKsAm2hbXxlPnJ_0fnTJhXp6T-2Gc6b6jmhTjXJIsWta8IuS0rQBB1L-9fKpUliNFHkv4lr7FW2x7QhrZ1D4lrvjihgBxKHq8yQl31uO6lmUOJkoRaS4LM1pmkSL_UOVyw8aUeuVjETFcJR1HFDHwWpHCLM8kY55fk6n1gEgDZnYNh1_FACqlm6LU4Vq14wfQgyF9sfrGzF8rxXX0nns_j-Dq2k8vN3mknh8yUHyzCyq6Sfqn6NeVdR0vPOciylyTtNq5kOUBFb8uJe48aV2uLGww3dYV8HbsgqW4k0zam3r3QDErfSRVIg-Usw; xq_r_token=1b73cbfb47fcbd8e2055ca4a6dc7a08905dacd7d; Hm_lpvt_1db88642e346389874251b5a1eded6e3=1752714700; is_overseas=0; ssxmod_itna=QqfxBD2D9DRQPY5i7YYxiwS4GhDYu0D0dGMD3qiQGglDFqAPKDHKm=lerDUhGr5h044VYmkTtDlxWeDZDG9dDqx0orXU7BB411D+iENYYe2GG+=3X0xOguYo7I=xmAkwKhSSIXNG2A+DnmeDQKDoxGkDivoD0IYwDiiTx0rD0eDPxDYDG4mDDvvQ84DjmEmFfoGImAeQIoDbORhz74DROdDS73A+IoGqW3Da1A3z8RGDmKDIhjozmoDFOL3Yq0k54i3Y=Ocaq0OZ+BGR0gvh849m1xkHYRr/oRCYQD4KDx5qAxOx20Z3isrfDxRvt70KGitCH4N4DGbh5gYH7x+GksdC58CNR3sx=1mt2qxkGd+QmoC5ZGYdixKG52q4iiqPj53js4D; ssxmod_itna2=QqfxBD2D9DRQPY5i7YYxiwS4GhDYu0D0dGMD3qiQGglDFqAPKDHKm=lerDUhGr5h044VYmkwYDioSBbrtN4=Htz/DUihxz=w4aD"
# 初始化获取器
fetcher = XueQiuStockFetcher(
cookies=USER_COOKIES,
size=1000,
retry_count=3
)
# 1. 获取所有分组
print("===== 获取分组列表 =====")
cubes, funds, stocks_groups, mutual_funds = fetcher.get_groups()
if stocks_groups:
print("股票分组:")
for group in stocks_groups:
print(f"ID: {group['id']}, 名称: {group['name']}, 股票数量: {group['symbol_count']}, 分类: {group['category']}")
# 2. 获取指定分组的股票(以"全部"分组为例)
if stocks_groups and len(stocks_groups) > 0:
for target_group in stocks_groups:
print(f"\n===== 获取分组 [{target_group['name']}] 的股票 =====")
stocks = fetcher.get_stocks_by_group(
category=target_group['category'],
pid=target_group['id']
)
if stocks:
print(f"{len(stocks)}只股票:")
for idx, stock in enumerate(stocks[:10], 1): # 只显示前10只
print(f"{idx}. 代码: {stock['symbol']}, 名称: {stock['name']}, 市场: {stock['exchange']}")
if len(stocks) > 10:
print(f"... 还有{len(stocks)-10}只股票未显示")
time.sleep(1)

View File

@ -7,23 +7,28 @@ Desc: 东方财富网-行情首页-沪深京 A 股
import requests
import pandas as pd
import time
import logging
from functools import lru_cache
def fetch_with_retries_em(url, params, max_retries=3, delay=2):
"""带重试机制的 GET 请求"""
last_err = None
for attempt in range(max_retries):
try:
response = requests.get(url, params=params, timeout=5)
response.raise_for_status()
return response.json()
except requests.RequestException as e:
print(f"请求失败,第 {attempt + 1} 次重试: {e}")
logging.debug(f"请求失败,第 {attempt + 1} 次重试: {e}")
last_err = e
time.sleep(delay)
logging.error(f"reached max({max_retries}) retries and failed. last error: {last_err}")
return None
def stock_zh_a_spot_em(fs='m:0 t:6,m:0 t:80,m:1 t:2,m:1 t:23,m:0 t:81 s:2048', pz=100) -> pd.DataFrame:
def stock_zh_a_spot_em(fs='m:0 t:6,m:0 t:80,m:1 t:2,m:1 t:23,m:0 t:81 s:2048', pz=100, fs_desc='default market') -> pd.DataFrame:
"""
东方财富网-沪深京 A 股-实时行情
https://quote.eastmoney.com/center/gridlist.html#hs_a_board
@ -44,7 +49,7 @@ def stock_zh_a_spot_em(fs='m:0 t:6,m:0 t:80,m:1 t:2,m:1 t:23,m:0 t:81 s:2048', p
"invt": "2",
"fid": "f3",
"fs": fs,
"fields": "f2,f3,f4,f5,f6,f7,f8,f9,f10,f11,f12,f14,f15,f16,f17,f18,f20,f21,f22,f23,f24,f25,f26,f37,f38,f39,f40,f41,f45,f46,f48,f49,f57,f61,f100,f112,f113,f114,f115,f221",
"fields": "f2,f3,f4,f5,f6,f7,f8,f9,f10,f11,f12,f14,f15,f16,f17,f18,f20,f21,f22,f23,f24,f25,f26,f37,f38,f39,f40,f41,f45,f46,f48,f49,f57,f61,f100,f112,f113,f114,f115,f221,f13",
"_": "1623833739532",
}
@ -61,7 +66,7 @@ def stock_zh_a_spot_em(fs='m:0 t:6,m:0 t:80,m:1 t:2,m:1 t:23,m:0 t:81 s:2048', p
# 获取 total 数据来更新 pn_max
if pn == 1:
pn_max = (data_json["data"].get("total", 0) + pz - 1) // pz
print(f'total pages: {pn_max}, total data lines: {data_json["data"].get("total", 0)}, curr lines: {len(diff_data)}, page size: {pz}')
logging.info(f"market: {fs_desc}, total data lines: {data_json['data'].get('total', 0)}, total pages: {pn_max}, curr lines: {len(diff_data)}, page size: {pz}")
pn += 1
time.sleep(0.5) # 防止请求过快
@ -77,7 +82,7 @@ def stock_zh_a_spot_em(fs='m:0 t:6,m:0 t:80,m:1 t:2,m:1 t:23,m:0 t:81 s:2048', p
"f25": "年初至今涨跌幅", "f26": "上市时间", "f37": "加权净资产收益率", "f38": "总股本", "f39": "已流通股份",
"f40": "营业收入", "f41": "营业收入同比增长", "f45": "归属净利润", "f46": "归属净利润同比增长", "f48": "每股未分配利润",
"f49": "毛利率", "f57": "资产负债率", "f61": "每股公积金", "f100": "所处行业", "f112": "每股收益", "f113": "每股净资产",
"f114": "市盈率静", "f115": "市盈率TTM", "f221": "报告期"
"f114": "市盈率静", "f115": "市盈率TTM", "f221": "报告期", "f13": "代码前缀"
}
temp_df.rename(columns=column_map, inplace=True)
@ -85,7 +90,7 @@ def stock_zh_a_spot_em(fs='m:0 t:6,m:0 t:80,m:1 t:2,m:1 t:23,m:0 t:81 s:2048', p
"最新价", "涨跌幅", "涨跌额", "成交量", "成交额", "振幅", "换手率", "量比", "今开", "最高", "最低", "昨收", "涨速", "5分钟涨跌", "60日涨跌幅",
"年初至今涨跌幅", "市盈率动", "市盈率TTM", "市盈率静", "市净率", "每股收益", "每股净资产", "每股公积金", "每股未分配利润",
"加权净资产收益率", "毛利率", "资产负债率", "营业收入", "营业收入同比增长", "归属净利润", "归属净利润同比增长", "总股本", "已流通股份",
"总市值", "流通市值"
"总市值", "流通市值", "代码前缀"
]
for col in numeric_columns:
temp_df[col] = pd.to_numeric(temp_df[col], errors="coerce")
@ -431,7 +436,7 @@ def code_id_map_em() -> dict:
data_json = fetch_with_retries_em(url, params)
if not data_json or "data" not in data_json or "diff" not in data_json["data"]:
print(f"市场 {market_id} 数据获取失败或为空,跳过。")
logging.warning(f"市场 {market_id} 数据获取失败或为空,跳过。")
break
temp_df = pd.DataFrame(data_json["data"]["diff"])
@ -441,7 +446,7 @@ def code_id_map_em() -> dict:
if pn == 1 and "total" in data_json["data"]:
total = int(data_json["data"]["total"])
pn_max = (total // pz) + 1 # 计算最大页数
print(f"市场 {market_id} 总数据量: {total}, 需要页数: {pn_max}, 当前获取数量: {len(temp_df)}, 每页最大拉取行数: {pz}")
logging.info(f"市场 {market_id} 总数据量: {total}, 需要页数: {pn_max}, 当前获取数量: {len(temp_df)}, 每页最大拉取行数: {pz}")
# 按 f13 进行分组并存入字典
grouped = temp_df.groupby('f13')
@ -452,7 +457,7 @@ def code_id_map_em() -> dict:
pn += 1 # 翻页继续
print(f'获取 {market_id} 已获取总股票数: {fetched_cnt}, 总股票数: {total}')
logging.info(f'获取 {market_id} 已获取总股票数: {fetched_cnt}, 总股票数: {total}')
return code_id_dict
@ -501,6 +506,79 @@ def code_id_map_em2() -> dict:
return code_id_dict
def stock_zh_a_hist_new(
em_symbol: str = "106.000001",
period: str = "daily",
start_date: str = "19700101",
end_date: str = "20500101",
adjust: str = "",
) -> pd.DataFrame:
"""
东方财富网-行情首页-沪深京 A 股-每日行情
https://quote.eastmoney.com/concept/sh603777.html?from=classic
:param symbol: 股票代码
:type symbol: str
:param period: choice of {'daily', 'weekly', 'monthly'}
:type period: str
:param start_date: 开始日期
:type start_date: str
:param end_date: 结束日期
:type end_date: str
:param adjust: choice of {"qfq": "前复权", "hfq": "后复权", "": "不复权"}
:type adjust: str
:return: 每日行情
:rtype: pandas.DataFrame
"""
adjust_dict = {"qfq": "1", "hfq": "2", "": "0"}
period_dict = {"daily": "101", "weekly": "102", "monthly": "103"}
url = "http://push2his.eastmoney.com/api/qt/stock/kline/get"
params = {
"fields1": "f1,f2,f3,f4,f5,f6",
"fields2": "f51,f52,f53,f54,f55,f56,f57,f58,f59,f60,f61,f116",
"ut": "7eea3edcaed734bea9cbfc24409ed989",
"klt": period_dict[period],
"fqt": adjust_dict[adjust],
"secid": em_symbol,
"beg": start_date,
"end": end_date,
"_": "1623766962675",
}
data_json = fetch_with_retries_em(url, params)
if not data_json or not (data_json["data"] and data_json["data"]["klines"]):
return pd.DataFrame()
temp_df = pd.DataFrame(
[item.split(",") for item in data_json["data"]["klines"]]
)
temp_df.columns = [
"日期",
"开盘",
"收盘",
"最高",
"最低",
"成交量",
"成交额",
"振幅",
"涨跌幅",
"涨跌额",
"换手率",
]
temp_df.index = pd.to_datetime(temp_df["日期"])
temp_df.reset_index(inplace=True, drop=True)
temp_df["开盘"] = pd.to_numeric(temp_df["开盘"])
temp_df["收盘"] = pd.to_numeric(temp_df["收盘"])
temp_df["最高"] = pd.to_numeric(temp_df["最高"])
temp_df["最低"] = pd.to_numeric(temp_df["最低"])
temp_df["成交量"] = pd.to_numeric(temp_df["成交量"])
temp_df["成交额"] = pd.to_numeric(temp_df["成交额"])
temp_df["振幅"] = pd.to_numeric(temp_df["振幅"])
temp_df["涨跌幅"] = pd.to_numeric(temp_df["涨跌幅"])
temp_df["涨跌额"] = pd.to_numeric(temp_df["涨跌额"])
temp_df["换手率"] = pd.to_numeric(temp_df["换手率"])
return temp_df
def stock_zh_a_hist(
symbol: str = "000001",
period: str = "daily",

View File

@ -1,607 +0,0 @@
000001
000002
000063
000100
000157
000166
000301
000333
000338
000408
000425
000538
000568
000596
000617
000625
000651
000661
000708
000725
000733
000768
000776
000786
000792
000800
000807
000858
000876
000895
000938
000963
000977
000983
000999
001289
001965
001979
002001
002007
002027
002049
002050
002074
002129
002142
002179
002180
002230
002236
002241
002252
002271
002304
002311
002352
002371
002410
002415
002459
002460
002466
002475
002493
002555
002594
002601
002603
002648
002709
002714
002736
002812
002821
002841
002916
002920
002938
003816
300014
300015
300033
300059
300122
300124
300142
300223
300274
300308
300316
300347
300408
300413
300418
300433
300442
300450
300454
300496
300498
300628
300661
300750
300751
300759
300760
300782
300832
300896
300919
300957
300979
300999
301269
600000
600009
600010
600011
600015
600016
600018
600019
600023
600025
600026
600027
600028
600029
600030
600031
600036
600039
600048
600050
600061
600085
600089
600104
600111
600115
600132
600150
600161
600176
600183
600188
600196
600219
600233
600276
600309
600332
600346
600362
600372
600406
600415
600426
600436
600438
600460
600489
600515
600519
600547
600570
600584
600585
600588
600600
600660
600674
600690
600732
600741
600745
600760
600795
600803
600809
600837
600845
600875
600886
600887
600893
600900
600905
600918
600919
600926
600938
600941
600958
600989
600999
601006
601009
601012
601021
601059
601066
601088
601100
601111
601117
601138
601166
601169
601186
601211
601225
601229
601236
601238
601288
601318
601319
601328
601336
601360
601377
601390
601398
601600
601601
601607
601618
601628
601633
601658
601668
601669
601688
601689
601698
601699
601728
601766
601788
601799
601800
601808
601816
601818
601838
601857
601865
601868
601872
601877
601878
601881
601888
601898
601899
601901
601916
601919
601939
601985
601988
601989
601995
601998
603019
603195
603259
603260
603288
603296
603369
603392
603501
603659
603799
603806
603833
603899
603986
603993
605117
605499
688008
688009
688012
688036
688041
688082
688111
688126
688187
688223
688256
688271
688303
688363
688396
688599
688981
000009
000034
000035
000039
000066
000069
000400
000423
000519
000547
000591
000623
000629
000630
000683
000690
000738
000818
000830
000831
000878
000887
000932
000933
000960
000967
000975
000988
000998
001914
002008
002025
002028
002044
002064
002065
002078
002080
002081
002085
002091
002120
002123
002131
002138
002145
002151
002152
002156
002176
002185
002192
002195
002202
002212
002223
002240
002245
002266
002268
002273
002281
002292
002294
002312
002340
002353
002368
002372
002384
002389
002396
002405
002407
002409
002414
002422
002432
002436
002439
002444
002456
002463
002465
002472
002497
002508
002511
002517
002532
002541
002544
002558
002572
002602
002607
002624
002625
002738
002739
002756
002791
002831
300001
300002
300003
300012
300017
300024
300037
300054
300058
300068
300070
300073
300088
300118
300133
300136
300144
300182
300207
300212
300251
300253
300285
300296
300315
300339
300346
300383
300390
300394
300395
300438
300457
300459
300474
300502
300529
300558
300567
300568
300573
300595
300601
300604
300627
300676
300699
300724
300763
300769
300866
301236
301558
600004
600007
600008
600038
600066
600096
600118
600129
600131
600141
600143
600153
600157
600160
600166
600167
600170
600177
600256
600258
600316
600323
600325
600352
600392
600398
600399
600418
600482
600486
600497
600498
600499
600516
600521
600529
600535
600549
600563
600637
600655
600667
600699
600704
600754
600755
600763
600765
600771
600816
600820
600839
600859
600862
600867
600879
600884
600885
600895
600959
600988
600998
601058
601155
601168
601216
601233
601615
601636
601677
601727
601866
601880
601966
603000
603077
603129
603156
603236
603290
603444
603456
603486
603568
603588
603596
603605
603606
603613
603650
603688
603737
603816
603882
603885
603939
605358
688002
688005
688063
688072
688099
688122
688169
688188
688390
688536
688598
688617
688777
688001
688019
688037
688047
688048
688052
688106
688107
688110
688120
688123
688141
688146
688153
688172
688200
688213
688220
688234
688249
688279
688347
688352
688361
688362
688385
688409
688432
688484
688498
688521
688525
688582
688596
688608
688702
688728
688798
688349
688472
688506

View File

@ -1,11 +0,0 @@
689009
688981
688517
000001
000002
000001
688353
688093
688303
000063
000100

View File

@ -0,0 +1,127 @@
import akshare as ak
import pandas as pd
import datetime
import time
import logging
# 配置日志
logging.basicConfig(
filename='stock_signals.log',
level=logging.INFO,
format='%(asctime)s [%(levelname)s] %(message)s',
)
# 板块关键词
TARGET_SECTORS = ['互联网服务', '芯片', '消费', '房地产']
def fetch_sector_stocks(sector_name):
"""
获取某个行业板块的股票列表
"""
try:
df_plates = ak.stock_board_industry_name_em()
if not df_plates['板块名称'].isin([sector_name]).any():
logging.warning(f"{sector_name} not exists!")
return pd.DataFrame()
df = ak.stock_board_industry_cons_em(symbol=sector_name)
return df[['代码', '名称']]
except Exception as e:
logging.error(f"获取板块 {sector_name} 股票失败: {e}")
return pd.DataFrame()
def compute_signals(df):
"""
根据行情数据计算交易信号
"""
signals = []
for _, row in df.iterrows():
try:
code = row['代码']
name = row['名称']
pct_today = row['当日涨跌幅']
pct_year = row['年内涨跌幅']
pe = row['市盈率']
pb = row['市净率']
signal = ''
if pct_today < -5:
signal += '今日大跌; '
if pct_year < -20:
signal += '年内大跌; '
if pe < 50:
signal += '低市盈率; '
if signal:
signals.append({
'代码': code,
'名称': name,
'当日涨跌幅': pct_today,
'年内涨跌幅': pct_year,
'市盈率': pe,
'市净率': pb,
'信号': signal.strip()
})
except Exception as e:
logging.warning(f"处理股票 {row} 时出错: {e}")
return pd.DataFrame(signals)
def fetch_and_analyze():
"""
获取行情并计算信号
"""
logging.info("开始获取并分析行情数据")
all_stocks = pd.DataFrame()
for sector in TARGET_SECTORS:
df = fetch_sector_stocks(sector)
if df.empty:
continue
logging.info(f"获取到板块 [{sector}] {len(df)} 只股票")
for code in df['代码']:
try:
# 获取日K线
kline = ak.stock_zh_a_hist(symbol=code, period='daily', adjust='qfq')
kline['日期'] = pd.to_datetime(kline['日期'])
kline.set_index('日期', inplace=True)
today = kline.iloc[-1]
close_today = today['收盘']
close_5d = kline.iloc[-5]['收盘'] if len(kline) >= 5 else today['收盘']
close_month = kline.iloc[-21]['收盘'] if len(kline) >= 21 else today['收盘']
close_year = kline.iloc[0]['收盘']
pct_today = (close_today / today['开盘'] - 1) * 100
pct_5d = (close_today / close_5d -1) * 100
pct_month = (close_today / close_month -1) * 100
pct_year = (close_today / close_year -1) *100
# 获取市盈率、市净率
fundamentals = ak.stock_a_lg_indicator(symbol=code)
pe = fundamentals.iloc[-1]['市盈率(TTM)']
pb = fundamentals.iloc[-1]['市净率']
all_stocks = pd.concat([all_stocks, pd.DataFrame([{
'代码': code,
'名称': df.loc[df['代码']==code, '名称'].values[0],
'当日涨跌幅': pct_today,
'5日涨跌幅': pct_5d,
'本月涨跌幅': pct_month,
'年内涨跌幅': pct_year,
'市盈率': pe,
'市净率': pb
}])])
except Exception as e:
logging.error(f"获取股票 {code} 数据失败: {e}")
continue
signals = compute_signals(all_stocks)
if not signals.empty:
signals.to_csv(f'stock_signals_{datetime.date.today()}.csv', index=False, encoding='utf-8-sig')
logging.info(f"生成信号 {len(signals)} 条,已写入 CSV。")
else:
logging.info("未发现符合条件的交易信号")
if __name__ == "__main__":
fetch_and_analyze()

View File

@ -0,0 +1,361 @@
import akshare as ak
import schedule
import time
import logging
from datetime import datetime, timedelta
import pandas as pd
import numpy as np
from typing import List, Dict, Tuple
# 配置日志记录
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler("stock_analysis.log", encoding='utf-8'),
logging.StreamHandler()
]
)
logger = logging.getLogger("stock_analyzer")
# 关注的板块列表
WATCH_SECTORS = {
"互联网": "互联网服务",
"芯片": "半导体及元件",
"消费": "消费电子",
"房地产": "房地产开发"
}
# 技术指标参数配置
TECHNICAL_PARAMS = {
"rsi_period": 14,
"macd_fast": 12,
"macd_slow": 26,
"macd_signal": 9,
"bollinger_period": 20
}
# 异动检测阈值
ANOMALY_THRESHOLDS = {
"daily_drop": -5, # 当日跌幅超过5%
"yearly_drop": -20, # 本年跌幅超过20%
"pe_threshold": 50 # 市盈率低于50
}
def fetch_sector_stocks(sector_name):
"""
获取某个行业板块的股票列表
"""
try:
df_plates = ak.stock_board_industry_name_em()
if not df_plates['板块名称'].isin([sector_name]).any():
logging.warning(f"{sector_name} not exists!")
return pd.DataFrame()
df = ak.stock_board_industry_cons_em(symbol=sector_name)
return df[['代码', '名称']]
except Exception as e:
logging.error(f"获取板块 {sector_name} 股票失败: {e}")
return pd.DataFrame()
def get_sector_stocks(sector_name: str, sector_code: str) -> List[str]:
"""获取指定板块的股票列表"""
try:
logger.info(f"获取[{sector_name}]板块股票列表")
# 使用同花顺概念板块获取股票列表
stock_df = fetch_sector_stocks(sector_code)
# 提取股票代码并去重
stock_codes = list(set(stock_df["代码"].tolist()))
logger.info(f"成功获取[{sector_name}]板块股票{len(stock_codes)}")
# 为避免请求过于频繁返回前20只股票
return stock_codes[:20]
except Exception as e:
logger.error(f"获取[{sector_name}]板块股票列表失败: {str(e)}", exc_info=True)
return []
def get_stock_data(stock_code: str) -> Dict:
"""获取个股详细数据"""
try:
logger.info(f"获取股票[{stock_code}]数据")
# 获取实时行情数据
spot_df = ak.stock_zh_a_spot_em()
stock_info = spot_df[spot_df["代码"] == stock_code]
if stock_info.empty:
logger.warning(f"未找到股票[{stock_code}]的实时数据")
return None
stock_info = stock_info.iloc[0]
current_price = stock_info["最新价"]
stock_name = stock_info["名称"]
# 计算日期范围
today = datetime.now().strftime("%Y%m%d")
five_days_ago = (datetime.now() - timedelta(days=7)).strftime("%Y%m%d") # 包含周末
month_ago = (datetime.now() - timedelta(days=30)).strftime("%Y%m%d")
year_start = datetime.now().replace(month=1, day=1).strftime("%Y%m%d")
# 获取历史行情数据
history_df = ak.stock_zh_a_daily(symbol=stock_code, start_date=year_start, end_date=today)
if history_df.empty:
logger.warning(f"未找到股票[{stock_code}]的历史数据")
return None
# 计算各类涨跌幅
indicators = {
"代码": stock_code,
"名称": stock_name,
"当前价": current_price,
"当日涨跌幅(%)": stock_info["涨跌幅"],
"五日涨跌幅(%)": calculate_change(history_df, 5),
"本月涨跌幅(%)": calculate_change(history_df, days=30),
"本年涨跌幅(%)": calculate_change(history_df, start_date=year_start),
"市盈率(PE)": get_valuation_indicator(stock_code, "pe"),
"市净率(PB)": get_valuation_indicator(stock_code, "pb"),
"技术指标": calculate_technical_indicators(history_df),
"更新时间": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
}
logger.info(f"成功获取股票[{stock_code}({stock_name})]数据")
return indicators
except Exception as e:
logger.error(f"获取股票[{stock_code}]数据失败: {str(e)}", exc_info=True)
return None
def calculate_change(history_df: pd.DataFrame, days: int = None, start_date: str = None) -> float:
"""计算涨跌幅"""
try:
if history_df.empty:
return None
# 按天数计算
if days:
if len(history_df) >= days:
start_price = history_df.iloc[-days]["收盘"]
end_price = history_df.iloc[-1]["收盘"]
return (end_price - start_price) / start_price * 100
else:
return None
# 按起始日期计算
if start_date:
# 转换日期格式
history_df["日期"] = pd.to_datetime(history_df["日期"])
start_date_obj = datetime.strptime(start_date, "%Y%m%d")
# 找到起始日期后的数据
start_row = history_df[history_df["日期"] >= start_date_obj].iloc[0]
end_row = history_df.iloc[-1]
return (end_row["收盘"] - start_row["开盘"]) / start_row["开盘"] * 100
except Exception as e:
logger.error(f"计算涨跌幅失败: {str(e)}")
return None
def get_valuation_indicator(stock_code: str, indicator: str) -> float:
"""获取估值指标(市盈率、市净率)"""
try:
valuation_df = ak.stock_a_indicator_lg(symbol=stock_code)
if valuation_df.empty or indicator not in valuation_df.columns:
return None
# 返回最新的指标值
return valuation_df[indicator].iloc[-1]
except Exception as e:
logger.error(f"获取股票[{stock_code}]的{indicator}失败: {str(e)}")
return None
def calculate_technical_indicators(history_df: pd.DataFrame) -> Dict:
"""计算技术指标"""
indicators = {}
try:
if history_df.empty:
return indicators
# 确保有足够的数据计算指标
if len(history_df) < TECHNICAL_PARAMS["bollinger_period"]:
logger.warning("历史数据不足,无法计算所有技术指标")
return indicators
close_prices = history_df["收盘"]
# 计算RSI
delta = close_prices.diff()
gain = (delta.where(delta > 0, 0)).rolling(window=TECHNICAL_PARAMS["rsi_period"]).mean()
loss = (-delta.where(delta < 0, 0)).rolling(window=TECHNICAL_PARAMS["rsi_period"]).mean()
rs = gain / loss
indicators["RSI"] = 100 - (100 / (1 + rs)).iloc[-1]
# 计算MACD
ema_fast = close_prices.ewm(span=TECHNICAL_PARAMS["macd_fast"], adjust=False).mean()
ema_slow = close_prices.ewm(span=TECHNICAL_PARAMS["macd_slow"], adjust=False).mean()
indicators["MACD"] = (ema_fast - ema_slow).iloc[-1]
indicators["MACD_Signal"] = indicators["MACD"].ewm(span=TECHNICAL_PARAMS["macd_signal"], adjust=False).mean()
# 计算布林带
sma = close_prices.rolling(window=TECHNICAL_PARAMS["bollinger_period"]).mean()
std = close_prices.rolling(window=TECHNICAL_PARAMS["bollinger_period"]).std()
indicators["布林带上轨"] = (sma + 2 * std).iloc[-1]
indicators["布林带中轨"] = sma.iloc[-1]
indicators["布林带下轨"] = (sma - 2 * std).iloc[-1]
return indicators
except Exception as e:
logger.error(f"计算技术指标失败: {str(e)}")
return indicators
def detect_anomalies_and_signals(stock_data: Dict) -> Tuple[List[str], List[str]]:
"""检测股票异动和生成交易信号"""
anomalies = []
signals = []
if not stock_data:
return anomalies, signals
# 检测异动情况
# 当日跌幅超过阈值
if (stock_data["当日涨跌幅(%)"] is not None and
stock_data["当日涨跌幅(%)"] < ANOMALY_THRESHOLDS["daily_drop"]):
anomalies.append(
f"当日跌幅超过{abs(ANOMALY_THRESHOLDS['daily_drop'])}%: "
f"{stock_data['当日涨跌幅(%)']:.2f}%"
)
# 本年跌幅超过阈值
if (stock_data["本年涨跌幅(%)"] is not None and
stock_data["本年涨跌幅(%)"] < ANOMALY_THRESHOLDS["yearly_drop"]):
anomalies.append(
f"本年跌幅超过{abs(ANOMALY_THRESHOLDS['yearly_drop'])}%: "
f"{stock_data['本年涨跌幅(%)']:.2f}%"
)
# 市盈率低于阈值
if (stock_data["市盈率(PE)"] is not None and
stock_data["市盈率(PE)"] > 0 and # 排除负市盈率
stock_data["市盈率(PE)"] < ANOMALY_THRESHOLDS["pe_threshold"]):
anomalies.append(
f"市盈率低于{ANOMALY_THRESHOLDS['pe_threshold']}: "
f"{stock_data['市盈率(PE)']:.2f}"
)
# 生成交易信号
ti = stock_data.get("技术指标", {})
# RSI信号
if "RSI" in ti:
if ti["RSI"] < 30:
signals.append(f"RSI超卖({ti['RSI']:.2f}),可能的买入信号")
elif ti["RSI"] > 70:
signals.append(f"RSI超买({ti['RSI']:.2f}),可能的卖出信号")
# MACD信号
if "MACD" in ti and "MACD_Signal" in ti:
if ti["MACD"] > ti["MACD_Signal"]:
signals.append(f"MACD金叉可能的买入信号")
else:
signals.append(f"MACD死叉可能的卖出信号")
# 布林带信号
if ("布林带上轨" in ti and "布林带下轨" in ti and
stock_data["当前价"] is not None):
current_price = stock_data["当前价"]
if current_price < ti["布林带下轨"]:
signals.append(f"价格低于布林带下轨,可能的买入信号")
elif current_price > ti["布林带上轨"]:
signals.append(f"价格高于布林带上轨,可能的卖出信号")
return anomalies, signals
def analyze_sector(sector_name: str, sector_code: str):
"""分析指定板块"""
logger.info(f"\n===== 开始分析[{sector_name}]板块 =====")
# 获取板块股票列表
stock_codes = get_sector_stocks(sector_name, sector_code)
if not stock_codes:
logger.warning(f"[{sector_name}]板块没有可分析的股票")
return
# 分析每只股票
for stock_code in stock_codes:
stock_data = get_stock_data(stock_code)
if not stock_data:
continue
# 检测异动和信号
anomalies, signals = detect_anomalies_and_signals(stock_data)
# 记录分析结果
logger.info(f"\n----- 股票: {stock_data['代码']} {stock_data['名称']} -----")
logger.info(f"当前价: {stock_data['当前价']:.2f}")
logger.info(f"当日涨跌幅: {stock_data['当日涨跌幅(%)']:.2f}%")
logger.info(f"五日涨跌幅: {stock_data['五日涨跌幅(%)']:.2f}%" if stock_data['五日涨跌幅(%)'] else "五日涨跌幅: 数据不足")
logger.info(f"本月涨跌幅: {stock_data['本月涨跌幅(%)']:.2f}%" if stock_data['本月涨跌幅(%)'] else "本月涨跌幅: 数据不足")
logger.info(f"本年涨跌幅: {stock_data['本年涨跌幅(%)']:.2f}%" if stock_data['本年涨跌幅(%)'] else "本年涨跌幅: 数据不足")
logger.info(f"市盈率(PE): {stock_data['市盈率(PE)']:.2f}" if stock_data['市盈率(PE)'] else "市盈率(PE): 数据不足")
logger.info(f"市净率(PB): {stock_data['市净率(PB)']:.2f}" if stock_data['市净率(PB)'] else "市净率(PB): 数据不足")
if anomalies:
logger.warning("----- 异动警告 -----")
for anomaly in anomalies:
logger.warning(f"- {anomaly}")
if signals:
logger.info("----- 交易信号 -----")
for signal in signals:
logger.info(f"- {signal}")
logger.info(f"===== [{sector_name}]板块分析完成 =====")
def daily_analysis():
"""每日分析任务"""
logger.info("\n\n=====================================")
logger.info("===== 开始每日股票分析任务 =====")
logger.info(f"分析时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
logger.info("=====================================")
# 分析所有关注的板块
for sector_name, sector_code in WATCH_SECTORS.items():
analyze_sector(sector_name, sector_code)
logger.info("\n===== 每日股票分析任务完成 =====")
def main():
"""主函数"""
logger.info("股票分析程序启动")
# 测试时可以立即运行一次
logger.info("执行首次测试分析")
daily_analysis()
# 设置定时任务每天17:00运行
logger.info("设置定时任务每天17:00执行分析")
schedule.every().day.at("17:00").do(daily_analysis)
# 运行定时任务
try:
while True:
schedule.run_pending()
time.sleep(60) # 每分钟检查一次
except KeyboardInterrupt:
logger.info("程序被用户中断")
except Exception as e:
logger.error(f"程序运行出错: {str(e)}", exc_info=True)
finally:
logger.info("股票分析程序退出")
if __name__ == "__main__":
main()

View File

@ -18,6 +18,7 @@ import pymysql
import logging
import csv
import os
import re
import time
from datetime import datetime
from futu import OpenQuoteContext, RET_OK # Futu API client
@ -26,6 +27,7 @@ import argparse
import src.crawling.stock_hist_em as his_em
import src.logger.logger as logger
import src.config.config as config
from src.crawler.zixuan.xueqiu_zixuan import XueQiuStockFetcher
# 配置日志
logger.setup_logging()
@ -41,58 +43,36 @@ def flush_code_map():
print(code_id_map_em_df)
return code_id_map_em_df
# 获取历史K线如果失败就重试
def fetch_with_retry(code: str, s_date, e_date, adjust: str = '', max_retries: int = 3) -> pd.DataFrame :
retries = 0
while retries < max_retries:
try:
# 调用 stock_zh_a_hist 获取历史数据
df = his_em.stock_zh_a_hist(
symbol=code,
period="daily",
start_date=s_date,
end_date=e_date,
adjust=adjust,
)
# 如果获取到的数据为空,记录日志并重试
if df.empty:
logging.info(f'{code} empty data. retry...')
retries += 1
time.sleep(3) # 每次重试前休眠 3 秒
else:
return df
except Exception as e:
retries += 1
time.sleep(3) # 每次重试前休眠 3 秒
return pd.DataFrame()
# 获取所有市场的当年股价快照,带重试机制。
def fetch_snap_all(max_retries: int = 3) -> pd.DataFrame:
# 检查文件是否存在
file_name = f'{res_dir}/snapshot_em_{current_date}.csv'
if os.path.exists(file_name):
try:
# 读取本地文件
snap_data = pd.read_csv(file_name, encoding='utf-8')
logging.info(f"load snapshot data from local: {file_name}\n\n")
return snap_data
except Exception as e:
logging.warning(f"读取本地文件失败: {e},将重新拉取数据\n\n")
# 拉取数据
market_fs = {"china_a": "m:0 t:6,m:0 t:80,m:1 t:2,m:1 t:23,m:0 t:81 s:2048",
"hk": "m:128 t:3,m:128 t:4,m:128 t:1,m:128 t:2",
"us": "m:105,m:106,m:107"}
result = pd.DataFrame()
for market_id, fs in market_fs.items():
retries = 0
while retries < max_retries:
try:
df = his_em.stock_zh_a_spot_em(fs)
# 如果获取到的数据为空,记录日志并重试
if df.empty:
logging.warning(f'{market_id} empty data. retry...')
retries += 1
time.sleep(3) # 每次重试前休眠 3 秒
else:
print(f'get {market_id} stock snapshot. stock count: {len(df)}')
result = pd.concat([result, df], ignore_index=True)
break
except Exception as e:
retries += 1
time.sleep(3) # 每次重试前休眠 3 秒
if retries >= max_retries:
logging.warning(f'{market_id} fetching error.')
df = his_em.stock_zh_a_spot_em(fs, fs_desc=market_id)
if df.empty:
logging.warning(f'{market_id} empty data. please check.')
return pd.DataFrame()
else:
logging.info(f'get {market_id} stock snapshot. stock count: {len(df)}')
result = pd.concat([result, df], ignore_index=True)
result.to_csv(file_name, index=False, encoding='utf-8')
logging.info(f"get snapshot data and write to file: {file_name}\n\n")
return result
@ -127,6 +107,55 @@ def load_index_codes():
conn.close()
return hs300_data + hk_data + us_data
def format_stock_code(code):
"""
用正则表达式将 "SZ300750" 转换为 "SZ.300750"
"""
# 正则模式匹配开头的1个或多个字母 followed by 1个或多个数字
pattern = r'^([A-Za-z]+)(\d+)$'
match = re.match(pattern, code)
if match:
# 提取字母部分和数字部分,用点号拼接
letters = match.group(1)
numbers = match.group(2)
return f"{letters}.{numbers}"
else:
# 不匹配模式时返回原始字符串(如已包含点号、有其他字符等)
return code
def load_xueqiu_codes():
# 替换为你的实际cookie
USER_COOKIES = "u=5682299253; HMACCOUNT=AA6F9D2598CE96D7; xq_is_login=1; snbim_minify=true; _c_WBKFRo=BuebJX5KAbPh1PGBVFDvQTV7x7VF8W2cvWtaC99v; _nb_ioWEgULi=; cookiesu=661740133906455; device_id=fbe0630e603f726742fec4f9a82eb5fb; s=b312165egu; bid=1f3e6ffcb97fd2d9b4ddda47551d4226_m7fv1brw; Hm_lvt_1db88642e346389874251b5a1eded6e3=1751852390; xq_a_token=a0fd17a76966314ab80c960412f08e3fffb3ec0f; xqat=a0fd17a76966314ab80c960412f08e3fffb3ec0f; xq_id_token=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJ1aWQiOjU2ODIyOTkyNTMsImlzcyI6InVjIiwiZXhwIjoxNzU0NzAzMjk5LCJjdG0iOjE3NTIxMTEyOTkyODYsImNpZCI6ImQ5ZDBuNEFadXAifQ.Vbs-LDgB4bCJI2N644DwfeptdcamKsAm2hbXxlPnJ_0fnTJhXp6T-2Gc6b6jmhTjXJIsWta8IuS0rQBB1L-9fKpUliNFHkv4lr7FW2x7QhrZ1D4lrvjihgBxKHq8yQl31uO6lmUOJkoRaS4LM1pmkSL_UOVyw8aUeuVjETFcJR1HFDHwWpHCLM8kY55fk6n1gEgDZnYNh1_FACqlm6LU4Vq14wfQgyF9sfrGzF8rxXX0nns_j-Dq2k8vN3mknh8yUHyzCyq6Sfqn6NeVdR0vPOciylyTtNq5kOUBFb8uJe48aV2uLGww3dYV8HbsgqW4k0zam3r3QDErfSRVIg-Usw; xq_r_token=1b73cbfb47fcbd8e2055ca4a6dc7a08905dacd7d; Hm_lpvt_1db88642e346389874251b5a1eded6e3=1752714700; is_overseas=0; ssxmod_itna=QqfxBD2D9DRQPY5i7YYxiwS4GhDYu0D0dGMD3qiQGglDFqAPKDHKm=lerDUhGr5h044VYmkTtDlxWeDZDG9dDqx0orXU7BB411D+iENYYe2GG+=3X0xOguYo7I=xmAkwKhSSIXNG2A+DnmeDQKDoxGkDivoD0IYwDiiTx0rD0eDPxDYDG4mDDvvQ84DjmEmFfoGImAeQIoDbORhz74DROdDS73A+IoGqW3Da1A3z8RGDmKDIhjozmoDFOL3Yq0k54i3Y=Ocaq0OZ+BGR0gvh849m1xkHYRr/oRCYQD4KDx5qAxOx20Z3isrfDxRvt70KGitCH4N4DGbh5gYH7x+GksdC58CNR3sx=1mt2qxkGd+QmoC5ZGYdixKG52q4iiqPj53js4D; ssxmod_itna2=QqfxBD2D9DRQPY5i7YYxiwS4GhDYu0D0dGMD3qiQGglDFqAPKDHKm=lerDUhGr5h044VYmkwYDioSBbrtN4=Htz/DUihxz=w4aD"
# 初始化获取器
fetcher = XueQiuStockFetcher(
cookies=USER_COOKIES,
size=1000,
retry_count=3
)
all_codes = []
stocks = fetcher.get_stocks_by_group(
category=1, # 股票
pid=-1 # 全部
)
if stocks:
for item in stocks:
code = item['symbol']
mkt = item['marketplace']
if mkt:
if mkt.lower() == 'cn':
code = format_stock_code(code)
elif mkt.lower() == 'hk':
code = f"HK.{code}"
else:
code = f"US.{code}"
all_codes.append({'code': code, 'code_name': item['name']})
return all_codes
# 读取富途自选股的指定分类股
def load_futu_all_codes():
quote_ctx = OpenQuoteContext(host='127.0.0.1', port=11111)
@ -143,14 +172,6 @@ def load_futu_all_codes():
return stock_data
# 获取特定的行
def get_specific_date_row(data, date):
"""获取特定日期的行"""
for row in data:
if row['日期'] == date:
return row
return None
# 获取股票数据,并统计收益率
def calculate_stock_statistics(market, code, code_name):
try:
@ -158,10 +179,16 @@ def calculate_stock_statistics(market, code, code_name):
last_year = datetime.now().year - 1
last_year_str = str(last_year)
# 获取历史数据
data = fetch_with_retry(code, "20210101", current_date, 'qfq')
# 调用 stock_zh_a_hist 获取历史数据
data = his_em.stock_zh_a_hist_new(
em_symbol=code,
period="daily",
start_date="20210101",
end_date=current_date,
adjust='qfq',
)
if data.empty:
logging.warning(f'{code}, {code_name} has no data. skipping...')
#logging.warning(f'fetch data for {code}, {code_name} failed. skipping...')
return None
# 获取当前日期的股价
@ -173,7 +200,7 @@ def calculate_stock_statistics(market, code, code_name):
# 获取年初股价,也就是上一年的最后一个交易日的收盘价
year_data = data[data['日期'].str.startswith(last_year_str)]
if year_data.empty:
logging.warning(f"{code}, {code_name} 未找到上一年的数据 ({last_year_str}), 以 {defaut_row['日期']} 的数据来代替")
logging.debug(f"{code}, {code_name} 未找到上一年的数据 ({last_year_str}), 以 {defaut_row['日期']} 的数据来代替")
year_begin_row = defaut_row
else:
year_begin_row = year_data.loc[year_data['日期'].idxmax()]
@ -182,35 +209,35 @@ def calculate_stock_statistics(market, code, code_name):
try:
row_0923 = data[data['日期'] == '2024-09-23'].iloc[0]
except IndexError:
logging.warning(f"{code}, {code_name} 未找到0923的数据, 以 {defaut_row['日期']} 的数据来代替")
logging.debug(f"{code}, {code_name} 未找到0923的数据, 以 {defaut_row['日期']} 的数据来代替")
row_0923 = defaut_row
# 获取0930收盘价
try:
row_0930 = data[data['日期'] == '2024-09-30'].iloc[0]
except IndexError:
logging.warning(f"{code}, {code_name} 未找到0930的数据, 以 {defaut_row['日期']} 的数据来代替")
logging.debug(f"{code}, {code_name} 未找到0930的数据, 以 {defaut_row['日期']} 的数据来代替")
row_0930 = defaut_row
# 获取1008开盘价、收盘价
try:
row_1008 = data[data['日期'] == '2024-10-08'].iloc[0]
except IndexError:
logging.warning(f"{code}, {code_name} 未找到1008的数据, 以 {defaut_row['日期']} 的数据来代替")
logging.debug(f"{code}, {code_name} 未找到1008的数据, 以 {defaut_row['日期']} 的数据来代替")
row_1008 = defaut_row
# 获取0403收盘价
try:
row_0403 = data[data['日期'] == '2025-04-03'].iloc[0]
except IndexError:
logging.warning(f"{code}, {code_name} 未找到0403的数据, 以 {defaut_row['日期']} 的数据来代替")
logging.debug(f"{code}, {code_name} 未找到0403的数据, 以 {defaut_row['日期']} 的数据来代替")
row_0403 = defaut_row
# 获取0407收盘价
try:
row_0407 = data[data['日期'] == '2025-04-07'].iloc[0]
except IndexError:
logging.warning(f"{code}, {code_name} 未找到0407的数据, 以 {defaut_row['日期']} 的数据来代替")
logging.debug(f"{code}, {code_name} 未找到0407的数据, 以 {defaut_row['日期']} 的数据来代替")
row_0407 = defaut_row
# 获取2021年以来的最高价
@ -221,7 +248,7 @@ def calculate_stock_statistics(market, code, code_name):
# 获取年内的最高价、最低价
year_data = data[data['日期'].str.startswith(current_year)]
if year_data.empty:
logging.warning(f"{code}, {code_name} 未找到年内的数据, 以 {defaut_row['日期']} 的数据来代替")
logging.debug(f"{code}, {code_name} 未找到年内的数据, 以 {defaut_row['日期']} 的数据来代替")
year_min_row = defaut_row
year_max_row = defaut_row
else:
@ -300,27 +327,29 @@ def write_to_csv(results, filename):
# 主函数,执行逻辑
def main(list, debug):
futu_codes = []
xueqiu_codes = []
index_codes = []
if list == 'futu':
futu_codes = load_futu_all_codes()
elif list == 'xueqiu':
xueqiu_codes = load_xueqiu_codes()
elif list == 'all':
futu_codes = load_futu_all_codes()
xueqiu_codes = load_xueqiu_codes()
index_codes = load_index_codes()
else:
index_codes = load_index_codes()
codes = futu_codes + index_codes
codes = futu_codes + index_codes + xueqiu_codes
all_results = []
# 获取快照数据,并保存到文件
# 获取快照数据
snap_data = fetch_snap_all()
if snap_data.empty:
logging.error(f"fetching snapshot data error!")
return
file_name = f'{res_dir}/snapshot_em_{current_date}.csv'
snap_data.to_csv(file_name, index=False, encoding='utf-8')
logging.info(f"市场快照数据已经写入 CSV 文件 {file_name}\n\n")
em_code_map = {row['代码']: row['代码前缀'] for _, row in snap_data.iterrows()}
for item in codes:
code = item['code']
@ -331,9 +360,13 @@ def main(list, debug):
market, clean_code = code.split(".")
except ValueError:
logging.error(f"wrong format code: {code}")
if clean_code not in em_code_map:
logging.warning(f"wrong stock code {clean_code}, please check.")
continue
em_code = f"{em_code_map[clean_code]}.{clean_code}"
logging.info(f"正在处理股票 {market}.{clean_code}, {code_name}...")
result = calculate_stock_statistics(market, clean_code, code_name)
result = calculate_stock_statistics(market, em_code, code_name)
if result:
match = snap_data.loc[snap_data['代码'] == clean_code]
if not match.empty: # 如果找到了匹配项
@ -344,6 +377,9 @@ def main(list, debug):
logging.warning(f'{market}.{clean_code} has no snapshot data.')
all_results.append(result)
logging.info(f"get data succ. {market}.{clean_code}, em_code: {em_code}, name: {code_name}...")
else:
logging.warning(f"get data faild. {market}.{clean_code}, em_code: {em_code}, name: {code_name}")
if debug:
break
@ -367,4 +403,6 @@ if __name__ == "__main__":
# 调用主函数
#flush_code_map()
#print(load_futu_all_codes())
#print(load_xueqiu_codes())
main(args.list, args.debug)