From b4865aa376c1a90a5241cb3ee89dcddc2e709bae Mon Sep 17 00:00:00 2001 From: kegao1995 Date: Wed, 12 Feb 2025 12:20:58 -0800 Subject: [PATCH 01/12] First demo --- Makefile | 0 app.py | 18 +++++++++++++++ assets/style.css | 30 +++++++++++++++++++++++++ environment.yml | 2 +- src/__init__.py | 0 src/callbacks.py | 53 +++++++++++++++++++++++++++++++++++++++++++ src/layout.py | 58 ++++++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 160 insertions(+), 1 deletion(-) create mode 100644 Makefile create mode 100644 app.py create mode 100644 assets/style.css create mode 100644 src/__init__.py create mode 100644 src/callbacks.py create mode 100644 src/layout.py diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..e69de29 diff --git a/app.py b/app.py new file mode 100644 index 0000000..f665a40 --- /dev/null +++ b/app.py @@ -0,0 +1,18 @@ +import dash +import dash_bootstrap_components as dbc +from src.layout import layout +from src.callbacks import register_callbacks + +# 初始化 Dash 应用 +app = dash.Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP]) +app.title = "NASDAQ 100 Companies" + +# 绑定布局 +app.layout = layout + +# 注册回调函数 +register_callbacks(app) + +# 运行应用 +if __name__ == "__main__": + app.run_server(debug=True) diff --git a/assets/style.css b/assets/style.css new file mode 100644 index 0000000..2f5ae0c --- /dev/null +++ b/assets/style.css @@ -0,0 +1,30 @@ +/* 设置页面背景颜色 */ +body { + background-color: #f8f9fa; + font-family: Arial, sans-serif; +} + +/* 侧边栏样式 */ +.sidebar { + background-color: #ffffff; + border-right: 1px solid #ddd; + padding: 20px; +} + +/* 高亮导航项 */ +.sidebar .nav-link.active { + font-weight: bold; + color: #007bff !important; +} + +/* 标题样式 */ +h2 { + font-weight: bold; +} + +/* 表格样式 */ +.dash-table-container { + margin-top: 20px; +} + +/* 筛 diff --git a/environment.yml b/environment.yml index 1a6ca54..c303487 100644 --- a/environment.yml +++ b/environment.yml @@ -1,4 +1,4 @@ -name: "hirehunt" +name: "532" channels: - conda-forge diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/callbacks.py b/src/callbacks.py new file mode 100644 index 0000000..ee12a99 --- /dev/null +++ b/src/callbacks.py @@ -0,0 +1,53 @@ +import pandas as pd +from dash import Input, Output + +# 预设数据(模拟 NASDAQ 100 数据) +data = { + "Ticker": ["AAPL", "NVDA", "MSFT", "AMZN", "AVGO", "META"], + "Name": ["Apple Inc", "NVIDIA Corp", "Microsoft Corp", "Amazon.com Inc", "Broadcom Inc", "Meta Platforms Inc"], + "Weight": ["8.68%", "8.03%", "7.70%", "6.09%", "4.39%", "3.94%"], + "Price": [236.23, 130.97, 409.05, 230.67, 236.01, 725.01], + "IntradayReturn": [1.55, -1.38, -0.58, -0.90, 0.41, 0.72], + "Volume": ["28.06M", "123.99M", "10.64M", "19.29M", "10.40M", "8.69M"], + "Amount": ["6.57B", "16.26B", "4.35B", "4.44B", "2.43B", "6.26B"], + "MarketCap": ["3548.66B", "3207.33B", "3040.83B", "2444.58B", "1106.26B", "1836.93B"], + "YTDReturn": [-5.56, -2.48, -2.95, 5.14, 1.80, 23.83], + "Sector": ["Tech", "Tech", "Tech", "Consumer", "Tech", "Consumer"], +} + +df = pd.DataFrame(data) + +def register_callbacks(app): + """注册 Dash 回调函数""" + + @app.callback( + Output("stock-table", "data"), + [Input("filter-ticker", "value"), + Input("filter-name", "value"), + Input("filter-sector", "value")] + ) + def update_table(ticker, name, sector): + """更新表格数据""" + filtered_df = df.copy() + + # 按 Ticker 过滤(模糊匹配) + if ticker: + filtered_df = filtered_df[filtered_df["Ticker"].str.contains(ticker, case=False, na=False)] + + # 按 Name 过滤(模糊匹配) + if name: + filtered_df = filtered_df[filtered_df["Name"].str.contains(name, case=False, na=False)] + + # 按 Sector 过滤 + if sector and sector != "All": + filtered_df = filtered_df[filtered_df["Sector"] == sector] + + return filtered_df.to_dict("records") + + @app.callback( + Output("show-charts", "style"), + [Input("show-charts", "value")] + ) + def toggle_chart_visibility(show): + """控制图表显示隐藏""" + return {"display": "block" if show else "none"} diff --git a/src/layout.py b/src/layout.py new file mode 100644 index 0000000..360645f --- /dev/null +++ b/src/layout.py @@ -0,0 +1,58 @@ +import dash_bootstrap_components as dbc +from dash import dcc, html, dash_table + +# 侧边栏 (Sidebar) +sidebar = html.Div( + [ + html.H5("US", className="text-muted"), + html.Ul( + [ + html.Li(dbc.NavLink("NASDAQ 100", active=True, href="#")), + html.Li("S&P 500"), + ] + ), + ], + className="sidebar p-3", + style={"width": "200px", "height": "100vh", "position": "fixed", "left": "0", "top": "0", "background": "#f8f9fa"}, +) + +# 筛选条件 (Filter Criteria) +filter_form = dbc.Row( + [ + dbc.Col(dcc.Input(id="filter-ticker", type="text", placeholder="Ticker", className="form-control"), width=2), + dbc.Col(dcc.Input(id="filter-name", type="text", placeholder="Name", className="form-control"), width=3), + dbc.Col(dcc.Dropdown( + id="filter-sector", + options=[{"label": "All", "value": "All"}] + [{"label": sec, "value": sec} for sec in ["Tech", "Consumer"]], + value="All", + clearable=False + ), width=2), + ], + className="mb-3", +) + +# 数据表格 (Table) +table = dash_table.DataTable( + id="stock-table", + columns=[ + {"name": col, "id": col} for col in ["Ticker", "Name", "Weight", "Price", "IntradayReturn", "Volume", "Amount", "MarketCap", "YTDReturn"] + ], + style_table={"overflowX": "auto"}, + style_cell={"textAlign": "left"}, +) + +# 页面内容 (Main Content) +content = html.Div( + [ + html.H2("NASDAQ 100 Companies", className="mt-3"), + html.A("NASDAQ 100 Index ETF", href="#", className="text-primary"), + dbc.Checkbox(id="show-charts", label="Show Charts", value=False), + html.Div("Filter Criteria", className="text-muted"), + filter_form, + table, + ], + style={"margin-left": "220px", "padding": "20px"}, +) + +# 组合完整布局 +layout = html.Div([sidebar, content]) From 2487952e79bbdd24dd7fb2d7e948d1895e45e34f Mon Sep 17 00:00:00 2001 From: kegao1995 Date: Wed, 12 Feb 2025 18:00:45 -0800 Subject: [PATCH 02/12] First nasdaq 100 dataframe demo --- src/app.py | 20 ++- src/callbacks.py | 64 ++++++--- src/layout.py | 137 ++++++++++++++++-- src/qqqm_data.py | 91 ++++++++++++ src/xueqiu_data.py | 346 +++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 628 insertions(+), 30 deletions(-) create mode 100644 src/qqqm_data.py create mode 100644 src/xueqiu_data.py diff --git a/src/app.py b/src/app.py index 47b197a..f665a40 100644 --- a/src/app.py +++ b/src/app.py @@ -1,2 +1,18 @@ -# Kept empty for Milestone 1 -# Software will be written in Python with Dash \ No newline at end of file +import dash +import dash_bootstrap_components as dbc +from src.layout import layout +from src.callbacks import register_callbacks + +# 初始化 Dash 应用 +app = dash.Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP]) +app.title = "NASDAQ 100 Companies" + +# 绑定布局 +app.layout = layout + +# 注册回调函数 +register_callbacks(app) + +# 运行应用 +if __name__ == "__main__": + app.run_server(debug=True) diff --git a/src/callbacks.py b/src/callbacks.py index ee12a99..5754bdf 100644 --- a/src/callbacks.py +++ b/src/callbacks.py @@ -1,25 +1,27 @@ import pandas as pd -from dash import Input, Output - -# 预设数据(模拟 NASDAQ 100 数据) -data = { - "Ticker": ["AAPL", "NVDA", "MSFT", "AMZN", "AVGO", "META"], - "Name": ["Apple Inc", "NVIDIA Corp", "Microsoft Corp", "Amazon.com Inc", "Broadcom Inc", "Meta Platforms Inc"], - "Weight": ["8.68%", "8.03%", "7.70%", "6.09%", "4.39%", "3.94%"], - "Price": [236.23, 130.97, 409.05, 230.67, 236.01, 725.01], - "IntradayReturn": [1.55, -1.38, -0.58, -0.90, 0.41, 0.72], - "Volume": ["28.06M", "123.99M", "10.64M", "19.29M", "10.40M", "8.69M"], - "Amount": ["6.57B", "16.26B", "4.35B", "4.44B", "2.43B", "6.26B"], - "MarketCap": ["3548.66B", "3207.33B", "3040.83B", "2444.58B", "1106.26B", "1836.93B"], - "YTDReturn": [-5.56, -2.48, -2.95, 5.14, 1.80, 23.83], - "Sector": ["Tech", "Tech", "Tech", "Consumer", "Tech", "Consumer"], -} - -df = pd.DataFrame(data) +from dash import Input, Output, dcc \ + +from src.qqqm_data import getQQQMHolding + +df = getQQQMHolding() + def register_callbacks(app): """注册 Dash 回调函数""" + def highlight_change(val): + try: + val = float(val) + if val < 0: + opacity = min(abs(val) / 0.1, 1) + color = f'rgba(255, 0, 0, {opacity})' # 红色渐变 + else: + opacity = min(val / 0.1, 1) + color = f'rgba(0, 255, 0, {opacity})' # 绿色渐变 + return color + except: + return '' + @app.callback( Output("stock-table", "data"), [Input("filter-ticker", "value"), @@ -44,6 +46,25 @@ def update_table(ticker, name, sector): return filtered_df.to_dict("records") + @app.callback( + Output("stock-table", "style_data_conditional"), + Input("stock-table", "data") + ) + def update_intraday_return_styles(data): + """应用 highlight_change 至 IntradayReturn 列""" + styles = [] + if data: + for i, row in enumerate(data): + val = row.get("IntradayReturn") + if val is None: + continue + color = highlight_change(val) + styles.append({ + "if": { "row_index": i, "column_id": "IntradayReturn" }, + "backgroundColor": color + }) + return styles + @app.callback( Output("show-charts", "style"), [Input("show-charts", "value")] @@ -51,3 +72,12 @@ def update_table(ticker, name, sector): def toggle_chart_visibility(show): """控制图表显示隐藏""" return {"display": "block" if show else "none"} + + @app.callback( + Output("download-csv", "data"), + Input("download-csv-btn", "n_clicks"), + prevent_initial_call=True + ) + def download_csv(n_clicks): + """将 df 导出为 CSV 文件下载""" + return dcc.send_data_frame(df.to_csv, "NASDAQ_100.csv", index=False) diff --git a/src/layout.py b/src/layout.py index 360645f..d398b26 100644 --- a/src/layout.py +++ b/src/layout.py @@ -1,5 +1,5 @@ import dash_bootstrap_components as dbc -from dash import dcc, html, dash_table +from dash import dcc, html, dash_table, callback, Input, Output, State # 侧边栏 (Sidebar) sidebar = html.Div( @@ -20,39 +20,154 @@ filter_form = dbc.Row( [ dbc.Col(dcc.Input(id="filter-ticker", type="text", placeholder="Ticker", className="form-control"), width=2), - dbc.Col(dcc.Input(id="filter-name", type="text", placeholder="Name", className="form-control"), width=3), + dbc.Col(dcc.Input(id="filter-name", type="text", placeholder="Name", className="form-control"), width=2), dbc.Col(dcc.Dropdown( id="filter-sector", - options=[{"label": "All", "value": "All"}] + [{"label": sec, "value": sec} for sec in ["Tech", "Consumer"]], + options=[{"label": "All", "value": "All"}] + [{"label": sec, "value": sec} for sec in [ + 'Information Technology', 'Consumer Discretionary', 'Communication Services', + 'Consumer Staples', 'Materials', 'Health Care', 'Industrials', 'Utilities', + 'Financials', 'Energy', 'Real Estate' + ]], value="All", clearable=False - ), width=2), + ), width=3), ], className="mb-3", ) -# 数据表格 (Table) +# 保存原始列定义(用于恢复原始表头名称) +original_columns = [ + {"name": col, "id": col, "type": "numeric", "format": {"specifier": ".2%"}} + if col in [ + 'Weight', 'IntradayReturn', 'IntradayContribution','DividendYield', + 'YTDReturn', 'YTDContribution' + ] + else {"name": col, "id": col, "type": "numeric", "format": {"specifier": ".2f"}} + if col in [ + 'Price', 'PE', 'PB', 'Dividend' + ] + else {"name": col, "id": col} + for col in [ + 'Ticker', 'Name', 'Weight', 'Price', 'IntradayReturn', 'Volume', 'Amount', + 'IntradayContribution', 'MarketCap', 'YTDReturn', 'YTDContribution', 'PE', + 'PB', 'Profit_TTM', 'DividendYield', 'Dividend', 'SharesOutstanding', + 'Sector', 'Date' + ] +] + +# 数据表格 (Table) 修改:添加自定义排序属性 table = dash_table.DataTable( id="stock-table", - columns=[ - {"name": col, "id": col} for col in ["Ticker", "Name", "Weight", "Price", "IntradayReturn", "Volume", "Amount", "MarketCap", "YTDReturn"] - ], - style_table={"overflowX": "auto"}, - style_cell={"textAlign": "left"}, + columns=original_columns, + sort_action="custom", # 启用自定义排序 + sort_mode="single", # 单列排序 + style_table={ + "overflowX": "auto", + "margin": "20px", + "boxShadow": "0 4px 6px rgba(0, 0, 0, 0.1)" + }, + style_cell={ + "textAlign": "left", # 修改为左对齐 + "padding": "12px", # 增加内边距 + "fontFamily": "Arial, sans-serif", + "fontSize": "14px", + }, + style_header={ + "backgroundColor": "#f0f0f0", + "fontWeight": "bold", + "border": "1px solid #ccc" + }, + style_data={ + "backgroundColor": "white", + "border": "1px solid #ccc" + }, + style_data_conditional=[], # 回调将更新该属性 ) +# 添加 dcc.Store 保存排序状态和原始数据顺序 +store_components = html.Div([ + dcc.Store(id="sort-state", data={}), + dcc.Store(id="original-data") # 假定在首次加载时设置为原始数据内容 +]) + # 页面内容 (Main Content) content = html.Div( [ html.H2("NASDAQ 100 Companies", className="mt-3"), - html.A("NASDAQ 100 Index ETF", href="#", className="text-primary"), + html.A("NASDAQ 100 Index ETF", href="https://www.invesco.com/us/financial-products/etfs/product-detail?audienceType=Investor&productId=ETF-QQQM", className="text-primary"), dbc.Checkbox(id="show-charts", label="Show Charts", value=False), html.Div("Filter Criteria", className="text-muted"), filter_form, + # 新增下载 CSV 按钮和下载组件 + dbc.Button("Download CSV", id="download-csv-btn", color="primary", className="mb-3"), + dcc.Download(id="download-csv"), table, + store_components # 添加排序状态与原始数据存储 ], style={"margin-left": "220px", "padding": "20px"}, ) # 组合完整布局 layout = html.Div([sidebar, content]) + +# 回调函数:根据表头点击循环更新排序状态和箭头显示 +@callback( + Output("stock-table", "data", allow_duplicate=True), + Output("stock-table", "columns", allow_duplicate=True), + Output("sort-state", "data", allow_duplicate=True), + Output("original-data", "data", allow_duplicate=True), + Output("stock-table", "sort_by", allow_duplicate=True), + Input("stock-table", "sort_by"), + State("sort-state", "data"), + State("stock-table", "data"), + State("original-data", "data"), + prevent_initial_call=True +) +def update_sort(sort_by, sort_state, data, original_data): + # 初始化原始数据 + if not original_data: + original_data = data + + # 决定要操作的排序列 + if sort_by: + sort_col = sort_by[0]["column_id"] + else: + # 如果没有新的 sort_by,但已有上次点击记录,则使用它 + sort_col = sort_state.get("last_sorted") + if not sort_col: + return data, original_columns, sort_state, original_data, sort_by + + # 根据上一次状态计算新的排序方向 + prev = sort_state.get(sort_col, "none") + if prev == "none": + new_direction = "asc" + elif prev == "asc": + new_direction = "desc" + else: + new_direction = "none" + # 重置排序状态,并记录当前点击列 + sort_state = {col_def["id"]: "none" for col_def in original_columns} + sort_state["last_sorted"] = sort_col + sort_state[sort_col] = new_direction + + # 更新列标题,添加箭头提示 + new_columns = [] + for col_def in original_columns: + cid = col_def["id"] + base_name = col_def["name"].split(" ")[0] + arrow = "" + if sort_state.get(cid, "none") == "asc": + arrow = " ↑" + elif sort_state.get(cid, "none") == "desc": + arrow = " ↓" + new_columns.append({**col_def, "name": base_name + arrow}) + + # 更新数据:若方向为 "none" 恢复原始,否则排序数据 + if new_direction == "none": + sorted_data = original_data + sort_by = [] # 清空排序状态 + else: + reverse = True if new_direction == "desc" else False + sorted_data = sorted(data, key=lambda row: row.get(sort_col, None), reverse=reverse) + + return sorted_data, new_columns, sort_state, original_data, sort_by diff --git a/src/qqqm_data.py b/src/qqqm_data.py new file mode 100644 index 0000000..708f081 --- /dev/null +++ b/src/qqqm_data.py @@ -0,0 +1,91 @@ +# %% +import requests +import pandas as pd +from io import BytesIO + +from src.xueqiu_data import getBatchQuote +# 添加缓存变量 +_ndx_holding_cache = None + +def downloadQQQMHolding(): + global _ndx_holding_cache + if _ndx_holding_cache is not None: + return _ndx_holding_cache + + try: + # Send a GET request to the URL + url = "https://www.invesco.com/us/financial-products/etfs/holdings/main/holdings/0?audienceType=Investor&action=download&ticker=QQQM" + response = requests.get(url) + response.raise_for_status() # Raise an error for bad status codes + + # 使用 BytesIO 将字节内容转换为文件对象 + content_io = BytesIO(response.content) + # 使用 pandas 读取 CSV 内容 + df = pd.read_csv(content_io) + _ndx_holding_cache = df # 缓存数据 + return df + + except requests.exceptions.RequestException as e: + print(f"Error downloading the CSV file: {e}") + +# 清理df +def getQQQMHolding(): + df = downloadQQQMHolding() + df = df.rename(columns={'Holding Ticker': 'Ticker'}) + df['Ticker'] = df['Ticker'].str.strip() + + valid_classes = ['Common Stock', 'American Depository Receipt', 'American Depository Receipt - NY'] + df = df[df['Class of Shares'].isin(valid_classes)] + + filter_column = ['Ticker', 'Name', 'Weight', 'Sector', 'Class of Shares'] + df = df[filter_column] + + # 将 Weight 列转换为数值类型 + df['Weight'] = pd.to_numeric(df['Weight'], errors='coerce') + df["Weight"] = df["Weight"] / df["Weight"].sum() + + xueqiu_df= getBatchQuote(df['Ticker']) + + # 在 Ticker 列上进行合并 + merged_df = pd.merge(df,xueqiu_df,left_on='Ticker',right_on = 'symbol') + + # 排序 + + merged_df = merged_df.rename(columns={ + 'current': 'Price', + 'percent': 'IntradayReturn', + 'exchange': 'Exchange', + 'volume': 'Volume', + 'amount': 'Amount', + 'market_capital': 'MarketCap', + 'current_year_percent': 'YTDReturn', + 'total_shares': 'SharesOutstanding', + 'pe_ttm': 'PE', + 'pe_forecast': 'ForwardPE', + 'pb': 'PB', + 'dividend': 'Dividend', + 'dividend_yield': 'DividendYield', + 'profit_four':'Profit_TTM', + 'Timestamp_str':'Date', + }) + merged_df['IntradayContribution'] = merged_df['Weight'] * merged_df['IntradayReturn'] + merged_df['YTDContribution'] = merged_df['Weight'] * merged_df['YTDReturn'] + + output_columns = ['Ticker', 'Name', 'Weight', 'Price','IntradayReturn', + 'Volume','Amount','IntradayContribution', + 'MarketCap', 'YTDReturn','YTDContribution', + 'PE','PB','Profit_TTM', 'DividendYield','Dividend', + 'SharesOutstanding','Sector','Date'] + merged_df = merged_df[output_columns] + + return merged_df + + + +if __name__ == "__main__": + df = getQQQMHolding() + print(df) + + + +# %% diff --git a/src/xueqiu_data.py b/src/xueqiu_data.py new file mode 100644 index 0000000..57ff4b4 --- /dev/null +++ b/src/xueqiu_data.py @@ -0,0 +1,346 @@ +# %% +import requests +import pandas as pd +from datetime import datetime +import pytz + +import requests + +headers = { + "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", + "accept-language": "zh-CN,zh;q=0.9,en;q=0.8", + "priority": "u=0, i", + "sec-ch-ua": '"Not(A:Brand";v="99", "Google Chrome";v="133", "Chromium";v="133")', + "sec-ch-ua-mobile": "?0", + "sec-ch-ua-platform": '"Windows"', + "sec-fetch-dest": "document", + "sec-fetch-mode": "navigate", + "sec-fetch-site": "none", + "sec-fetch-user": "?1", + "upgrade-insecure-requests": "1", + "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36" +} + +cookies = { + "cookiesu": "501716971730603", + "device_id": "b466cc5fe5c41c2cf5113e1dc9758e94", + "s": "br1biz2pdb", + "bid": "3f1caaa1da9c9048cf5319e6a0c33666_lwsmxccn", + "remember": "1", + "xq_a_token": "ad0c76ebec59c335683e9c829d229902421be184", + "xqat": "ad0c76ebec59c335683e9c829d229902421be184", + "xq_id_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJ1aWQiOjIxMTA3NTAwNjIsImlzcyI6InVjIiwiZXhwIjoxNzQxMzI1NTA5LCJjdG0iOjE3Mzg3MzM3NzkyMjYsImNpZCI6ImQ5ZDBuNEFadXAifQ.o3ERaxqVjeTNJblmuRKENhbxML8EMGa0zQuiI4JMy38qDqK3QRcyYBOG2hnfJf1QGCS_1jtUacxjgLjEDwZ_cAeUCdHZ4a4PmFHIxLzwCm_cLg38T2d5NqddtaiceZFl-rmm4kzF6WAUEoAUrJXnrYn9yd4PWjA6pG1tr99und9iB-SZ78Tml0UGHyrka8Qt1ebJ6EVqDrlNABThY9utY9-a4pPnHknN-ooJ15O0rLhTIrqnhO8ZHSer-5ii6rpZuVCfu2xZFXt0YalPXQEcdKVJm5RpADv2ydqPZUDEI8X2GB1dyt2yOn6o8zwbPnV86nQIUQbCKo__gonzvHttnw", + "xq_r_token": "fa088756ba361870080110924808b5fa01c3ef13", + "xq_is_login": "1", + "u": "2110750062", + "Hm_lvt_1db88642e346389874251b5a1eded6e3": "1738819328,1738860881,1738891671,1739173290", + "HMACCOUNT": "90AC0DA1311E6AC3", + "Hm_lpvt_1db88642e346389874251b5a1eded6e3": "1739173300", +} + + + +def getMinuteData(ticker): + url = 'https://stock.xueqiu.com/v5/stock/chart/minute.json?symbol='+ ticker +'&period=1d' + + response = requests.get(url, headers=headers, cookies=cookies) + response_json = response.json() + + # 提取所有字段并转换为DataFrame + data_items = response_json['data']['items'] + + # 移除不需要的字段 + for item in data_items: + item.pop('macd', None) + item.pop('kdj', None) + item.pop('ratio', None) + item.pop('capital', None) + item.pop('volume_compare', None) + + df = pd.DataFrame(data_items) + + # 填充缺失值 + df.fillna({'high': df['current'], 'low': df['current']}, inplace=True) + df['amount_total'] = df['amount_total'].ffill() + df['volume_total'] = df['volume_total'].ffill() + + # 将 timestamp 转换为 pandas 的 datetime 对象 + df['Timestamp_str'] = pd.to_datetime(df['timestamp'].to_list(), unit='ms').tz_localize('UTC').tz_convert('America/New_York') + df['Timestamp_str'] = df['Timestamp_str'].dt.strftime('%Y-%m-%d %H:%M:%S') + + # 加入 Ticker 列 + df['Ticker'] = ticker + + return df + +def getJson(symbols): + symbols_str = ','.join(symbols) + url = 'https://stock.xueqiu.com/v5/stock/batch/quote.json?symbol=' + symbols_str + '&extend=detail&is_delay_hk=false' + + response = requests.get(url, headers=headers, cookies=cookies) + response_json = response.json() + quote_data_list = [item['quote'] for item in response_json['data']['items']] + + return quote_data_list + + +def getBatchQuote(symbols): + symbols_str = ','.join(symbols) + url = 'https://stock.xueqiu.com/v5/stock/batch/quote.json?symbol=' + symbols_str + '&extend=detail' + + response = requests.get(url, headers=headers, cookies=cookies) + response_json = response.json() + quote_data_list = [item['quote'] for item in response_json['data']['items']] + + # 将所有 quote 数据转换为 DataFrame,每个 quote 数据占一行 + df = pd.DataFrame(quote_data_list) + """ + output_colnames=['symbol', 'code', 'exchange', 'name', 'type', 'sub_type', 'status', + 'current', 'currency', 'percent', 'chg', 'timestamp', 'time', + 'lot_size', 'tick_size', 'open', 'last_close', 'high', 'low', + 'avg_price', 'volume', 'amount', 'turnover_rate', 'amplitude', + 'market_capital', 'float_market_capital', 'total_shares', + 'float_shares', 'issue_date', 'lock_set', 'current_year_percent', + 'high52w', 'low52w', 'variable_tick_size', 'volume_ratio', 'eps', + 'pe_ttm', 'pe_lyr', 'navps', 'pb', 'dividend', 'dividend_yield', 'psr', + 'short_ratio', 'inst_hld', 'beta', 'timestamp_ext', 'current_ext', + 'percent_ext', 'chg_ext', 'contract_size', 'pe_forecast', + 'profit_forecast', 'profit', 'profit_four', 'pledge_ratio', + 'goodwill_in_net_assets', 'shareholder_funds'] + """ + # 将 timestamp 转换为 pandas 的 datetime 对象 + df['Timestamp_str'] = ( + pd.to_datetime(df['timestamp'].to_list(), unit='ms') + .tz_localize('UTC') + .tz_convert('America/New_York') + .strftime('%Y-%m-%d %H:%M:%S %z') + ) + + # 填充缺失值 + df.fillna({'dividend': 0, 'dividend_yield': 0}, inplace=True) + + df['percent'] = df['percent'].div(100) + df['current_year_percent'] = df['current_year_percent'].div(100) + try: + df['dividend_yield'] = df['dividend_yield'].div(100) + except: + pass + + + return df + +def getBondQuote(symbols): + symbols_str = ','.join(symbols) + url = 'https://stock.xueqiu.com/v5/stock/batch/quote.json?symbol=' + symbols_str + '&extend=detail' + + + response = requests.get(url, headers=headers, cookies=cookies) + response_json = response.json() + quote_data_list = [item['quote'] for item in response_json['data']['items']] + + # 将所有 quote 数据转换为 DataFrame,每个 quote 数据占一行 + df = pd.DataFrame(quote_data_list) + """ + output_colnames=['symbol', 'code', 'exchange', 'name', 'type', 'sub_type', 'status', + 'current', 'currency', 'percent', 'chg', 'timestamp', 'time', + 'lot_size', 'tick_size', 'open', 'last_close', 'high', 'low', + 'avg_price', 'volume', 'amount', 'turnover_rate', 'amplitude', + 'market_capital', 'float_market_capital', 'total_shares', + 'float_shares', 'issue_date', 'lock_set', 'current_year_percent', + 'high52w', 'low52w', 'limit_up', 'limit_down', 'volume_ratio', + 'par_value', 'circulation', 'close_dirty_price', 'coupon_rate', + 'list_date', 'maturity_date', 'value_dates', 'termtomaturity', + 'accrued_interest', 'dis_next_pay_date', 'convert_rate', 'payment_mode', + 'variety_type', 'new_issue_rating', 'credit_rating', 'rating', + 'Timestamp_str'] + """ + # 将 timestamp 转换为 pandas 的 datetime 对象 + df['Timestamp_str'] = ( + pd.to_datetime(df['timestamp'].to_list(), unit='ms') + .tz_localize('UTC') + .tz_convert('Asia/Shanghai') + .strftime('%Y-%m-%d %H:%M:%S %z') + ) + df['list_date_str'] = ( + pd.to_datetime(df['list_date'].to_list(), unit='ms') + .tz_localize('UTC') + .tz_convert('Asia/Shanghai') + .strftime('%Y-%m-%d') + ) + df['maturity_date_str'] = ( + pd.to_datetime(df['maturity_date'].to_list(), unit='ms') + .tz_localize('UTC') + .tz_convert('Asia/Shanghai') + .strftime('%Y-%m-%d') + ) + # 填充缺失值 + #df.fillna({'dividend': 0, 'dividend_yield': 0}, inplace=True) + + df['percent'] = df['percent'].div(100) + df['current_year_percent'] = df['current_year_percent'].div(100) + + df['coupon_rate'] = df['coupon_rate'].div(100) + return df + +def getETFQuote(symbols): + symbols_str = ','.join(symbols) + url = 'https://stock.xueqiu.com/v5/stock/batch/quote.json?symbol=' + symbols_str + '&extend=detail' + + response = requests.get(url, headers=headers, cookies=cookies) + response_json = response.json() + quote_data_list = [item['quote'] for item in response_json['data']['items']] + df = pd.DataFrame(quote_data_list) + ''' + output_colnames=['symbol', 'code', 'exchange', 'name', 'type', 'sub_type', 'status', + 'current', 'currency', 'percent', 'chg', 'timestamp', 'time', + 'lot_size', 'tick_size', 'open', 'last_close', 'high', 'low', + 'avg_price', 'volume', 'amount', 'turnover_rate', 'amplitude', + 'market_capital', 'float_market_capital', 'total_shares', + 'float_shares', 'issue_date', 'lock_set', 'current_year_percent', + 'high52w', 'low52w', 'limit_up', 'limit_down', 'volume_ratio', + 'unit_nav', 'acc_unit_nav', 'premium_rate', 'found_date', + 'expiration_date', 'nav_date', 'iopv'] + ''' + + # 将 timestamp 转换为 pandas 的 datetime 对象 + df['Timestamp_str'] = ( + pd.to_datetime(df['timestamp'].to_list(), unit='ms') + .tz_localize('UTC') + .tz_convert('Asia/Shanghai') + .strftime('%Y-%m-%d %H:%M:%S %z') + ) + df['NAV_Date_str'] = ( + pd.to_datetime(df['nav_date'].to_list(), unit='ms') + .tz_localize('UTC') + .tz_convert('Asia/Shanghai') + .strftime('%Y-%m-%d') + ) + # 填充缺失值 + #df.fillna({'dividend': 0, 'dividend_yield': 0}, inplace=True) + + df['percent'] = df['percent'].div(100) + df['current_year_percent'] = df['current_year_percent'].div(100) + + df['premium_rate'] = df['premium_rate'].div(100) + + return df + + +def getUSETFQuote(symbols): + #symbols = ["IBTF","IBTG"] + symbols_str = ','.join(symbols) + url = 'https://stock.xueqiu.com/v5/stock/batch/quote.json?symbol=' + symbols_str + '&extend=detail' + + response = requests.get(url, headers=headers, cookies=cookies) + response_json = response.json() + quote_data_list = [item['quote'] for item in response_json['data']['items']] + df = pd.DataFrame(quote_data_list) + + # 将 timestamp 转换为 pandas 的 datetime 对象 + df['Timestamp_str'] = ( + pd.to_datetime(df['timestamp'].to_list(), unit='ms') + .tz_localize('UTC') + .tz_convert('Asia/Shanghai') + .strftime('%Y-%m-%d %H:%M:%S %z') + ) + + # 填充缺失值 + #df.fillna({'dividend': 0, 'dividend_yield': 0}, inplace=True) + + df['percent'] = df['percent'].div(100) + df['current_year_percent'] = df['current_year_percent'].div(100) + return df + +def get_current_beijing_time(): + # 获取当前UTC时间 + utc_now = datetime.now(pytz.utc) + # 将UTC时间转换为北京时间 + beijing_tz = pytz.timezone('Asia/Shanghai') + beijing_now = utc_now.astimezone(beijing_tz) + # 将北京时间转换为毫秒时间戳 + milliseconds = int(beijing_now.timestamp() * 1000) + return milliseconds + +def getStockHistory(symbol,days = 30, begin = get_current_beijing_time()): + """ + 获取一个股票的日线历史数据。 + + 参数: + symbol (str): 股票代码。 + days (int): 前推日数,默认为30天。 + begin (int): 开始前推的时间,默认为当前北京时间的毫秒时间戳。 + + 返回: + pd.DataFrame: 包含股票日线历史数据的DataFrame。 + + 数据列: + - timestamp: 时间戳(毫秒) + - volume: 成交量 + - open: 开盘价 + - high: 最高价 + - low: 最低价 + - close: 收盘价 + - chg: 涨跌额 + - percent: 涨跌幅 + - turnoverrate: 换手率 + - amount: 成交额 + - volume_post: 盘后成交量 + - amount_post: 盘后成交额 + - Timestamp_str: 格式化的北京时间字符串 + - Ticker: 股票代码 + """ + + url = f'https://stock.xueqiu.com/v5/stock/chart/kline.json?symbol={symbol}&begin={begin}&period=day&type=before&count=-{days}' + + response = requests.get(url, headers=headers, cookies=cookies) + response_json = response.json() + + # 提取列名和数据项 + columns = response_json['data']['column'] + items = response_json['data']['item'] + + # 将数据项转换为 DataFrame,并设置列名 + df = pd.DataFrame(items, columns=columns) + + # 将 timestamp 转换为 pandas 的 datetime 对象 + df['Timestamp_str'] = ( + pd.to_datetime(df['timestamp'].to_list(), unit='ms') + .tz_localize('UTC') + .tz_convert('Asia/Shanghai') + .strftime('%Y-%m-%d %H:%M:%S %z') + ) + + df['percent'] = df['percent'].div(100) + # 加入 Ticker 列 + df['Ticker'] = symbol + """ + df.columns = ['timestamp', 'volume', 'open', 'high', 'low', 'close', 'chg', 'percent', + 'turnoverrate', 'amount', 'volume_post', 'amount_post', + 'Timestamp_str','Ticker'] + """ + return df + + + + +# %% +if __name__ == "__main__": + pass + df = getMinuteData('AAPL') + #ticker_list = ['SH019742'] + # ticker_list = ['SZ161125','SZ161130','SH513000','SH513300'] + # df = getETFQuote(ticker_list) + #df.to_excel("etf.xlsx") + # bond_ticker_list = ['SH019746','SH019742'] + # df = getBondQuote(bond_ticker_list) + # df.to_excel("bond.xlsx") + + #stock_ticker_list = ['AAPL','GOOG'] + #df = getBatchQuote(stock_ticker_list) + + + +# %% + + +# %% From 56c8424e441a3623a115a213e0851340f5f6c070 Mon Sep 17 00:00:00 2001 From: kegao1995 Date: Wed, 12 Feb 2025 18:11:53 -0800 Subject: [PATCH 03/12] fixed bug in timestamp conversion --- src/xueqiu_data.py | 77 ++++++++++++++++------------------------------ 1 file changed, 27 insertions(+), 50 deletions(-) diff --git a/src/xueqiu_data.py b/src/xueqiu_data.py index 57ff4b4..3082cdd 100644 --- a/src/xueqiu_data.py +++ b/src/xueqiu_data.py @@ -65,8 +65,9 @@ def getMinuteData(ticker): df['volume_total'] = df['volume_total'].ffill() # 将 timestamp 转换为 pandas 的 datetime 对象 - df['Timestamp_str'] = pd.to_datetime(df['timestamp'].to_list(), unit='ms').tz_localize('UTC').tz_convert('America/New_York') - df['Timestamp_str'] = df['Timestamp_str'].dt.strftime('%Y-%m-%d %H:%M:%S') + df['Timestamp_str'] = pd.to_datetime(df['timestamp'], unit='ms', utc=True)\ + .tz_convert('America/New_York')\ + .dt.strftime('%Y-%m-%d %H:%M:%S') # 加入 Ticker 列 df['Ticker'] = ticker @@ -109,12 +110,9 @@ def getBatchQuote(symbols): 'goodwill_in_net_assets', 'shareholder_funds'] """ # 将 timestamp 转换为 pandas 的 datetime 对象 - df['Timestamp_str'] = ( - pd.to_datetime(df['timestamp'].to_list(), unit='ms') - .tz_localize('UTC') - .tz_convert('America/New_York') - .strftime('%Y-%m-%d %H:%M:%S %z') - ) + df['Timestamp_str'] = pd.to_datetime(df['timestamp'], unit='ms', utc=True)\ + .dt.tz_convert('America/New_York')\ + .dt.strftime('%Y-%m-%d %H:%M:%S %z') # 填充缺失值 df.fillna({'dividend': 0, 'dividend_yield': 0}, inplace=True) @@ -155,24 +153,15 @@ def getBondQuote(symbols): 'Timestamp_str'] """ # 将 timestamp 转换为 pandas 的 datetime 对象 - df['Timestamp_str'] = ( - pd.to_datetime(df['timestamp'].to_list(), unit='ms') - .tz_localize('UTC') - .tz_convert('Asia/Shanghai') - .strftime('%Y-%m-%d %H:%M:%S %z') - ) - df['list_date_str'] = ( - pd.to_datetime(df['list_date'].to_list(), unit='ms') - .tz_localize('UTC') - .tz_convert('Asia/Shanghai') - .strftime('%Y-%m-%d') - ) - df['maturity_date_str'] = ( - pd.to_datetime(df['maturity_date'].to_list(), unit='ms') - .tz_localize('UTC') - .tz_convert('Asia/Shanghai') - .strftime('%Y-%m-%d') - ) + df['Timestamp_str'] = pd.to_datetime(df['timestamp'], unit='ms', utc=True)\ + .dt.tz_convert('Asia/Shanghai')\ + .dt.strftime('%Y-%m-%d %H:%M:%S %z') + df['list_date_str'] = pd.to_datetime(df['list_date'], unit='ms', utc=True)\ + .dt.tz_convert('Asia/Shanghai')\ + .dt.strftime('%Y-%m-%d') + df['maturity_date_str'] = pd.to_datetime(df['maturity_date'], unit='ms', utc=True)\ + .dt.tz_convert('Asia/Shanghai')\ + .dt.strftime('%Y-%m-%d') # 填充缺失值 #df.fillna({'dividend': 0, 'dividend_yield': 0}, inplace=True) @@ -203,18 +192,12 @@ def getETFQuote(symbols): ''' # 将 timestamp 转换为 pandas 的 datetime 对象 - df['Timestamp_str'] = ( - pd.to_datetime(df['timestamp'].to_list(), unit='ms') - .tz_localize('UTC') - .tz_convert('Asia/Shanghai') - .strftime('%Y-%m-%d %H:%M:%S %z') - ) - df['NAV_Date_str'] = ( - pd.to_datetime(df['nav_date'].to_list(), unit='ms') - .tz_localize('UTC') - .tz_convert('Asia/Shanghai') - .strftime('%Y-%m-%d') - ) + df['Timestamp_str'] = pd.to_datetime(df['timestamp'], unit='ms', utc=True)\ + .dt.tz_convert('Asia/Shanghai')\ + .dt.strftime('%Y-%m-%d %H:%M:%S %z') + df['NAV_Date_str'] = pd.to_datetime(df['nav_date'], unit='ms', utc=True)\ + .dt.tz_convert('Asia/Shanghai')\ + .dt.strftime('%Y-%m-%d') # 填充缺失值 #df.fillna({'dividend': 0, 'dividend_yield': 0}, inplace=True) @@ -237,12 +220,9 @@ def getUSETFQuote(symbols): df = pd.DataFrame(quote_data_list) # 将 timestamp 转换为 pandas 的 datetime 对象 - df['Timestamp_str'] = ( - pd.to_datetime(df['timestamp'].to_list(), unit='ms') - .tz_localize('UTC') - .tz_convert('Asia/Shanghai') - .strftime('%Y-%m-%d %H:%M:%S %z') - ) + df['Timestamp_str'] = pd.to_datetime(df['timestamp'], unit='ms', utc=True)\ + .dt.tz_convert('Asia/Shanghai')\ + .dt.strftime('%Y-%m-%d %H:%M:%S %z') # 填充缺失值 #df.fillna({'dividend': 0, 'dividend_yield': 0}, inplace=True) @@ -303,12 +283,9 @@ def getStockHistory(symbol,days = 30, begin = get_current_beijing_time()): df = pd.DataFrame(items, columns=columns) # 将 timestamp 转换为 pandas 的 datetime 对象 - df['Timestamp_str'] = ( - pd.to_datetime(df['timestamp'].to_list(), unit='ms') - .tz_localize('UTC') - .tz_convert('Asia/Shanghai') - .strftime('%Y-%m-%d %H:%M:%S %z') - ) + df['Timestamp_str'] = pd.to_datetime(df['timestamp'], unit='ms', utc=True)\ + .dt.tz_convert('Asia/Shanghai')\ + .dt.strftime('%Y-%m-%d %H:%M:%S %z') df['percent'] = df['percent'].div(100) # 加入 Ticker 列 From a1455305ee23906f38729c1c9b0dd4677e1ea845 Mon Sep 17 00:00:00 2001 From: kegao1995 Date: Wed, 12 Feb 2025 19:37:31 -0800 Subject: [PATCH 04/12] Updated pie chart --- Makefile | 4 ++++ app.py | 18 ---------------- src/app.py | 4 ++-- src/callbacks.py | 9 ++++---- src/layout.py | 54 +++++++++++++++++++++++++++++++++++++++++++++++- src/qqqm_data.py | 2 +- 6 files changed, 65 insertions(+), 26 deletions(-) delete mode 100644 app.py diff --git a/Makefile b/Makefile index e69de29..b970f30 100644 --- a/Makefile +++ b/Makefile @@ -0,0 +1,4 @@ +.PHONY: run +run: + python src/app.py + diff --git a/app.py b/app.py deleted file mode 100644 index f665a40..0000000 --- a/app.py +++ /dev/null @@ -1,18 +0,0 @@ -import dash -import dash_bootstrap_components as dbc -from src.layout import layout -from src.callbacks import register_callbacks - -# 初始化 Dash 应用 -app = dash.Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP]) -app.title = "NASDAQ 100 Companies" - -# 绑定布局 -app.layout = layout - -# 注册回调函数 -register_callbacks(app) - -# 运行应用 -if __name__ == "__main__": - app.run_server(debug=True) diff --git a/src/app.py b/src/app.py index f665a40..8bb7b28 100644 --- a/src/app.py +++ b/src/app.py @@ -1,7 +1,7 @@ import dash import dash_bootstrap_components as dbc -from src.layout import layout -from src.callbacks import register_callbacks +from layout import layout +from callbacks import register_callbacks # 初始化 Dash 应用 app = dash.Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP]) diff --git a/src/callbacks.py b/src/callbacks.py index 5754bdf..addd2d0 100644 --- a/src/callbacks.py +++ b/src/callbacks.py @@ -1,7 +1,8 @@ import pandas as pd -from dash import Input, Output, dcc \ +import plotly.express as px # 新增导入 Plotly Express +from dash import Input, Output, dcc -from src.qqqm_data import getQQQMHolding +from qqqm_data import getQQQMHolding df = getQQQMHolding() @@ -26,9 +27,9 @@ def highlight_change(val): Output("stock-table", "data"), [Input("filter-ticker", "value"), Input("filter-name", "value"), - Input("filter-sector", "value")] + Input("filter-sector", "value")] # 移除 PE 滑块监听 ) - def update_table(ticker, name, sector): + def update_table(ticker, name, sector): # 移除 pe_range 参数 """更新表格数据""" filtered_df = df.copy() diff --git a/src/layout.py b/src/layout.py index d398b26..1e6ed74 100644 --- a/src/layout.py +++ b/src/layout.py @@ -1,6 +1,56 @@ +# %% import dash_bootstrap_components as dbc from dash import dcc, html, dash_table, callback, Input, Output, State +import pandas as pd +from pyecharts.charts import Pie +from pyecharts import options as opts +from qqqm_data import getQQQMHolding + + +def render_pie_chart(): + # 1. 获取并处理数据 + _df = getQQQMHolding() + df_group = _df.groupby('Name', as_index=False).agg({'Weight': 'sum'}) + df_group = df_group.sort_values(by='Weight', ascending=False) + top_10 = df_group.nlargest(10, 'Weight') + rest = df_group.iloc[10:] + rest_combined = pd.DataFrame({'Name': ['Other Companies'], 'Weight': [rest['Weight'].sum()]}) + combined_df = pd.concat([top_10, rest_combined], ignore_index=True) + + # 2. 组装 (名称, 权重) 列表 + data = list(zip(combined_df['Name'], combined_df['Weight'])) + + # 3. 为前 10 项指定不同颜色 + colors= [ + "#5470C6", "#91CC75", "#FAC858", "#EE6666", "#73C0DE", + "#3BA272", "#FC8452", "#9A60B4", "#EA7CCC", "#4A90E2","#999999"] + + # 4. 创建环形图 + chart = ( + Pie() + .add( + series_name="", + data_pair=data, + radius=["40%", "75%"], # 环形图的内外半径 + center=["50%", "50%"] # 图表居中 + ) + .set_colors(colors) # 为每个扇区设置颜色 + .set_global_opts( + title_opts=opts.TitleOpts( + title="NASDAQ 100 Companies by Weight", + pos_left="center" + ), + legend_opts=opts.LegendOpts(is_show=False), # 隐藏图例 + tooltip_opts=opts.TooltipOpts(formatter="{b}: {d}%") + ) + ) + + # 5. 返回嵌入式 HTML + return chart.render_embed() + + +# %% # 侧边栏 (Sidebar) sidebar = html.Div( [ @@ -90,7 +140,7 @@ dcc.Store(id="original-data") # 假定在首次加载时设置为原始数据内容 ]) -# 页面内容 (Main Content) +# 页面内容 (Main Content) 修改:使用 html.Iframe 显示 pyecharts 图表 content = html.Div( [ html.H2("NASDAQ 100 Companies", className="mt-3"), @@ -98,6 +148,8 @@ dbc.Checkbox(id="show-charts", label="Show Charts", value=False), html.Div("Filter Criteria", className="text-muted"), filter_form, + # 修改:使用 Iframe 显示 pyecharts 渲染的图表 + html.Iframe(srcDoc=render_pie_chart(), style={"border": "0", "width": "100%", "height": "600px"}), # 新增下载 CSV 按钮和下载组件 dbc.Button("Download CSV", id="download-csv-btn", color="primary", className="mb-3"), dcc.Download(id="download-csv"), diff --git a/src/qqqm_data.py b/src/qqqm_data.py index 708f081..bc0491e 100644 --- a/src/qqqm_data.py +++ b/src/qqqm_data.py @@ -3,7 +3,7 @@ import pandas as pd from io import BytesIO -from src.xueqiu_data import getBatchQuote +from xueqiu_data import getBatchQuote # 添加缓存变量 _ndx_holding_cache = None From 1b916bbfacf75781de1c830f94309d38a4273597 Mon Sep 17 00:00:00 2001 From: kegao1995 Date: Wed, 12 Feb 2025 19:53:15 -0800 Subject: [PATCH 05/12] Fixed unused code --- src/callbacks.py | 12 ++---------- src/layout.py | 6 ++---- 2 files changed, 4 insertions(+), 14 deletions(-) diff --git a/src/callbacks.py b/src/callbacks.py index addd2d0..6f2a29e 100644 --- a/src/callbacks.py +++ b/src/callbacks.py @@ -27,9 +27,9 @@ def highlight_change(val): Output("stock-table", "data"), [Input("filter-ticker", "value"), Input("filter-name", "value"), - Input("filter-sector", "value")] # 移除 PE 滑块监听 + Input("filter-sector", "value")] ) - def update_table(ticker, name, sector): # 移除 pe_range 参数 + def update_table(ticker, name, sector): """更新表格数据""" filtered_df = df.copy() @@ -66,14 +66,6 @@ def update_intraday_return_styles(data): }) return styles - @app.callback( - Output("show-charts", "style"), - [Input("show-charts", "value")] - ) - def toggle_chart_visibility(show): - """控制图表显示隐藏""" - return {"display": "block" if show else "none"} - @app.callback( Output("download-csv", "data"), Input("download-csv-btn", "n_clicks"), diff --git a/src/layout.py b/src/layout.py index 1e6ed74..01ebd96 100644 --- a/src/layout.py +++ b/src/layout.py @@ -1,4 +1,3 @@ -# %% import dash_bootstrap_components as dbc from dash import dcc, html, dash_table, callback, Input, Output, State import pandas as pd @@ -50,7 +49,6 @@ def render_pie_chart(): return chart.render_embed() -# %% # 侧边栏 (Sidebar) sidebar = html.Div( [ @@ -143,9 +141,8 @@ def render_pie_chart(): # 页面内容 (Main Content) 修改:使用 html.Iframe 显示 pyecharts 图表 content = html.Div( [ - html.H2("NASDAQ 100 Companies", className="mt-3"), + html.H1("NASDAQ 100 Companies", className="mt-3"), html.A("NASDAQ 100 Index ETF", href="https://www.invesco.com/us/financial-products/etfs/product-detail?audienceType=Investor&productId=ETF-QQQM", className="text-primary"), - dbc.Checkbox(id="show-charts", label="Show Charts", value=False), html.Div("Filter Criteria", className="text-muted"), filter_form, # 修改:使用 Iframe 显示 pyecharts 渲染的图表 @@ -175,6 +172,7 @@ def render_pie_chart(): State("original-data", "data"), prevent_initial_call=True ) + def update_sort(sort_by, sort_state, data, original_data): # 初始化原始数据 if not original_data: From ca853d2a0e795124ff8cfb8ad6abdd722cd6d485 Mon Sep 17 00:00:00 2001 From: kegao1995 Date: Wed, 12 Feb 2025 19:53:59 -0800 Subject: [PATCH 06/12] added pyecharts --- environment.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/environment.yml b/environment.yml index c303487..4db388e 100644 --- a/environment.yml +++ b/environment.yml @@ -12,4 +12,5 @@ dependencies: - scikit-learn=1.3.2 - matplotlib=3.9.2 - jupyterlab=4.2.6 - - pip=24.3 \ No newline at end of file + - pyecharts=2.0.8 + - pip=24.3 \ No newline at end of file From 3d0c364e5628b4169e7bca72a51ed5099304112c Mon Sep 17 00:00:00 2001 From: kegao1995 Date: Wed, 12 Feb 2025 20:01:54 -0800 Subject: [PATCH 07/12] added pyecharts piechart --- environment.yml | 4 +++- src/callbacks.py | 1 - src/layout.py | 4 ++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/environment.yml b/environment.yml index 4db388e..07dd332 100644 --- a/environment.yml +++ b/environment.yml @@ -13,4 +13,6 @@ dependencies: - matplotlib=3.9.2 - jupyterlab=4.2.6 - pyecharts=2.0.8 - - pip=24.3 \ No newline at end of file + - pip=24.3 + - pyecharts=2.0.8 + \ No newline at end of file diff --git a/src/callbacks.py b/src/callbacks.py index 6f2a29e..73e1489 100644 --- a/src/callbacks.py +++ b/src/callbacks.py @@ -6,7 +6,6 @@ df = getQQQMHolding() - def register_callbacks(app): """注册 Dash 回调函数""" diff --git a/src/layout.py b/src/layout.py index 01ebd96..9016606 100644 --- a/src/layout.py +++ b/src/layout.py @@ -143,10 +143,10 @@ def render_pie_chart(): [ html.H1("NASDAQ 100 Companies", className="mt-3"), html.A("NASDAQ 100 Index ETF", href="https://www.invesco.com/us/financial-products/etfs/product-detail?audienceType=Investor&productId=ETF-QQQM", className="text-primary"), + # 使用 Iframe 显示 pyecharts 渲染的图表 + html.Iframe(srcDoc=render_pie_chart(), style={"border": "0", "width": "100%", "height": "600px"}), html.Div("Filter Criteria", className="text-muted"), filter_form, - # 修改:使用 Iframe 显示 pyecharts 渲染的图表 - html.Iframe(srcDoc=render_pie_chart(), style={"border": "0", "width": "100%", "height": "600px"}), # 新增下载 CSV 按钮和下载组件 dbc.Button("Download CSV", id="download-csv-btn", color="primary", className="mb-3"), dcc.Download(id="download-csv"), From ad2448fde5bc830c1d89a31189e2bab8c97370a3 Mon Sep 17 00:00:00 2001 From: kegao1995 Date: Thu, 13 Feb 2025 09:48:03 -0800 Subject: [PATCH 08/12] added realtime data feature --- src/callbacks.py | 63 +++++++++++++++++++++++++++++++++--------------- src/layout.py | 39 ++++++++++++++++++++++++++---- 2 files changed, 77 insertions(+), 25 deletions(-) diff --git a/src/callbacks.py b/src/callbacks.py index 73e1489..370342f 100644 --- a/src/callbacks.py +++ b/src/callbacks.py @@ -1,11 +1,10 @@ import pandas as pd import plotly.express as px # 新增导入 Plotly Express from dash import Input, Output, dcc +import dash from qqqm_data import getQQQMHolding -df = getQQQMHolding() - def register_callbacks(app): """注册 Dash 回调函数""" @@ -22,29 +21,44 @@ def highlight_change(val): except: return '' + # 回调:每 n 秒更新数据,存入 dcc.Store(需在布局中添加 dcc.Store(id="data-store")) + @app.callback( + Output("data-store", "data"), + Input("data-update-interval", "n_intervals") + ) + def update_data(n_intervals): + return getQQQMHolding().to_dict("records") + + # 回调:根据下拉菜单选择更新频率,修改 Interval 组件属性(需在布局中添加 dcc.Interval(id="data-update-interval")和 dcc.Dropdown(id="update-speed")) + @app.callback( + [Output("data-update-interval", "disabled"), Output("data-update-interval", "interval")], + Input("update-speed", "value") + ) + def update_interval_speed(value): + if value == "3秒": + return False, 3000 + elif value == "10秒": + return False, 10000 + else: # "不更新" + return True, 1000 # interval 值无关紧要 + @app.callback( Output("stock-table", "data"), [Input("filter-ticker", "value"), Input("filter-name", "value"), - Input("filter-sector", "value")] + Input("filter-sector", "value"), + Input("data-store", "data")] ) - def update_table(ticker, name, sector): + def update_table(ticker, name, sector, data): """更新表格数据""" - filtered_df = df.copy() - - # 按 Ticker 过滤(模糊匹配) + df = pd.DataFrame(data) if data else pd.DataFrame() if ticker: - filtered_df = filtered_df[filtered_df["Ticker"].str.contains(ticker, case=False, na=False)] - - # 按 Name 过滤(模糊匹配) + df = df[df["Ticker"].str.contains(ticker, case=False, na=False)] if name: - filtered_df = filtered_df[filtered_df["Name"].str.contains(name, case=False, na=False)] - - # 按 Sector 过滤 + df = df[df["Name"].str.contains(name, case=False, na=False)] if sector and sector != "All": - filtered_df = filtered_df[filtered_df["Sector"] == sector] - - return filtered_df.to_dict("records") + df = df[df["Sector"] == sector] + return df.to_dict("records") @app.callback( Output("stock-table", "style_data_conditional"), @@ -67,9 +81,18 @@ def update_intraday_return_styles(data): @app.callback( Output("download-csv", "data"), - Input("download-csv-btn", "n_clicks"), + [Input("download-csv-btn", "n_clicks"), + Input("data-store", "data")], prevent_initial_call=True ) - def download_csv(n_clicks): - """将 df 导出为 CSV 文件下载""" - return dcc.send_data_frame(df.to_csv, "NASDAQ_100.csv", index=False) + def download_csv(n_clicks, data): + """当点击按钮时将数据导出为 CSV 文件下载""" + ctx = dash.callback_context + if not ctx.triggered or ctx.triggered[0]["prop_id"] != "download-csv-btn.n_clicks": + # 如果不是按钮触发,则不进行下载 + from dash.exceptions import PreventUpdate + raise PreventUpdate + df = pd.DataFrame(data) if data else pd.DataFrame() + def generate_csv_text(_): + return df.to_csv(index=False) + return dcc.send_string(generate_csv_text, "NASDAQ_100.csv") diff --git a/src/layout.py b/src/layout.py index 9016606..e4f8d74 100644 --- a/src/layout.py +++ b/src/layout.py @@ -83,6 +83,25 @@ def render_pie_chart(): className="mb-3", ) +# 修改更新频率选择 Dropdown:选项改为 "3秒"、"10秒" 和 "不更新",默认值为 "不更新" +update_speed_dropdown = dbc.Row( + dbc.Col( + dcc.Dropdown( + id="update-speed", + options=[ + {"label": "3秒", "value": "3秒"}, + {"label": "10秒", "value": "10秒"}, + {"label": "不更新", "value": "不更新"} + ], + value="不更新", + clearable=False, + style={"width": "150px"} + ), + width="auto" + ), + className="mb-3" +) + # 保存原始列定义(用于恢复原始表头名称) original_columns = [ {"name": col, "id": col, "type": "numeric", "format": {"specifier": ".2%"}} @@ -132,12 +151,20 @@ def render_pie_chart(): style_data_conditional=[], # 回调将更新该属性 ) -# 添加 dcc.Store 保存排序状态和原始数据顺序 +# 修改 store_components,新增 data-store 用于存放更新数据 store_components = html.Div([ dcc.Store(id="sort-state", data={}), - dcc.Store(id="original-data") # 假定在首次加载时设置为原始数据内容 + dcc.Store(id="original-data"), + dcc.Store(id="data-store") ]) +# 新增 dcc.Interval 用于触发数据更新(隐藏组件) +data_update_interval = dcc.Interval( + id="data-update-interval", + interval=1000, # 默认 1秒 + n_intervals=0 +) + # 页面内容 (Main Content) 修改:使用 html.Iframe 显示 pyecharts 图表 content = html.Div( [ @@ -146,12 +173,14 @@ def render_pie_chart(): # 使用 Iframe 显示 pyecharts 渲染的图表 html.Iframe(srcDoc=render_pie_chart(), style={"border": "0", "width": "100%", "height": "600px"}), html.Div("Filter Criteria", className="text-muted"), + update_speed_dropdown, # 新增更新频率选择控件 filter_form, # 新增下载 CSV 按钮和下载组件 dbc.Button("Download CSV", id="download-csv-btn", color="primary", className="mb-3"), dcc.Download(id="download-csv"), table, - store_components # 添加排序状态与原始数据存储 + store_components, # 添加排序状态与原始数据存储 + data_update_interval # 新增 Interval 控件,用于周期更新 ], style={"margin-left": "220px", "padding": "20px"}, ) @@ -206,9 +235,9 @@ def update_sort(sort_by, sort_state, data, original_data): cid = col_def["id"] base_name = col_def["name"].split(" ")[0] arrow = "" - if sort_state.get(cid, "none") == "asc": + if sort_state.get(cid, "asc") == "asc": arrow = " ↑" - elif sort_state.get(cid, "none") == "desc": + elif sort_state.get(cid, "desc") == "desc": arrow = " ↓" new_columns.append({**col_def, "name": base_name + arrow}) From 6b0c99eff2595ff8da0b5556408e7ba0dcf85132 Mon Sep 17 00:00:00 2001 From: kegao1995 Date: Thu, 13 Feb 2025 11:53:25 -0800 Subject: [PATCH 09/12] update environment.yml --- environment.yml | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/environment.yml b/environment.yml index 07dd332..1a6ca54 100644 --- a/environment.yml +++ b/environment.yml @@ -1,4 +1,4 @@ -name: "532" +name: "hirehunt" channels: - conda-forge @@ -12,7 +12,4 @@ dependencies: - scikit-learn=1.3.2 - matplotlib=3.9.2 - jupyterlab=4.2.6 - - pyecharts=2.0.8 - - pip=24.3 - - pyecharts=2.0.8 - \ No newline at end of file + - pip=24.3 \ No newline at end of file From 3c6cc5cc1082df3e551c54bcf2f6155e12c21a56 Mon Sep 17 00:00:00 2001 From: kegao1995 Date: Thu, 13 Feb 2025 12:00:54 -0800 Subject: [PATCH 10/12] added update time selection --- src/callbacks.py | 4 ++-- src/layout.py | 11 ++++++----- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/callbacks.py b/src/callbacks.py index 370342f..51f0dd9 100644 --- a/src/callbacks.py +++ b/src/callbacks.py @@ -35,9 +35,9 @@ def update_data(n_intervals): Input("update-speed", "value") ) def update_interval_speed(value): - if value == "3秒": + if value == "3s": return False, 3000 - elif value == "10秒": + elif value == "10s": return False, 10000 else: # "不更新" return True, 1000 # interval 值无关紧要 diff --git a/src/layout.py b/src/layout.py index e4f8d74..4be4df1 100644 --- a/src/layout.py +++ b/src/layout.py @@ -89,11 +89,11 @@ def render_pie_chart(): dcc.Dropdown( id="update-speed", options=[ - {"label": "3秒", "value": "3秒"}, - {"label": "10秒", "value": "10秒"}, - {"label": "不更新", "value": "不更新"} + {"label": "3s", "value": "3s"}, + {"label": "10s", "value": "10s"}, + {"label": "No Update", "value": "No Update"} ], - value="不更新", + value="No Update", clearable=False, style={"width": "150px"} ), @@ -172,8 +172,9 @@ def render_pie_chart(): html.A("NASDAQ 100 Index ETF", href="https://www.invesco.com/us/financial-products/etfs/product-detail?audienceType=Investor&productId=ETF-QQQM", className="text-primary"), # 使用 Iframe 显示 pyecharts 渲染的图表 html.Iframe(srcDoc=render_pie_chart(), style={"border": "0", "width": "100%", "height": "600px"}), - html.Div("Filter Criteria", className="text-muted"), + html.Div("Refresh Time", className="text-muted"), update_speed_dropdown, # 新增更新频率选择控件 + html.Div("Filter Criteria", className="text-muted"), filter_form, # 新增下载 CSV 按钮和下载组件 dbc.Button("Download CSV", id="download-csv-btn", color="primary", className="mb-3"), From 3f081bb723d479d2dda3f54ee21f7cd4f91288a5 Mon Sep 17 00:00:00 2001 From: kegao1995 Date: Thu, 13 Feb 2025 12:19:36 -0800 Subject: [PATCH 11/12] fixed environment.yml, add dash_bootstrap_components==1.7.1 --- environment.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/environment.yml b/environment.yml index c2be2f0..2b1b44d 100644 --- a/environment.yml +++ b/environment.yml @@ -7,11 +7,12 @@ channels: dependencies: - python=3.11 - dash=2.10.0 - - dash_bootstrap_components=1.7.1 - pandas=2.2.3 - numpy=1.26.4 - scikit-learn=1.3.2 - matplotlib=3.9.2 - jupyterlab=4.2.6 - pip=24.3 - - pyecharts=2.0.8 \ No newline at end of file + - pyecharts=2.0.8 + - pip: + - dash_bootstrap_components==1.7.1 \ No newline at end of file From 72b64f2a25864538bd53501ad09eecfde3d97664 Mon Sep 17 00:00:00 2001 From: kegao1995 Date: Fri, 14 Feb 2025 23:43:57 -0800 Subject: [PATCH 12/12] hide s&p 500 column --- src/layout.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/layout.py b/src/layout.py index 4be4df1..ea66ef9 100644 --- a/src/layout.py +++ b/src/layout.py @@ -56,7 +56,7 @@ def render_pie_chart(): html.Ul( [ html.Li(dbc.NavLink("NASDAQ 100", active=True, href="#")), - html.Li("S&P 500"), + #html.Li("S&P 500"), ] ), ],