diff --git a/TurtleOnTime.py b/TurtleOnTime.py index 365e9a6..bbd46c8 100644 --- a/TurtleOnTime.py +++ b/TurtleOnTime.py @@ -12,12 +12,20 @@ import time import threading import yaml # 添加YAML支持 import logging +from stock_em import stock_zh_a_spot_em +from etf_em import fund_etf_spot_em +import random +import urllib.parse +import requests ''' todo -1 运行过程框架调整,支持多个turtle同时监测 -2 增加运行状态写入yaml文件,读取文件恢复状态 +1 运行过程框架调整,支持多个turtle同时监测 done +2 增加运行状态写入yaml文件,读取文件恢复状态 done +3 测试每分钟获取实时信息,是否稳定。 done +4 etf实时数据使用异步方式,获取速度加快了 done +4 获取数据,调用clash代理 done ''' @dataclass @@ -346,6 +354,34 @@ class TurtleTrading_OnTime(object): 3、实时监测主流程 ''' + def __init__(self, turtles: list[TurtleTrading], user_email): + self.turtles = turtles # List of TurtleTrading instances + self.user_email = user_email + self.email_events = {} # Track email response events for each turtle + logging.basicConfig(level=logging.INFO) + # Load previous state from YAML if exists + self.load_previous_state() + + self.clash_api = "http://127.0.0.1:9090" + self.node_group_name_encoded = urllib.parse.quote("🚀代理线路", safe='') # 根据 Clash 中策略组的名字填写 + self.clash_secret = "6Gp-fdt-veS-ugv" + self.node_names = [ + "R1-0|香港-NF|深|负载均衡", + "R1-1|香港-NF|粤|负载均衡", + "R1-2|香港-NF|沪|负载均衡", + "R1-3|香港-NF|湘|负载均衡", + "R2-1|香港-NF|HKT家宽", + "R2-2|香港-NF|HKT家宽", + "R2-3|香港-NF|HKT家宽", + "R2-5|香港-NF|HKT家宽", + "R3-1|香港-NF|BGP静态", + "R4-1|台湾-NF|家宽|原生", + "R5-1|日本-NF|GMO|原生IP", + "R5-4|日本-NF|IIJ|精品", + "R6-1|美国-NF|BGP", + "R9-1|狮城-NF|FDC", + ] + def load_previous_state(self): """Load previous state from YAML file if exists""" state_dir = "state" @@ -356,40 +392,48 @@ class TurtleTrading_OnTime(object): if os.path.exists(filename): with open(filename, 'r') as f: state_data = yaml.safe_load(f) + main_state = state_data.get('main_state', {}) # Restore state - for turtle_data in state_data.get('turtles', []): - # Find or create TurtleTrading instance - turtle = next((t for t in self.turtles if t.TradeCode == turtle_data['TradeCode']), None) - if not turtle: - # Create new instance if not found (should not happen) - turtle = TurtleTrading(**turtle_data) - self.turtles.append(turtle) - - # Restore attributes - turtle.TradeCode = turtle_data['TradeCode'] - turtle.type = turtle_data['type'] - turtle.riskcoe = turtle_data['riskcoe'] - turtle.Capital = turtle_data['Capital'] - turtle.cash = turtle_data['cash'] - turtle.TrigerTime = turtle_data['TrigerTime'] - turtle.BuyStates = [BuyState(**bs) for bs in turtle_data['BuyStates']] - turtle.tradeslog = [TradeLog(**tl) for tl in turtle_data['tradeslog']] - turtle.BreakOutLog = [BreakOutLog(**bol) for bol in turtle_data['BreakOutLog']] - turtle.PriceNow = turtle_data['PriceNow'] - turtle.Donchian_20_up = turtle_data['Donchian_20_up'] - turtle.Donchian_10_down = turtle_data['Donchian_10_down'] - turtle.Donchian_50_up = turtle_data['Donchian_50_up'] - turtle.is_gap_up = turtle_data['is_gap_up'] - turtle.prev_heigh = turtle_data['prev_heigh'] + try: + for turtle_data in main_state.get('turtles', []): + # Find or create TurtleTrading instance + turtle = next((t for t in self.turtles if t.TradeCode == turtle_data['TradeCode']), None) + if not turtle: + # Create new instance if not found (should not happen) + turtle = TurtleTrading(**turtle_data) + self.turtles.append(turtle) + + # Restore attributes + turtle.TradeCode = turtle_data['TradeCode'] + turtle.type = turtle_data['type'] + turtle.riskcoe = turtle_data['riskcoe'] + turtle.Capital = turtle_data['Capital'] + turtle.cash = turtle_data['cash'] + turtle.TrigerTime = turtle_data['TrigerTime'] + turtle.BuyStates = [BuyState(**bs) for bs in turtle_data['BuyStates']] + turtle.tradeslog = [TradeLog(**tl) for tl in turtle_data['tradeslog']] + turtle.BreakOutLog = [BreakOutLog(**bol) for bol in turtle_data['BreakOutLog']] + + except Exception as e: + logging.error(f"Error loading previous state: {e}") + def switch_random_node(self): + '''随机切换clash节点 + ''' + selected_node = random.choice(self.node_names) + url = f"{self.clash_api}/proxies/{self.node_group_name_encoded}" + headers = { + "Authorization": f"Bearer {self.clash_secret}" + } + try: + res = requests.put(url, headers=headers, json={"name": selected_node}) + if res.status_code == 204: + print(f"[✓] 成功切换节点为:{selected_node}") + else: + print(f"[✗] 切换失败:{res.status_code} - {res.text}") + except Exception as e: + print(f"[!] 请求失败: {e}") - def __init__(self, turtles: list[TurtleTrading], user_email): - self.turtles = turtles # List of TurtleTrading instances - self.user_email = user_email - self.email_events = {} # Track email response events for each turtle - logging.basicConfig(level=logging.INFO) - # Load previous state from YAML if exists - self.load_previous_state() def day_end(self): """Save current state to YAML file at the end of the day""" @@ -429,20 +473,21 @@ class TurtleTrading_OnTime(object): def get_stocks_data(self): """获取实时股票、基金数据,不保存 """ - stock_data = ak.stock_zh_a_spot_em() - stock_data = stock_data.dropna(subset=['最新价']) - # # print(stock_zh_a_spot_df) - # # stock_zh_a_spot_df第一列加上时间,精确到分钟 - # stock_zh_a_spot_df['时间'] = datetime.now().strftime("%Y-%m-%d %H:%M:%S") - # mysql_database.insert_db(stock_zh_a_spot_df, "stock_price", True, "代码") + try: + self.switch_random_node() + stock_data = stock_zh_a_spot_em() + stock_data = stock_data.dropna(subset=['最新价']) + + etf_data = fund_etf_spot_em() + etf_data = etf_data.dropna(subset=['最新价']) + + # 成功调用,使用logging记录日志,加上时间 + logging.info(f"Successfully fetched stock and ETF data at {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") + + except Exception as e: + logging.error(f"Error occurred while getting stock data: {e}") + return None, None - etf_data = ak.fund_etf_spot_em() - # etf_data = ak.fund_etf_spot_ths() - # etf_data = etf_data.dropna(subset=['当前-单位净值']) - etf_data = etf_data.dropna(subset=['最新价']) - - # etf_data['时间'] = datetime.now().strftime("%Y-%m-%d %H:%M:%S") - # mysql_database.insert_db(etf_data, "etf_price", True, "代码") return stock_data, etf_data def Buy_stock(self, turtle: TurtleTrading, price_now): @@ -704,88 +749,6 @@ class TurtleTrading_OnTime(object): Net_return=abs(turtle.Capital - available_cash)) turtle.tradeslog.append(sale_this_time) - # def run_short_trading_loop(self, stock_data, etf_data): - - # now = datetime.now().time() - # # 根据类型获取当前价格 - # if self.turtle.type == "stock": - # self.turtle.PriceNow = float(stock_data.loc[etf_data['代码'] == self.turtle.TradeCode, '最新价'].values[0]) - - # elif self.turtle.type == "etf": - # # self.turtle.PriceNow = float(etf_data.loc[etf_data['基金代码'] == self.turtle.TradeCode, '当前-单位净值'].values[0]) - # self.turtle.PriceNow = float(etf_data.loc[etf_data['代码'] == self.turtle.TradeCode, '最新价'].values[0]) - - # # # 9点30 判断是否跳空高开 - # if now.hour == 9 and now.minute == 30 and self.turtle.PriceNow > self.turtle.prev_heigh: - # self.turtle.is_gap_up = True - - - # # 判断当前仓位状态并执行相应操作 - # if self.turtle.TrigerTime == 0: - # # 空仓状态 - # if self.turtle.system1EnterNormal( - # self.turtle.PriceNow, - # self.turtle.Donchian_20_up, - # self.turtle.BreakOutLog - # ): - # self.Buy_stock(self.turtle.PriceNow) - - # # 突破 记录self.turtle.breakoutlog - # today = datetime.now().strftime("%Y-%m-%d") - # breakout_this_time = BreakOutLog(today, - # self.turtle.Donchian_20_up, - # self.turtle.Donchian_20_up - 2 * self.turtle.N, - # 'valid', - # None) - # self.turtle.BreakOutLog.append(breakout_this_time) - - # elif self.turtle.system1EnterSafe( - # self.turtle.PriceNow, - # self.turtle.Donchian_50_up - # ): - # self.Buy_stock(self.turtle.PriceNow) - - # elif 1 <= self.turtle.TrigerTime <= 3: - # # # 突破状态 - # # if self.turtle.system1EnterNormal( - # # self.turtle.PriceNow, - # # self.turtle.Donchian_20_up, - # # self.turtle.BreakOutLog - # # ): - # # self.Buy_stock(self.turtle.PriceNow) - # # elif self.turtle.system1EnterSafe( - # # self.turtle.PriceNow, - # # self.turtle.Donchian_50_up - # # ): - # # self.Buy_stock(self.turtle.PriceNow) - - # # 加仓状态 - # if self.turtle.add(self.turtle.PriceNow): - # self.add_stock(self.turtle.PriceNow) - - # # 止损状态 - # elif self.turtle.system_1_stop(self.turtle.PriceNow): - # self.stop_sale_stock(self.turtle.PriceNow) - - # # 止盈 - # elif self.turtle.system_1_Out( - # self.turtle.PriceNow, - # self.turtle.Donchian_10_down - # ): - # self.out_sale_stock(self.turtle.PriceNow) - - # elif self.turtle.TrigerTime == 4: - # # 满仓 止损 止盈 - # if self.turtle.system_1_stop(self.turtle.PriceNow): - # self.stop_sale_stock(self.turtle.PriceNow) - # elif self.turtle.system_1_Out( - # self.turtle.PriceNow, - # self.turtle.Donchian_10_down - # ): - # self.out_sale_stock(self.turtle.PriceNow) - - # # 等待 1 分钟后下一次循环 - # time.sleep(60) def Start_short_system(self): """启动short系统 @@ -824,7 +787,7 @@ class TurtleTrading_OnTime(object): (now.hour == 14 and 0 <= now.minute <= 59) or (now.hour == 15 and now.minute <= 0) ) - is_trading_time = True + # is_trading_time = True if not is_trading_time: @@ -832,14 +795,15 @@ class TurtleTrading_OnTime(object): time.sleep(60) continue - is_stop_time = (now.hour > 15 and now.minute > 0) #收盘时间 + is_stop_time = (now.hour >= 15 and now.minute > 0) #收盘时间 + # is_stop_time = (now.hour >= 18 and now.minute > 32) #收盘时间 if is_stop_time: break # 获取股票和ETF数据 self.monitor_all_turtles() # 等待一段时间后再次检查 - time.sleep(60) # 每分钟检查一次 + time.sleep(random.randint(60, 65))# 每分钟检查一次 # ------------------结束阶段-------------------- # 数据库更新当天数据,增加ATR、donchian数据 # 直接做个新表 @@ -886,8 +850,8 @@ class TurtleTrading_OnTime(object): if now.hour == 9 and now.minute == 30 and turtle.PriceNow > turtle.prev_heigh: turtle.is_gap_up = True - # fake_price = 1.470 - # turtle.PriceNow = fake_price + fake_price = 1.492 + turtle.PriceNow = fake_price # 判断当前仓位状态并执行相应操作 if turtle.TrigerTime == 0: if turtle.system1EnterNormal( diff --git a/__pycache__/etf_em.cpython-310.pyc b/__pycache__/etf_em.cpython-310.pyc new file mode 100644 index 0000000..85935fb Binary files /dev/null and b/__pycache__/etf_em.cpython-310.pyc differ diff --git a/__pycache__/stock_em.cpython-310.pyc b/__pycache__/stock_em.cpython-310.pyc new file mode 100644 index 0000000..97c0365 Binary files /dev/null and b/__pycache__/stock_em.cpython-310.pyc differ diff --git a/etf_em.py b/etf_em.py new file mode 100644 index 0000000..c099e5c --- /dev/null +++ b/etf_em.py @@ -0,0 +1,231 @@ +import asyncio +import httpx +import math +import pandas as pd +from datetime import datetime, timezone, timedelta +import logging + +logging.getLogger("httpx").setLevel(logging.WARNING) + +HEADERS = { + "User-Agent": ( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " + "AppleWebKit/537.36 (KHTML, like Gecko) " + "Chrome/122.0.0.0 Safari/537.36" + ), + "Referer": "https://quote.eastmoney.com/center/gridlist.html#fund_etf", # 合理来源页 + "Accept": "application/json, text/plain, */*", + "Accept-Language": "zh-CN,zh;q=0.9", + "Connection": "keep-alive", +} + + +async def fetch_page(client: httpx.AsyncClient, url: str, params: dict) -> pd.DataFrame: + try: + resp = await client.get(url, params=params, timeout=10) + data_json = resp.json() + if "data" in data_json and data_json["data"].get("diff"): + return pd.DataFrame(data_json["data"]["diff"]) + except Exception as e: + print(f"Error on page {params.get('pn')}: {e}") + return pd.DataFrame() + + +async def fund_etf_spot_em_async() -> pd.DataFrame: + url = "https://88.push2.eastmoney.com/api/qt/clist/get" + params = { + "pn": "1", + "pz": "100", + "po": "1", + "np": "1", + "ut": "bd1d9ddb04089700cf9c27f6f7426281", + "fltt": "2", + "invt": "2", + "wbp2u": "|0|0|0|web", + "fid": "f12", + "fs": "b:MK0021,b:MK0022,b:MK0023,b:MK0024,b:MK0827", + "fields": ( + "f1,f2,f3,f4,f5,f6,f7,f8,f9,f10," + "f12,f13,f14,f15,f16,f17,f18,f20,f21," + "f23,f24,f25,f22,f11,f30,f31,f32,f33," + "f34,f35,f38,f62,f63,f64,f65,f66,f69," + "f72,f75,f78,f81,f84,f87,f115,f124,f128," + "f136,f152,f184,f297,f402,f441" + ), + } + + async with httpx.AsyncClient(headers=HEADERS, http2=True) as client: + # 获取首页数据,确认总页数 + resp = await client.get(url, params=params) + data_json = resp.json() + if "data" not in data_json or not data_json["data"].get("diff"): + return pd.DataFrame() + + total = data_json["data"]["total"] + per_page = len(data_json["data"]["diff"]) + total_pages = math.ceil(total / per_page) + + tasks = [] + for page in range(1, total_pages + 1): + new_params = params.copy() + new_params["pn"] = str(page) + tasks.append(fetch_page(client, url, new_params)) + + dfs = await asyncio.gather(*tasks) + temp_df = pd.concat([df for df in dfs if not df.empty], ignore_index=True) + + temp_df.rename( + columns={ + "f12": "代码", + "f14": "名称", + "f2": "最新价", + "f4": "涨跌额", + "f3": "涨跌幅", + "f5": "成交量", + "f6": "成交额", + "f7": "振幅", + "f17": "开盘价", + "f15": "最高价", + "f16": "最低价", + "f18": "昨收", + "f8": "换手率", + "f10": "量比", + "f30": "现手", + "f31": "买一", + "f32": "卖一", + "f33": "委比", + "f34": "外盘", + "f35": "内盘", + "f62": "主力净流入-净额", + "f184": "主力净流入-净占比", + "f66": "超大单净流入-净额", + "f69": "超大单净流入-净占比", + "f72": "大单净流入-净额", + "f75": "大单净流入-净占比", + "f78": "中单净流入-净额", + "f81": "中单净流入-净占比", + "f84": "小单净流入-净额", + "f87": "小单净流入-净占比", + "f38": "最新份额", + "f21": "流通市值", + "f20": "总市值", + "f402": "基金折价率", + "f441": "IOPV实时估值", + "f297": "数据日期", + "f124": "更新时间", + }, + inplace=True, + ) + temp_df = temp_df[ + [ + "代码", + "名称", + "最新价", + "IOPV实时估值", + "基金折价率", + "涨跌额", + "涨跌幅", + "成交量", + "成交额", + "开盘价", + "最高价", + "最低价", + "昨收", + "振幅", + "换手率", + "量比", + "委比", + "外盘", + "内盘", + "主力净流入-净额", + "主力净流入-净占比", + "超大单净流入-净额", + "超大单净流入-净占比", + "大单净流入-净额", + "大单净流入-净占比", + "中单净流入-净额", + "中单净流入-净占比", + "小单净流入-净额", + "小单净流入-净占比", + "现手", + "买一", + "卖一", + "最新份额", + "流通市值", + "总市值", + "数据日期", + "更新时间", + ] + ].copy() + temp_df["最新价"] = pd.to_numeric(temp_df["最新价"], errors="coerce") + temp_df["涨跌额"] = pd.to_numeric(temp_df["涨跌额"], errors="coerce") + temp_df["涨跌幅"] = pd.to_numeric(temp_df["涨跌幅"], errors="coerce") + temp_df["成交量"] = pd.to_numeric(temp_df["成交量"], errors="coerce") + temp_df["成交额"] = pd.to_numeric(temp_df["成交额"], errors="coerce") + temp_df["开盘价"] = pd.to_numeric(temp_df["开盘价"], errors="coerce") + temp_df["最高价"] = pd.to_numeric(temp_df["最高价"], errors="coerce") + temp_df["最低价"] = pd.to_numeric(temp_df["最低价"], errors="coerce") + temp_df["昨收"] = pd.to_numeric(temp_df["昨收"], errors="coerce") + temp_df["换手率"] = pd.to_numeric(temp_df["换手率"], errors="coerce") + temp_df["量比"] = pd.to_numeric(temp_df["量比"], errors="coerce") + temp_df["委比"] = pd.to_numeric(temp_df["委比"], errors="coerce") + temp_df["外盘"] = pd.to_numeric(temp_df["外盘"], errors="coerce") + temp_df["内盘"] = pd.to_numeric(temp_df["内盘"], errors="coerce") + temp_df["流通市值"] = pd.to_numeric(temp_df["流通市值"], errors="coerce") + temp_df["总市值"] = pd.to_numeric(temp_df["总市值"], errors="coerce") + temp_df["振幅"] = pd.to_numeric(temp_df["振幅"], errors="coerce") + temp_df["现手"] = pd.to_numeric(temp_df["现手"], errors="coerce") + temp_df["买一"] = pd.to_numeric(temp_df["买一"], errors="coerce") + temp_df["卖一"] = pd.to_numeric(temp_df["卖一"], errors="coerce") + temp_df["最新份额"] = pd.to_numeric(temp_df["最新份额"], errors="coerce") + temp_df["IOPV实时估值"] = pd.to_numeric(temp_df["IOPV实时估值"], errors="coerce") + temp_df["基金折价率"] = pd.to_numeric(temp_df["基金折价率"], errors="coerce") + temp_df["主力净流入-净额"] = pd.to_numeric( + temp_df["主力净流入-净额"], errors="coerce" + ) + temp_df["主力净流入-净占比"] = pd.to_numeric( + temp_df["主力净流入-净占比"], errors="coerce" + ) + temp_df["超大单净流入-净额"] = pd.to_numeric( + temp_df["超大单净流入-净额"], errors="coerce" + ) + temp_df["超大单净流入-净占比"] = pd.to_numeric( + temp_df["超大单净流入-净占比"], errors="coerce" + ) + temp_df["大单净流入-净额"] = pd.to_numeric( + temp_df["大单净流入-净额"], errors="coerce" + ) + temp_df["大单净流入-净占比"] = pd.to_numeric( + temp_df["大单净流入-净占比"], errors="coerce" + ) + temp_df["中单净流入-净额"] = pd.to_numeric( + temp_df["中单净流入-净额"], errors="coerce" + ) + temp_df["中单净流入-净占比"] = pd.to_numeric( + temp_df["中单净流入-净占比"], errors="coerce" + ) + temp_df["小单净流入-净额"] = pd.to_numeric( + temp_df["小单净流入-净额"], errors="coerce" + ) + temp_df["小单净流入-净占比"] = pd.to_numeric( + temp_df["小单净流入-净占比"], errors="coerce" + ) + temp_df["数据日期"] = pd.to_datetime( + temp_df["数据日期"], format="%Y%m%d", errors="coerce" + ) + temp_df["更新时间"] = ( + pd.to_datetime(temp_df["更新时间"], unit="s", errors="coerce") + .dt.tz_localize("UTC") + .dt.tz_convert("Asia/Shanghai") + ) + return temp_df + + +# 示例同步调用封装(如需在同步代码中使用) +def fund_etf_spot_em() -> pd.DataFrame: + return asyncio.run(fund_etf_spot_em_async()) + + +if __name__ == "__main__": + df = fund_etf_spot_em() + print(df.head()) diff --git a/state/2025-05-30.yaml b/state/2025-05-30.yaml new file mode 100644 index 0000000..5254b58 --- /dev/null +++ b/state/2025-05-30.yaml @@ -0,0 +1,46 @@ +main_state: + email_events: {} + turtles: + - BreakOutLog: + - breakout_price: 1.491 + data: '2025-05-31' + lose_price: 1.459 + valid_or_not: valid + win_or_lose: null + BuyStates: + - add_price: 1.5 + atr: 0.016 + available_cash: 76724.9 + buy_price: 1.492 + is_gap_up: false + shares: 15600.0 + stop_price: 1.46 + trigger_time: 1 + Capital: 100000 + TradeCode: '513870' + TrigerTime: 1 + cash: 200000 + riskcoe: 0.0025 + tradeslog: + - Net_return: 0 + Net_value: 23275.2 + all_cost: 23275.100000000002 + all_shares: 15600.0 + atr: 0.016 + available_cash: 76724.9 + buy_price: 1.492 + cost: 23275.100000000002 + data: '2025-05-31' + shares: 15600.0 + type: "\u4E70\u5165" + type: etf + - BreakOutLog: [] + BuyStates: [] + Capital: 100000 + TradeCode: '600900' + TrigerTime: 0 + cash: 200000 + riskcoe: 0.0025 + tradeslog: [] + type: stock + user_email: guoyize2209@163.com diff --git a/stock_em.py b/stock_em.py new file mode 100644 index 0000000..3620d5b --- /dev/null +++ b/stock_em.py @@ -0,0 +1,165 @@ +import asyncio +from typing import Dict, List + +import aiohttp +import pandas as pd + + +# 增加了 User-Agent 头 +HEADERS = { + "User-Agent": ( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " + "AppleWebKit/537.36 (KHTML, like Gecko) " + "Chrome/122.0.0.0 Safari/537.36" + ), + "Referer": "https://quote.eastmoney.com/center/gridlist.html#hs_a_board", # 合理来源页 + "Accept": "application/json, text/plain, */*", + "Accept-Language": "zh-CN,zh;q=0.9", + "Connection": "keep-alive", +} + + +async def fetch_single_page( + session: aiohttp.ClientSession, url: str, params: Dict +) -> Dict: + """异步获取单页数据""" + async with session.get(url, params=params, headers=HEADERS, ssl=False) as response: + return await response.json() + + +async def fetch_all_pages_async(url: str, base_params: Dict) -> List[Dict]: + """异步获取所有页面数据""" + first_page_params = base_params.copy() + first_page_params["pn"] = "1" + + async with aiohttp.ClientSession() as session: + first_page_data = await fetch_single_page(session, url, first_page_params) + + if first_page_data.get("rc") != 0 or not first_page_data.get("data"): + return [first_page_data] + + total = first_page_data["data"]["total"] + page_size = int(base_params["pz"]) + total_pages = (total + page_size - 1) // page_size + total_pages = min(total_pages, 100) + + tasks = [] + for page in range(1, total_pages + 1): + page_params = base_params.copy() + page_params["pn"] = str(page) + tasks.append(fetch_single_page(session, url, page_params)) + + results = await asyncio.gather(*tasks) + return results + + +def process_data(page_results: List[Dict]) -> pd.DataFrame: + """处理数据为 DataFrame""" + all_data = [] + page_number = 1 + items_per_page = 100 + + for result in page_results: + if result.get("rc") == 0 and result.get("data") and result["data"].get("diff"): + page_data = result["data"]["diff"] + for item in page_data: + item["page_number"] = page_number + item["page_index"] = page_data.index(item) + all_data.extend(page_data) + page_number += 1 + + if not all_data: + return pd.DataFrame() + + df = pd.DataFrame(all_data) + df["序号"] = df.apply( + lambda row: (row["page_number"] - 1) * items_per_page + row["page_index"] + 1, + axis=1, + ) + df.drop(columns=["page_number", "page_index"], inplace=True, errors="ignore") + + column_map = { + "f1": "原序号", + "f2": "最新价", + "f3": "涨跌幅", + "f4": "涨跌额", + "f5": "成交量", + "f6": "成交额", + "f7": "振幅", + "f8": "换手率", + "f9": "市盈率-动态", + "f10": "量比", + "f11": "5分钟涨跌", + "f12": "代码", + "f13": "_", + "f14": "名称", + "f15": "最高", + "f16": "最低", + "f17": "今开", + "f18": "昨收", + "f20": "总市值", + "f21": "流通市值", + "f22": "涨速", + "f23": "市净率", + "f24": "60日涨跌幅", + "f25": "年初至今涨跌幅", + } + + df.rename(columns=column_map, inplace=True) + + desired_columns = [ + "序号", "代码", "名称", "最新价", "涨跌幅", "涨跌额", "成交量", "成交额", "振幅", + "最高", "最低", "今开", "昨收", "量比", "换手率", "市盈率-动态", "市净率", + "总市值", "流通市值", "涨速", "5分钟涨跌", "60日涨跌幅", "年初至今涨跌幅" + ] + available_columns = [col for col in desired_columns if col in df.columns] + df = df[available_columns] + + for col in [ + "最新价", "涨跌幅", "涨跌额", "成交量", "成交额", "振幅", "最高", "最低", "今开", + "昨收", "量比", "换手率", "市盈率-动态", "市净率", "总市值", "流通市值", "涨速", + "5分钟涨跌", "60日涨跌幅", "年初至今涨跌幅" + ]: + if col in df.columns: + df[col] = pd.to_numeric(df[col], errors="coerce") + + df.sort_values(by="涨跌幅", ascending=False, inplace=True) + df.reset_index(drop=True, inplace=True) + df["序号"] = df.index + 1 + return df + + +async def stock_zh_a_spot_em_async() -> pd.DataFrame: + url = "https://82.push2.eastmoney.com/api/qt/clist/get" + params = { + "pn": "1", + "pz": "100", + "po": "1", + "np": "1", + "ut": "bd1d9ddb04089700cf9c27f6f7426281", + "fltt": "2", + "invt": "2", + "fid": "f12", + "fs": "m:0 t:6,m:0 t:80,m:1 t:2,m:1 t:23,m:0 t:81 s:2048", + "fields": "f1,f2,f3,f4,f5,f6,f7,f8,f9,f10,f12,f13,f14,f15,f16,f17,f18," + "f20,f21,f23,f24,f25,f22,f11,f62,f128,f136,f115,f152", + } + results = await fetch_all_pages_async(url, params) + return process_data(results) + + +def stock_zh_a_spot_em() -> pd.DataFrame: + """ + 东方财富网-沪深京 A 股-实时行情 (同步接口) + https://quote.eastmoney.com/center/gridlist.html#hs_a_board + :return: 实时行情 + :rtype: pandas.DataFrame + """ + import nest_asyncio + nest_asyncio.apply() + return asyncio.run(stock_zh_a_spot_em_async()) + + +if __name__ == "__main__": + df = stock_zh_a_spot_em() + print(df)