使用 Python 构建股票分析仪表板的综合指南。 在过去的几周里,我使用 Python 的 Dash 库设计了一个股票价格分析面板。 我的主要布局灵感来自 Trading View 的网站。 面板需要显示一个烛台图表,其中包含一组要使用的指标以及用户选择的股票的一些其他额外细节。 所有这些信息都将由众所周知的 我们即将构建的视觉展示了一些公司在 2015 年 1 月 1 日至 2021 年 12 月 31 日期间的表现。 该项目的代码可以分为四个主要部分,按顺序排列: 1. 数据收集与清理 2.数据导入和默认图表 3. 应用布局 4. 交互性 加载所需的数据 首先,需要获取股票名称,以便 DataReader 可以在 Internet 上收集它们的数据。 为了完成这项工作,我发现了一个有趣的网页,其中显示了一个表格,其中包含公司的 BOVESPA 标识符以及它们所属的经济部门。 此阶段的代码包含项目“数据收集和清理”部分的一部分。 # These are the libaries to be used.import dashfrom dash import html, dcc, dash_table, callback_contextimport plotly.graph_objects as goimport dash_trich_components as dtcfrom dash.dependencies import Input, Outputimport dash_bootstrap_components as dbcimport plotly.express as pximport jsonimport pandas as pdfrom pandas_ta import bbandsimport pandas_datareader as webimport numpy as npimport datetimefrom scipy.stats import pearsonr# 1. Data Collection & Cleaning stocks = pd.read_html( 'https://blog./empresas-listadas-b3-bovespa', decimal=',')stocks_sectores = stocks[1]stocks_sectores.columns = stocks_sectores.iloc[0]stocks_sectores.drop(index=0, inplace=True)stocks_sectores_dict = stocks_sectores[[ 'Ticker', 'Setor']].to_dict(orient='list')# Implementing the economic sector names as the dictionay key and their stocks as values.d = {}for stock, sector in zip(stocks_sectores_dict['Ticker'], stocks_sectores_dict['Setor']): if sector not in d.keys(): d[sector] = [stock] else: d[sector].append(stock)# Correcting a tiny issue with the resulting dictionary: sometimes, two of its keys # were concerning the very same economic sector! Let's merge them into a single one.d['Bens Industriais'].extend(d['Bens industriais'])d.pop('Bens industriais')d['Bens Industriais']d['Energia Elétrica'].extend(d['Energia elétrica'])d.pop('Energia elétrica')d['Energia Elétrica']d['Aluguel de Veículos'].extend(d['Locação de veículos'])d.pop('Locação de veículos')d['Construção civil'].extend(d['Construção'])d.pop('Construção')d['Shopping Centers'].extend(d['Exploração de imóveis'])d.pop('Exploração de imóveis')# Now, we'll translate the economic sectors names to English for better understanding.translate = {'Alimentos': 'Food & Beverages', 'Varejo': 'Retail', 'Companhia aérea': 'Aerospace', 'Seguros': 'Insurace', 'Shopping Centers': 'Shopping Malls', 'Financeiro': 'Finance', 'Mineração': 'Mining', 'Químicos': 'Chemical', 'Educação': 'Education', 'Energia Elétrica': 'Electrical Energy', 'Viagens e lazer': 'Traveling', 'Construção civil': 'Real Estate', 'Saúde': 'Health', 'Siderurgia e Metalurgia': 'Steel & Metallurgy', 'Madeira e papel': 'Wood & Paper', 'Aluguel de Veículos': 'Vehicle Rental', 'Petróleo e Gás': 'Oil & Gas', 'Saneamento': 'Sanitation', 'Telecomunicações': 'Telecommunication', 'Tecnologia': 'Technology', 'Comércio': 'Commerce', 'Bens Industriais': 'Industrial Goods'}for key, value in translate.items(): d[value] = d.pop(key)# Unfortunately, some of the stocks listed in the dictionary cannot be accessed with yahoo's API. # Thus, we'll need to exclude them.for sector in list(d.keys()): for stock in d[sector]: # Trying to read the stock's data and removing it if it's not possible. try: web.DataReader(f'{stock}.SA', 'yahoo', '01-01-2015', '31-12-2021') except: d[sector].remove(stock) # After the removing process is completed, some economic sectors may not have # any stocks at all, so they won't be of use for the project. if d[sector] == []: d.pop(sector)# Converting it into a json filewith open('sector_stocks.json', 'w') as outfile: json.dump(d, outfile) 此操作的最终输出是一个名为“sector_stocks.json”的 JSON 文件,其键和值是经济部门名称及其各自的股票。 设置默认图表 正如您在视觉效果的 GIF 中所看到的,仪表板总共包含三个图表。 主要的是显示 OHLC 价格信息的烛台图。 另外两个显示了对公司过去 52 周业绩的一些见解。 折线图代表股票的每周平均价格,速度计显示当前价格与该期间达到的最小值和最大值之间的差距。 Dash 要求我们设置这些视觉效果的默认版本。 加载仪表板后,它们将立即显示。 考虑到这一点,我决定这些图表将按标准涵盖饮料行业巨头 AMBEV (ABEV3) 的股票数据。 请注意,以下代码构成了项目的“数据导入和默认图表”部分。
完成此任务后,我们终于可以开始创建仪表板了! 配置布局 仪表板的大部分组件都包含在两个 Bootstrap 列中。左侧部分占可用宽度的 75%,而右侧部分占据剩余空间。 然而,有一个元素不会放在这些分段中,即页面顶部的轮播。它显示了一些 BOVESPA 最相关股票的价格变化。 库存变化轮播 制作这个组件给我们带来了项目的技术限制之一。由于轮播需要显示一组股票的价格变化,它必须向 Yahoo Finance 的 API 发出多个请求。这会损害系统快速显示仪表板的速度。 考虑到这一点,我计划了一种方法,可以在不影响应用程序效率的情况下为我们提供我们想要的东西。 在创建“sector_stocks.json”文档时,还会生成第二个 JSON 文件。它将包含股票名称作为键,它们的价格变化作为值。由于仪表板的时间范围固定到 2021 年 12 月 1 日,因此不会给项目带来任何问题。 # 1. Data Collection & Cleaning (Continuance)# A function that is going to measure the stocks' price variation.def variation(name): df = web.DataReader(f'{name}.SA', 'yahoo', start='29-12-2021', end='30-12-2021') return 1-(df['Close'].iloc[-1] / df['Close'].iloc[-2])# Listing the companies to be shown in the Carousel.carousel_stocks = ['ITUB4', 'BBDC4', 'VALE3', 'PETR4', 'PETR3', 'ABEV3', 'BBAS3', 'B3SA3', 'ITSA4', 'CRFB3', 'CIEL3', 'EMBR3', 'JBSS3', 'MGLU3', 'PCAR3', 'SANB11', 'SULA11']# This dictionary will later be converted into a json file.carousel_prices = {}for stock in carousel_stocks: # Applying the 'variation' function for each of the list elements. carousel_prices[stock] = variation(stock)# Turning 'carousel_prices' into a json file.with open('carousel_prices.json', 'w') as outfile: json.dump(carousel_prices, outfile) 完成最后一个操作后,我们完成了脚本的第一部分。 是时候最终构建应用程序的布局了。
HTML Spans 显示股票名称及其价格变化。 仪表板的左侧部分 如前所述,应用程序的大部分内容都放在两个 Bootstrap 列中。 左边一个占可用宽度的 75%,另一个占据其余空间。 查看本文的 GIF 时,我们看到仪表板左侧的第一个功能是两个下拉菜单。 下拉菜单 这两个组件的目的是使用户能够选择将向其公开数据的公司。 首先,需要告知所需企业经营所在的经济部门(第一个下拉菜单)。 选择此选项后,第二个下拉列表会列出属于该行业的公司。 所需要做的就是单击想要的股票名称,仪表板将显示其数据。 但是由于我们没有设置所有的可视化组件,所以现在我们只处理两个下拉菜单之间的内部交互。 # 3. Application's Layout (Continuance)# Place the following code under the top carousel structure. # The column below will occupy 75% of the width available in the dashboard. dbc.Col([ # This row holds the dropdowns responsible for the selection of the stock # which informations are going to be displayed. dbc.Row([ # Both dropdowns are placed inside two Bootstrap columns with equal length. dbc.Col([ # A small title guiding the user on how to use the component. html.Label('Select the desired sector', style={'margin-left': '40px'}), # The economic sectors dropdown. It mentions all the ones that are # available in the 'sector_stocks' dictionary. dcc.Dropdown(options=[{'label': sector, 'value': sector} for sector in sorted(list(sector_stocks.keys()))], # The dashboard's default stock is ABEV3, which pertains # to the 'Food & Beverages' sector. So make sure # these informations are automatically displayed # when the dashboard is loaded. value='Food & Beverages', # It is essential to create an identification for the # components which will be used in interactivity operations. id='sectors-dropdown', style={'margin-left': '20px', 'width': '400px'}, searchable=False) ], width=6), # The column holding the stock names dropdown. dbc.Col([ # Nothing new here. Just using the same commands as above. html.Label('Select the stock to be displayed'), dcc.Dropdown( id='stocks-dropdown', value='ABEV3', style={'margin-right': '20px'}, searchable=False ) ], width=6) ]), # The left major column closing bracket ], width=9), # These are the dashboard's major dbc.Row and html.Div closing brackets. ]) ]) # 4. Interactivity# Allowing the stock dropdown to display the companies that are pertained to the # economic sector chosen.@ app.callback( # Observe how important the components id are to reference from where the function's # input comes from and in which element its output is used.@ app.callback( Output('stocks-dropdown', 'options'), Input('sectors-dropdown', 'value'))def modify_stocks_dropdown(sector): # The 'stocks-dropdown' dropdown values are the stocks that are part of the # selected economic domain. stocks = sector_stocks[sector] return stocks 上面的代码足以生成两个元素并激活它们的内部关系。 烛台图、时间跨度按钮和指标 本节将处理三个组件,因为将有一个方法来管理它们之间的关系。 烛台图是最相关的特征。 它显示了 pandas_datareader 的 DataReader 对象检索到的 OHLC 价格。 与交易视图一样,它的 x 轴长度可以使用时间跨度按钮进行编辑。 技术分析指标被列为清单元素; 当有人选择他们喜欢的那个时,必须在视觉上绘制其各自的线条 在这部分脚本中产生了多种交互。 我们必须耐心地创造它们。
完成这部分代码后,我们已经结束了面板的左侧。 仪表板的右侧部分 这整个区域暴露了一些关于股票及其各自经济部门的补充信息。 我们将要编写的代码比我们刚刚编写的代码要简单得多。 经济部门价格表 右上角的表格显示了在下拉列表中选择的属于经济领域的企业的当前价格。 这样一来,个人就能够想象公司在与竞争对手的竞争中表现如何。 # 3. Application's Layout (Continuance)# Beginning the Dashboard's right section. It should be under the panel's left-side code.# This other column occupies 25% of the dashboard's width. dbc.Col([ dbc.Row([ # The DataTable stores the prices from the companies that pertain # to the same economic sector as the one chosen in the dropdowns. dcc.Loading([ dash_table.DataTable( id='stocks-table', style_cell={'font_size': '12px', 'textAlign': 'center'}, style_header={'backgroundColor': 'black', 'padding-right': '62px', 'border': 'none'}, style_data={'height': '12px', 'backgroundColor': 'black', 'border': 'none'}, style_table={ 'height': '90px', 'overflowY': 'auto'}) ], id='loading-table', type='circle', color='#1F51FF') ], style={'margin-top': '28px'}), # The right major column closing bracket. ], width=3) # The dashboard's major dbc.Row and html.Div closing brackets. ])])# 4. Interactivity (Continuance)# The following function goes after the candlestick interactivity method.@ app.callback( Output('stocks-table', 'data'), Output('stocks-table', 'columns'), Input('sectors-dropdown', 'value'))# Updating the panel's DataTabledef update_stocks_table(sector): global sector_stocks # This DataFrame will be the base for the table. df = pd.DataFrame({'Stock': [stock for stock in sector_stocks[sector]], 'Close': [np.nan for i in range(len(sector_stocks[sector]))]}, index=[stock for stock in sector_stocks[sector]]) # Each one of the stock names and their respective prices are going to be stored # in the 'df' DataFrame. for stock in sector_stocks[sector]: stock_value = web.DataReader( f'{stock}.SA', 'yahoo', start='30-12-2021', end='31-12-2021')['Close'].iloc[-1] df.loc[stock, 'Close'] = f'R$ {stock_value :.2f}' # Finally, the DataFrame cell values are stored in a dictionary and its column # names in a list of dictionaries. return df.to_dict('records'), [{'name': i, 'id': i} for i in df.columns] update_stocks_table 可能看起来令人困惑,但它基本上需要返回两件事:填充字典中表格单元格的信息和字典列表中其列的名称。 有关 Dash 的 DataTable 工作原理的更多见解,请查看该库的文档。 当前价格卡 毫无疑问,这是最重要的家庭经纪人功能之一,此卡旨在显示所选公司的名称、当前价值以及与前一天相比的价格变化。
52 周数据轮播 这个轮播在面板右侧启动了 52 周信息区域。 该组件将包含一个显示公司每周平均价格的折线图和一个速度计,它基本上显示了当前值与该期间达到的最低和最高水平之间的距离。 与创建的任何其他事物一样,此结构需要与股票下拉列表中选择的值进行通信,以显示正确的信息。 但与这个项目中构建的其他不同,这个有两种交互方法,一种用于每个图表。 # 3. Application's Layout (Continuance)# This part of the script goes right under the price card construct.# In the section below, some informations about the stock's performance in the last # 52 weeks are going to be exposed. html.Hr(), html.H1( '52-Week Data', style={'font-size': '25px', 'text-align': 'center', 'color': 'grey', 'margin-top': '5px', 'margin-bottom': '0px'}), # Creating a Carousel showing the stock's weekly average price and a # Speedoemeter displaying how far its current price is from # the minimum and maximum values achieved. dtc.Carousel([ # By standard, the charts 'fig2' and 'fig3' made in the beginning # of the script are displayed. dcc.Graph(id='52-avg-week-price', figure=fig2), dcc.Graph(id='52-week-min-max', figure=fig3) ], slides_to_show=1, autoplay=True, speed=2000, style={'height': '220px', 'width': '310px', 'margin-bottom': '0px'}), # The right major column closing bracket. ], width=3) # The dashboard's major dbc.Row and html.Div closing brackets. ])])# 4. Interactivity (Continuance)# The function here is placed under the one dealing with the price card and# is responsible for updating the weekly average price chart.@app.callback( Output('52-avg-week-price', 'figure'), Input('stocks-dropdown', 'value'))def update_average_weekly_price(stock): # Receiving the stock's prices and measuring its average weekly price # in the last 52 weeks. df = web.DataReader(f'{stock}.SA', 'yahoo', start='2021-01-01', end='2021-12-31')['Close'] df_avg_52 = df.resample('W').mean().iloc[-52:] # Plotting the data in a line chart. fig2 = go.Figure() fig2.add_trace(go.Scatter(x=df_avg_52.index, y=df_avg_52.values)) fig2.update_layout( title={'text': 'Weekly Average Price', 'y': 0.9}, font={'size': 8}, plot_bgcolor='black', paper_bgcolor='black', font_color='grey', height=220, width=310, margin=dict(l=10, r=10, b=5, t=5), autosize=False, showlegend=False ) fig2.update_xaxes(tickformat='%m-%y', showticklabels=False, gridcolor='darkgrey', showgrid=False) fig2.update_yaxes(range=[df_avg_52.min()-1, df_avg_52.max()+1.5], showticklabels=False, gridcolor='darkgrey', showgrid=False) return fig2# This function will update the speedometer chart.@app.callback( Output('52-week-min-max', 'figure'), Input('stocks-dropdown', 'value'))def update_min_max(stock): # The same logic as 'update_average_weekly_price', but instead we are getting # the minimum and maximum prices reached in the last 52 weeks and comparing # them with the stock's current price. df = web.DataReader(f'{stock}.SA', 'yahoo', start='2021-01-01', end='2021-12-31')['Close'] df_avg_52 = df.resample('W').mean().iloc[-52:] df_52_weeks_min = df_avg_52.resample('W').min()[-52:].min() df_52_weeks_max = df_avg_52.resample('W').max()[-52:].max() current_price = df.iloc[-1] fig3 = go.Figure() fig3.add_trace(go.Indicator(mode='gauge+number', value=current_price, domain={'x': [0, 1], 'y': [0, 1]}, gauge={ 'axis': {'range': [df_52_weeks_min, df_52_weeks_max]}, 'bar': {'color': '#606bf3'}})) fig3.update_layout( title={'text': 'Min-Max Prices', 'y': 0.9}, font={'size': 8}, paper_bgcolor='black', font_color='grey', height=220, width=280, margin=dict(l=35, r=0, b=5, t=5), autosize=False, showlegend=False ) return fig3 我们即将结束仪表板的结构。 我们需要插入的只是股票与 BOVESPA 指数 (IBOVESPA) 及其经济领域平均价格的相关性。 股票的相关性 如前所述,这些最后的数字旨在提供与 IBOVESPA 及其经济部门同行相比企业绩效的一些观点。 要使用的统计量是 Pearson 的 r。
就是这样! 编写最后一段脚本将确保您拥有完整的股票数据分析仪表板! 结论 我真诚地希望您喜欢和我一起构建这个面板! |
|