modify scripts
This commit is contained in:
154
src/crawler/zixuan/em_zixuan.py
Normal file
154
src/crawler/zixuan/em_zixuan.py
Normal 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']}")
|
||||||
160
src/crawler/zixuan/qq_zixuan.py
Normal file
160
src/crawler/zixuan/qq_zixuan.py
Normal 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']}")
|
||||||
175
src/crawler/zixuan/xueqiu_zixuan.py
Normal file
175
src/crawler/zixuan/xueqiu_zixuan.py
Normal 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)
|
||||||
@ -7,23 +7,28 @@ Desc: 东方财富网-行情首页-沪深京 A 股
|
|||||||
import requests
|
import requests
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
import time
|
import time
|
||||||
|
import logging
|
||||||
|
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
|
|
||||||
|
|
||||||
def fetch_with_retries_em(url, params, max_retries=3, delay=2):
|
def fetch_with_retries_em(url, params, max_retries=3, delay=2):
|
||||||
"""带重试机制的 GET 请求"""
|
"""带重试机制的 GET 请求"""
|
||||||
|
last_err = None
|
||||||
for attempt in range(max_retries):
|
for attempt in range(max_retries):
|
||||||
try:
|
try:
|
||||||
response = requests.get(url, params=params, timeout=5)
|
response = requests.get(url, params=params, timeout=5)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
return response.json()
|
return response.json()
|
||||||
except requests.RequestException as e:
|
except requests.RequestException as e:
|
||||||
print(f"请求失败,第 {attempt + 1} 次重试: {e}")
|
logging.debug(f"请求失败,第 {attempt + 1} 次重试: {e}")
|
||||||
|
last_err = e
|
||||||
time.sleep(delay)
|
time.sleep(delay)
|
||||||
|
|
||||||
|
logging.error(f"reached max({max_retries}) retries and failed. last error: {last_err}")
|
||||||
return None
|
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 股-实时行情
|
东方财富网-沪深京 A 股-实时行情
|
||||||
https://quote.eastmoney.com/center/gridlist.html#hs_a_board
|
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",
|
"invt": "2",
|
||||||
"fid": "f3",
|
"fid": "f3",
|
||||||
"fs": fs,
|
"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",
|
"_": "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
|
# 获取 total 数据来更新 pn_max
|
||||||
if pn == 1:
|
if pn == 1:
|
||||||
pn_max = (data_json["data"].get("total", 0) + pz - 1) // pz
|
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
|
pn += 1
|
||||||
time.sleep(0.5) # 防止请求过快
|
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": "已流通股份",
|
"f25": "年初至今涨跌幅", "f26": "上市时间", "f37": "加权净资产收益率", "f38": "总股本", "f39": "已流通股份",
|
||||||
"f40": "营业收入", "f41": "营业收入同比增长", "f45": "归属净利润", "f46": "归属净利润同比增长", "f48": "每股未分配利润",
|
"f40": "营业收入", "f41": "营业收入同比增长", "f45": "归属净利润", "f46": "归属净利润同比增长", "f48": "每股未分配利润",
|
||||||
"f49": "毛利率", "f57": "资产负债率", "f61": "每股公积金", "f100": "所处行业", "f112": "每股收益", "f113": "每股净资产",
|
"f49": "毛利率", "f57": "资产负债率", "f61": "每股公积金", "f100": "所处行业", "f112": "每股收益", "f113": "每股净资产",
|
||||||
"f114": "市盈率静", "f115": "市盈率TTM", "f221": "报告期"
|
"f114": "市盈率静", "f115": "市盈率TTM", "f221": "报告期", "f13": "代码前缀"
|
||||||
}
|
}
|
||||||
temp_df.rename(columns=column_map, inplace=True)
|
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日涨跌幅",
|
"最新价", "涨跌幅", "涨跌额", "成交量", "成交额", "振幅", "换手率", "量比", "今开", "最高", "最低", "昨收", "涨速", "5分钟涨跌", "60日涨跌幅",
|
||||||
"年初至今涨跌幅", "市盈率动", "市盈率TTM", "市盈率静", "市净率", "每股收益", "每股净资产", "每股公积金", "每股未分配利润",
|
"年初至今涨跌幅", "市盈率动", "市盈率TTM", "市盈率静", "市净率", "每股收益", "每股净资产", "每股公积金", "每股未分配利润",
|
||||||
"加权净资产收益率", "毛利率", "资产负债率", "营业收入", "营业收入同比增长", "归属净利润", "归属净利润同比增长", "总股本", "已流通股份",
|
"加权净资产收益率", "毛利率", "资产负债率", "营业收入", "营业收入同比增长", "归属净利润", "归属净利润同比增长", "总股本", "已流通股份",
|
||||||
"总市值", "流通市值"
|
"总市值", "流通市值", "代码前缀"
|
||||||
]
|
]
|
||||||
for col in numeric_columns:
|
for col in numeric_columns:
|
||||||
temp_df[col] = pd.to_numeric(temp_df[col], errors="coerce")
|
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)
|
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"]:
|
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
|
break
|
||||||
|
|
||||||
temp_df = pd.DataFrame(data_json["data"]["diff"])
|
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"]:
|
if pn == 1 and "total" in data_json["data"]:
|
||||||
total = int(data_json["data"]["total"])
|
total = int(data_json["data"]["total"])
|
||||||
pn_max = (total // pz) + 1 # 计算最大页数
|
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 进行分组并存入字典
|
# 按 f13 进行分组并存入字典
|
||||||
grouped = temp_df.groupby('f13')
|
grouped = temp_df.groupby('f13')
|
||||||
@ -452,7 +457,7 @@ def code_id_map_em() -> dict:
|
|||||||
|
|
||||||
pn += 1 # 翻页继续
|
pn += 1 # 翻页继续
|
||||||
|
|
||||||
print(f'获取 {market_id} 已获取总股票数: {fetched_cnt}, 总股票数: {total}')
|
logging.info(f'获取 {market_id} 已获取总股票数: {fetched_cnt}, 总股票数: {total}')
|
||||||
|
|
||||||
return code_id_dict
|
return code_id_dict
|
||||||
|
|
||||||
@ -501,6 +506,79 @@ def code_id_map_em2() -> dict:
|
|||||||
|
|
||||||
return code_id_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(
|
def stock_zh_a_hist(
|
||||||
symbol: str = "000001",
|
symbol: str = "000001",
|
||||||
period: str = "daily",
|
period: str = "daily",
|
||||||
|
|||||||
@ -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
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
689009
|
|
||||||
688981
|
|
||||||
688517
|
|
||||||
000001
|
|
||||||
000002
|
|
||||||
000001
|
|
||||||
688353
|
|
||||||
688093
|
|
||||||
688303
|
|
||||||
000063
|
|
||||||
000100
|
|
||||||
127
src/static/akshare_daily_moni.py
Normal file
127
src/static/akshare_daily_moni.py
Normal 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()
|
||||||
361
src/static/akshare_daily_price.py
Normal file
361
src/static/akshare_daily_price.py
Normal 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()
|
||||||
@ -18,6 +18,7 @@ import pymysql
|
|||||||
import logging
|
import logging
|
||||||
import csv
|
import csv
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import time
|
import time
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from futu import OpenQuoteContext, RET_OK # Futu API client
|
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.crawling.stock_hist_em as his_em
|
||||||
import src.logger.logger as logger
|
import src.logger.logger as logger
|
||||||
import src.config.config as config
|
import src.config.config as config
|
||||||
|
from src.crawler.zixuan.xueqiu_zixuan import XueQiuStockFetcher
|
||||||
|
|
||||||
# 配置日志
|
# 配置日志
|
||||||
logger.setup_logging()
|
logger.setup_logging()
|
||||||
@ -41,58 +43,36 @@ def flush_code_map():
|
|||||||
print(code_id_map_em_df)
|
print(code_id_map_em_df)
|
||||||
return 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:
|
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",
|
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",
|
"hk": "m:128 t:3,m:128 t:4,m:128 t:1,m:128 t:2",
|
||||||
"us": "m:105,m:106,m:107"}
|
"us": "m:105,m:106,m:107"}
|
||||||
|
|
||||||
result = pd.DataFrame()
|
result = pd.DataFrame()
|
||||||
for market_id, fs in market_fs.items():
|
for market_id, fs in market_fs.items():
|
||||||
retries = 0
|
df = his_em.stock_zh_a_spot_em(fs, fs_desc=market_id)
|
||||||
while retries < max_retries:
|
if df.empty:
|
||||||
try:
|
logging.warning(f'{market_id} empty data. please check.')
|
||||||
df = his_em.stock_zh_a_spot_em(fs)
|
return pd.DataFrame()
|
||||||
# 如果获取到的数据为空,记录日志并重试
|
else:
|
||||||
if df.empty:
|
logging.info(f'get {market_id} stock snapshot. stock count: {len(df)}')
|
||||||
logging.warning(f'{market_id} empty data. retry...')
|
result = pd.concat([result, df], ignore_index=True)
|
||||||
retries += 1
|
|
||||||
time.sleep(3) # 每次重试前休眠 3 秒
|
result.to_csv(file_name, index=False, encoding='utf-8')
|
||||||
else:
|
logging.info(f"get snapshot data and write to file: {file_name}\n\n")
|
||||||
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.')
|
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@ -127,6 +107,55 @@ def load_index_codes():
|
|||||||
conn.close()
|
conn.close()
|
||||||
return hs300_data + hk_data + us_data
|
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():
|
def load_futu_all_codes():
|
||||||
quote_ctx = OpenQuoteContext(host='127.0.0.1', port=11111)
|
quote_ctx = OpenQuoteContext(host='127.0.0.1', port=11111)
|
||||||
@ -143,14 +172,6 @@ def load_futu_all_codes():
|
|||||||
|
|
||||||
return stock_data
|
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):
|
def calculate_stock_statistics(market, code, code_name):
|
||||||
try:
|
try:
|
||||||
@ -158,10 +179,16 @@ def calculate_stock_statistics(market, code, code_name):
|
|||||||
last_year = datetime.now().year - 1
|
last_year = datetime.now().year - 1
|
||||||
last_year_str = str(last_year)
|
last_year_str = str(last_year)
|
||||||
|
|
||||||
# 获取历史数据
|
# 调用 stock_zh_a_hist 获取历史数据
|
||||||
data = fetch_with_retry(code, "20210101", current_date, 'qfq')
|
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:
|
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
|
return None
|
||||||
|
|
||||||
# 获取当前日期的股价
|
# 获取当前日期的股价
|
||||||
@ -173,7 +200,7 @@ def calculate_stock_statistics(market, code, code_name):
|
|||||||
# 获取年初股价,也就是上一年的最后一个交易日的收盘价
|
# 获取年初股价,也就是上一年的最后一个交易日的收盘价
|
||||||
year_data = data[data['日期'].str.startswith(last_year_str)]
|
year_data = data[data['日期'].str.startswith(last_year_str)]
|
||||||
if year_data.empty:
|
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
|
year_begin_row = defaut_row
|
||||||
else:
|
else:
|
||||||
year_begin_row = year_data.loc[year_data['日期'].idxmax()]
|
year_begin_row = year_data.loc[year_data['日期'].idxmax()]
|
||||||
@ -182,35 +209,35 @@ def calculate_stock_statistics(market, code, code_name):
|
|||||||
try:
|
try:
|
||||||
row_0923 = data[data['日期'] == '2024-09-23'].iloc[0]
|
row_0923 = data[data['日期'] == '2024-09-23'].iloc[0]
|
||||||
except IndexError:
|
except IndexError:
|
||||||
logging.warning(f"{code}, {code_name} 未找到0923的数据, 以 {defaut_row['日期']} 的数据来代替")
|
logging.debug(f"{code}, {code_name} 未找到0923的数据, 以 {defaut_row['日期']} 的数据来代替")
|
||||||
row_0923 = defaut_row
|
row_0923 = defaut_row
|
||||||
|
|
||||||
# 获取0930收盘价
|
# 获取0930收盘价
|
||||||
try:
|
try:
|
||||||
row_0930 = data[data['日期'] == '2024-09-30'].iloc[0]
|
row_0930 = data[data['日期'] == '2024-09-30'].iloc[0]
|
||||||
except IndexError:
|
except IndexError:
|
||||||
logging.warning(f"{code}, {code_name} 未找到0930的数据, 以 {defaut_row['日期']} 的数据来代替")
|
logging.debug(f"{code}, {code_name} 未找到0930的数据, 以 {defaut_row['日期']} 的数据来代替")
|
||||||
row_0930 = defaut_row
|
row_0930 = defaut_row
|
||||||
|
|
||||||
# 获取1008开盘价、收盘价
|
# 获取1008开盘价、收盘价
|
||||||
try:
|
try:
|
||||||
row_1008 = data[data['日期'] == '2024-10-08'].iloc[0]
|
row_1008 = data[data['日期'] == '2024-10-08'].iloc[0]
|
||||||
except IndexError:
|
except IndexError:
|
||||||
logging.warning(f"{code}, {code_name} 未找到1008的数据, 以 {defaut_row['日期']} 的数据来代替")
|
logging.debug(f"{code}, {code_name} 未找到1008的数据, 以 {defaut_row['日期']} 的数据来代替")
|
||||||
row_1008 = defaut_row
|
row_1008 = defaut_row
|
||||||
|
|
||||||
# 获取0403收盘价
|
# 获取0403收盘价
|
||||||
try:
|
try:
|
||||||
row_0403 = data[data['日期'] == '2025-04-03'].iloc[0]
|
row_0403 = data[data['日期'] == '2025-04-03'].iloc[0]
|
||||||
except IndexError:
|
except IndexError:
|
||||||
logging.warning(f"{code}, {code_name} 未找到0403的数据, 以 {defaut_row['日期']} 的数据来代替")
|
logging.debug(f"{code}, {code_name} 未找到0403的数据, 以 {defaut_row['日期']} 的数据来代替")
|
||||||
row_0403 = defaut_row
|
row_0403 = defaut_row
|
||||||
|
|
||||||
# 获取0407收盘价
|
# 获取0407收盘价
|
||||||
try:
|
try:
|
||||||
row_0407 = data[data['日期'] == '2025-04-07'].iloc[0]
|
row_0407 = data[data['日期'] == '2025-04-07'].iloc[0]
|
||||||
except IndexError:
|
except IndexError:
|
||||||
logging.warning(f"{code}, {code_name} 未找到0407的数据, 以 {defaut_row['日期']} 的数据来代替")
|
logging.debug(f"{code}, {code_name} 未找到0407的数据, 以 {defaut_row['日期']} 的数据来代替")
|
||||||
row_0407 = defaut_row
|
row_0407 = defaut_row
|
||||||
|
|
||||||
# 获取2021年以来的最高价
|
# 获取2021年以来的最高价
|
||||||
@ -221,7 +248,7 @@ def calculate_stock_statistics(market, code, code_name):
|
|||||||
# 获取年内的最高价、最低价
|
# 获取年内的最高价、最低价
|
||||||
year_data = data[data['日期'].str.startswith(current_year)]
|
year_data = data[data['日期'].str.startswith(current_year)]
|
||||||
if year_data.empty:
|
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_min_row = defaut_row
|
||||||
year_max_row = defaut_row
|
year_max_row = defaut_row
|
||||||
else:
|
else:
|
||||||
@ -300,27 +327,29 @@ def write_to_csv(results, filename):
|
|||||||
# 主函数,执行逻辑
|
# 主函数,执行逻辑
|
||||||
def main(list, debug):
|
def main(list, debug):
|
||||||
futu_codes = []
|
futu_codes = []
|
||||||
|
xueqiu_codes = []
|
||||||
index_codes = []
|
index_codes = []
|
||||||
|
|
||||||
if list == 'futu':
|
if list == 'futu':
|
||||||
futu_codes = load_futu_all_codes()
|
futu_codes = load_futu_all_codes()
|
||||||
|
elif list == 'xueqiu':
|
||||||
|
xueqiu_codes = load_xueqiu_codes()
|
||||||
elif list == 'all':
|
elif list == 'all':
|
||||||
futu_codes = load_futu_all_codes()
|
futu_codes = load_futu_all_codes()
|
||||||
|
xueqiu_codes = load_xueqiu_codes()
|
||||||
index_codes = load_index_codes()
|
index_codes = load_index_codes()
|
||||||
else:
|
else:
|
||||||
index_codes = load_index_codes()
|
index_codes = load_index_codes()
|
||||||
codes = futu_codes + index_codes
|
codes = futu_codes + index_codes + xueqiu_codes
|
||||||
|
|
||||||
all_results = []
|
all_results = []
|
||||||
|
|
||||||
# 获取快照数据,并保存到文件
|
# 获取快照数据
|
||||||
snap_data = fetch_snap_all()
|
snap_data = fetch_snap_all()
|
||||||
if snap_data.empty:
|
if snap_data.empty:
|
||||||
logging.error(f"fetching snapshot data error!")
|
logging.error(f"fetching snapshot data error!")
|
||||||
return
|
return
|
||||||
file_name = f'{res_dir}/snapshot_em_{current_date}.csv'
|
em_code_map = {row['代码']: row['代码前缀'] for _, row in snap_data.iterrows()}
|
||||||
snap_data.to_csv(file_name, index=False, encoding='utf-8')
|
|
||||||
logging.info(f"市场快照数据已经写入 CSV 文件 {file_name}\n\n")
|
|
||||||
|
|
||||||
for item in codes:
|
for item in codes:
|
||||||
code = item['code']
|
code = item['code']
|
||||||
@ -331,9 +360,13 @@ def main(list, debug):
|
|||||||
market, clean_code = code.split(".")
|
market, clean_code = code.split(".")
|
||||||
except ValueError:
|
except ValueError:
|
||||||
logging.error(f"wrong format code: {code}")
|
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, em_code, code_name)
|
||||||
result = calculate_stock_statistics(market, clean_code, code_name)
|
|
||||||
if result:
|
if result:
|
||||||
match = snap_data.loc[snap_data['代码'] == clean_code]
|
match = snap_data.loc[snap_data['代码'] == clean_code]
|
||||||
if not match.empty: # 如果找到了匹配项
|
if not match.empty: # 如果找到了匹配项
|
||||||
@ -344,6 +377,9 @@ def main(list, debug):
|
|||||||
logging.warning(f'{market}.{clean_code} has no snapshot data.')
|
logging.warning(f'{market}.{clean_code} has no snapshot data.')
|
||||||
|
|
||||||
all_results.append(result)
|
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:
|
if debug:
|
||||||
break
|
break
|
||||||
@ -367,4 +403,6 @@ if __name__ == "__main__":
|
|||||||
|
|
||||||
# 调用主函数
|
# 调用主函数
|
||||||
#flush_code_map()
|
#flush_code_map()
|
||||||
|
#print(load_futu_all_codes())
|
||||||
|
#print(load_xueqiu_codes())
|
||||||
main(args.list, args.debug)
|
main(args.list, args.debug)
|
||||||
Reference in New Issue
Block a user