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 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",
|
||||
|
||||
@ -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 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)
|
||||
Reference in New Issue
Block a user