From cbd9b18ef4d0a6b5b37d22a55b1f39358b5bf6bd Mon Sep 17 00:00:00 2001 From: sophon Date: Sat, 19 Jul 2025 17:06:27 +0800 Subject: [PATCH] modify scripts --- src/crawler/zixuan/em_zixuan.py | 154 +++++++ src/crawler/zixuan/qq_zixuan.py | 160 +++++++ src/crawler/zixuan/xueqiu_zixuan.py | 175 +++++++ src/crawling/stock_hist_em.py | 96 +++- src/cursor/his_kline_em_codes.txt | 607 ------------------------- src/cursor/his_kline_em_done_codes.txt | 11 - src/static/akshare_daily_moni.py | 127 ++++++ src/static/akshare_daily_price.py | 361 +++++++++++++++ src/static/stat_growth_em.py | 176 ++++--- 9 files changed, 1171 insertions(+), 696 deletions(-) create mode 100644 src/crawler/zixuan/em_zixuan.py create mode 100644 src/crawler/zixuan/qq_zixuan.py create mode 100644 src/crawler/zixuan/xueqiu_zixuan.py delete mode 100644 src/cursor/his_kline_em_codes.txt delete mode 100644 src/cursor/his_kline_em_done_codes.txt create mode 100644 src/static/akshare_daily_moni.py create mode 100644 src/static/akshare_daily_price.py diff --git a/src/crawler/zixuan/em_zixuan.py b/src/crawler/zixuan/em_zixuan.py new file mode 100644 index 0000000..4508e03 --- /dev/null +++ b/src/crawler/zixuan/em_zixuan.py @@ -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']}") \ No newline at end of file diff --git a/src/crawler/zixuan/qq_zixuan.py b/src/crawler/zixuan/qq_zixuan.py new file mode 100644 index 0000000..e211586 --- /dev/null +++ b/src/crawler/zixuan/qq_zixuan.py @@ -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']}") diff --git a/src/crawler/zixuan/xueqiu_zixuan.py b/src/crawler/zixuan/xueqiu_zixuan.py new file mode 100644 index 0000000..978d3aa --- /dev/null +++ b/src/crawler/zixuan/xueqiu_zixuan.py @@ -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) diff --git a/src/crawling/stock_hist_em.py b/src/crawling/stock_hist_em.py index 325eca7..e3c75b5 100644 --- a/src/crawling/stock_hist_em.py +++ b/src/crawling/stock_hist_em.py @@ -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", diff --git a/src/cursor/his_kline_em_codes.txt b/src/cursor/his_kline_em_codes.txt deleted file mode 100644 index 9d61a12..0000000 --- a/src/cursor/his_kline_em_codes.txt +++ /dev/null @@ -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 \ No newline at end of file diff --git a/src/cursor/his_kline_em_done_codes.txt b/src/cursor/his_kline_em_done_codes.txt deleted file mode 100644 index 5a7dc70..0000000 --- a/src/cursor/his_kline_em_done_codes.txt +++ /dev/null @@ -1,11 +0,0 @@ -689009 -688981 -688517 -000001 -000002 -000001 -688353 -688093 -688303 -000063 -000100 diff --git a/src/static/akshare_daily_moni.py b/src/static/akshare_daily_moni.py new file mode 100644 index 0000000..4d62256 --- /dev/null +++ b/src/static/akshare_daily_moni.py @@ -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() \ No newline at end of file diff --git a/src/static/akshare_daily_price.py b/src/static/akshare_daily_price.py new file mode 100644 index 0000000..405b2d9 --- /dev/null +++ b/src/static/akshare_daily_price.py @@ -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() diff --git a/src/static/stat_growth_em.py b/src/static/stat_growth_em.py index fdf2a7e..8572796 100644 --- a/src/static/stat_growth_em.py +++ b/src/static/stat_growth_em.py @@ -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) \ No newline at end of file