適用于金融業(yè)的Python:股票投資組合分析(一)

分享 Kevin Boller如何利用一些非常有用的在線資源,Yahoo Finance API(需要解決,并且可能需要替換未來的數(shù)據(jù)源)和Jupyter notebook,以在很大程度上自動跟蹤和衡量股票投資組合相關情況 。

PME概述和個股票表現(xiàn)

簡述背景,作者從2002年開始投資他的股票投資組合,并在幾年前為其投資組合開發(fā)了一個財務模型。多年來,他會下載歷史價格,并將數(shù)據(jù)加載到財務模型中-雖然在線經(jīng)紀人計算已實現(xiàn)和未實現(xiàn)的回報,以及收入和股息,但在他進行自己的分析以評估時,喜歡在模型中包含歷史數(shù)據(jù)。他說有一種觀點/報告是從未從在線經(jīng)紀人和服務中找到的,那就是類似于“公開市場”的分析。簡而言之,公開市場等值(PME)是私募股權(quán)行業(yè)用來比較私募股權(quán)基金相對于行業(yè)基準表現(xiàn)的一組分析。具體參考https://docs.preqin.com/reports/Preqin-Special-Report-PME-July-2015.pdf

與此相關的是,絕大多數(shù)股票投資組合經(jīng)理無法選擇長期表現(xiàn)優(yōu)于大盤的股票組合,例如標準普爾500指數(shù)(約每20只積極管理的國內(nèi)基金中就有1只優(yōu)于指數(shù)基金)。即使一些個股表現(xiàn)優(yōu)異,其他個股表現(xiàn)不佳的股票往往會超過表現(xiàn)較好的股票,這意味著總體而言,投資者的境況比簡單地投資指數(shù)基金更糟糕。在商學院期間,作者了解了PME,在評估他當時持有的公共股票時,他在概念上也加入了類似的分析。要恰當?shù)刈龅竭@一點,我們應該衡量每個投資組合頭寸(持有期)相對于同一持有期內(nèi)標準普爾500指數(shù)等值美元投資的具體投資流入時間。舉個例子,如果你在2016年6月1日購買了一只股票,而且你仍然擁有它,你會想要將這只股票在那段時間的回報與2016年6月1日在標準普爾500指數(shù)(我們的基準例子)中同等美元投資的回報進行比較。在其他方面,你可能會發(fā)現(xiàn),即使一只股票表現(xiàn)相對較好,它在同一時間段內(nèi)仍可能落后于標準普爾500指數(shù)的回報。

過去,作者從雅虎財經(jīng)(Yahoo Finance)下載歷史價格數(shù)據(jù),并使用EXCEL中的指數(shù)和匹配函數(shù)來計算每個頭寸與標準普爾500指數(shù)的相對持有期表現(xiàn)。雖然這是實現(xiàn)這一目標的一種不錯的方式,但使用Jupyter notebook中的pandas進行同樣的操作更具伸縮性和可擴展性。無論何時下載新數(shù)據(jù)并加載到excel中,都不可避免地需要修改一些公式并驗證錯誤。使pandas,添加新的計算,例如累計ROI倍數(shù)(后面將介紹),幾乎不需要任何時間就能實現(xiàn)。并且使用Plotly的可視化效果具有很高的重現(xiàn)性,在預測方面也更有用。

聲明:這篇文章中的任何內(nèi)容都不應被視為投資建議。過去的表現(xiàn)不一定預示著未來的回報。這些是關于如何使用pandas導入不同時間間隔內(nèi)的一小部分股票樣本的數(shù)據(jù),以及如何根據(jù)指數(shù)對它們的個人表現(xiàn)進行基準測試的一般示例。你應該把所有與投資有關的問題都告訴你的財務顧問。

除了貢獻本教程之外,作者還在繼續(xù)修改和構(gòu)建此方法,并在本文末尾概述了進一步開發(fā)的一些注意事項。作者相信這個帖子會對面向數(shù)據(jù)科學的初學者和中級金融專業(yè)人員有幫助,特別是因為這應該延伸到許多其他類型的金融分析。這種方法在某種意義上是“類似PME的”,即它衡量的是相等持有期內(nèi)的投資流入。由于公開市場投資的流動性比私募股權(quán)高得多,假設你遵循的是落后止損方法,從作者的角度來看,更重要的是專注于積極持股-通常情況下,明智的做法是剝離表現(xiàn)遜于基準的持股,或者你出于各種原因不再想持有的持股,作者主張著眼長遠,只要他們表現(xiàn)優(yōu)異,作者就很高興長期持有這些份額的股票。

資源:作者現(xiàn)在是DataCamp的訂閱者(未來的帖子將在DataCamp上發(fā)布),他說這個關于Python for Finance的社區(qū)教程非常棒。
作者已經(jīng)為這篇文章創(chuàng)建了一個repo,包括這里的Python筆記本和這里的excel文件。
如果你想看到完整的交互式版本(因為jupyter <←>> gihub集成非常棒),你可以在這里使用nbviewer查看。https://nbviewer.jupyter.org/github/kdboller/pythonsp500/blob/b45eb79daf15d3c3032a8f398c207c9d2721ac19/Investment%20Portfolio%20Python%20Notebook_03_2018_blog%20example.ipynb

我們要完成的大綱:

導入標準普爾500指數(shù)和樣本股票代碼數(shù)據(jù),使用雅虎財經(jīng)API創(chuàng)建一個合并的投資組合‘主’文件,該文件將樣本投資組合數(shù)據(jù)與歷史股票代碼和歷史標準普爾500指數(shù)數(shù)據(jù)相結(jié)合,確定每項投資的收購日期標準普爾500指數(shù)的收盤價是多少,這使我們可以計算標準普爾500指數(shù)的等值股票position與相同美元投資的股票position計算投資組合position與標準普爾500指數(shù)在這段時間內(nèi)的相對百分比和美元價值回報,計算累計投資組合回報和ROI倍數(shù)。

為了評估這個示例投資組合與市場指數(shù)相比有多好,一個更重要的項目:動態(tài)計算每個position相對于落后止損的表現(xiàn),例如,如果一個position收盤比收盤高點低25%,考慮在下一個交易日賣出該position。

可視化

總回報比較-隨著時間的推移,每個position相對于指數(shù)基準累計回報的百分比回報-每個position相對于基準累積投資的美元收益/(虧損)
-鑒于上述情況,總體投資回報與標準普爾500指數(shù)投資的同等權(quán)重和時間段相比如何?調(diào)整后的高收盤價百分比比較-每個position最近的收盤價相對于其自買入以來的調(diào)整后收盤價是多少?

Data Import和Dataframe操作

從導入必要的Python庫開始,導入Plotly offline模塊,然后讀入我們的示例項目組合DataFrame。

# Import initial libraries
import pandas as pd
import numpy as np
import datetime
import matplotlib.pyplot as plt
import plotly.graph_objs as go
%matplotlib inline
# Imports in order to be able to use Plotly offline.
from plotly import __version__
from plotly.offline import download_plotlyjs, init_notebook_mode, plot, iplot
print(__version__) # requires version >= 1.9.0
init_notebook_mode(connected=True)
# Import the Sample worksheet with acquisition dates and initial cost basis:
portfolio_df = pd.read_excel('Sample stocks acquisition dates_costs.xlsx', sheet_name='Sample')
portfolio_df.head(10)

2020410訂正:sheetname 改為 sheet_name,可能由于時間過去2年,函數(shù)改了參數(shù)。上面在.xlsx文件下載鏈接:https://github.com/kdboller/pythonsp500

現(xiàn)在您已經(jīng)閱讀了示例投資組合文件,您將創(chuàng)建一些變量來捕捉標準普爾500指數(shù)和所有投資組合的股票代碼的日期范圍。請注意,這是此notebook為數(shù)不多的需要每周更新的方面之一(調(diào)整日期范圍以包括最近的交易周-在此,我們將此價格運行到2018年3月9日)。

# Date Ranges for SP 500 and for all tickers
# Modify these date ranges each week.
# The below will pull back stock prices from the start date until end date specified.
start_sp = datetime.datetime(2013, 1, 1)
end_sp = datetime.datetime(2018, 3, 9)
# This variable is used for YTD performance.
end_of_last_year = datetime.datetime(2017, 12, 29)
# These are separate if for some reason want different date range than SP.
stocks_start = datetime.datetime(2013, 1, 1)
stocks_end = datetime.datetime(2018, 3, 9)

正如Python Finance培訓帖子中所提到的,pandas-datareader包使我們能夠讀取來自Google, Yahoo! Finance and the World Bank。在這里,作者將重點介紹雅虎金融(Yahoo! Finance),雖然他已經(jīng)與Quantopian進行了非常初步的合作,也開始將quandl作為數(shù)據(jù)源進行研究。正如DataCamp帖子中也提到的,Yahoo API端點最近發(fā)生了更改,這需要安裝臨時修復程序才能支持Yahoo! Finance投入到工作中去。作者已經(jīng)在下面的代碼中做了需要的細微調(diào)整。作者注意到一些小的數(shù)據(jù)問題,數(shù)據(jù)并不總是像預期的那樣讀入,或者有時會錯過最后一個交易日。雖然這些問題相對較少發(fā)生,但他正在繼續(xù)關注Yahoo! Finance并且認為將是未來最好和最可靠的數(shù)據(jù)來源。

# Leveraged from the helpful Datacamp Python Finance trading blog post.
from pandas_datareader import data as pdr
import fix_yahoo_finance as yf
yf.pdr_override() # <== that's all it takes :-)
sp500 = pdr.get_data_yahoo('^GSPC', 
                           start_sp,
                             end_sp)
    
sp500.head()

訂正

*** `fix_yahoo_finance` was renamed to `yfinance`. ***
Please install and use `yfinance` directly using `pip install yfinance -U`

More information: https://github.com/ranaroussi/yfinance

如果你帶著你自己的notebook,一旦你成功地從雅虎的API讀取了數(shù)據(jù),你應該會看到類似下面的內(nèi)容:


1

加載標準普爾500數(shù)據(jù)后,您將看到作者檢查了dataframe的頭部和尾部,并將數(shù)據(jù)壓縮為只包括Adj Close列。Adjusted Close與Close columns之間的區(qū)別在于,Adjusted Close會反映紅利(請參閱下面的未來開發(fā)區(qū)域)。當一家公司發(fā)放股息時,股價會減去每股股息的大小,因為公司正在分配公司收益的一部分。出于此分析的目的,您只需要分析此列。作者還創(chuàng)建了一個dataframe,其中只包括標準普爾指數(shù)在2017年最后一天(2018年初)adjusted close;這是為了運行單個股票相對于標準普爾500指數(shù)表現(xiàn)的YTD比較。

在下面的代碼中,您將創(chuàng)建示例投資組合dataframe中所有自動收報機的數(shù)組。然后編寫一個函數(shù),將所有的股票代碼及其相關數(shù)據(jù)讀入一個新的dataframe中,這基本上與您對標準普爾500指數(shù)采取的方法相同,但適用于投資組合的所有股票代碼。

# Generate a dynamic list of tickers to pull from Yahoo Finance API based on the imported file with tickers.
tickers = portfolio_df['Ticker'].unique()
tickers
# Stock comparison code
def get(tickers, startdate, enddate):
    def data(ticker):
        return (pdr.get_data_yahoo(ticker, start=startdate, end=enddate))
    datas = map(data, tickers)
    return(pd.concat(datas, keys=tickers, names=['Ticker', 'Date']))
               
all_data = get(tickers, stocks_start, stocks_end)

與標準普爾500dataframe一樣,您將創(chuàng)建一個adj_close dataframe,它只包含所有股票代碼的Adj Closecolumn。如果您查看上面作者鏈接到的repo中的notebook,您會發(fā)現(xiàn)這段代碼包含的代碼塊比下面顯示的更多。為了在這里描述這一點,作者在下面包含了通向初始merged_portfolio dataframe的所有代碼。

# Also only pulling the ticker, date and adj. close columns for our tickers.
adj_close = all_data[['Adj Close']].reset_index()
adj_close.head()
# Grabbing the ticker close from the end of last year
adj_close_start = adj_close[adj_close['Date']==end_of_last_year]
adj_close_start.head()
# Grab the latest stock close price
adj_close_latest = adj_close[adj_close['Date']==stocks_end]
adj_close_latest
adj_close_latest.set_index('Ticker', inplace=True)
adj_close_latest.head()
# Set portfolio index prior to merging with the adj close latest.
portfolio_df.set_index(['Ticker'], inplace=True)
portfolio_df.head()
# Merge the portfolio dataframe with the adj close dataframe; they are being joined by their indexes.
merged_portfolio = pd.merge(portfolio_df, adj_close_latest, left_index=True, right_index=True)
merged_portfolio.head()
# The below creates a new column which is the ticker return; takes the latest adjusted close for each position
# and divides that by the initial share cost.
merged_portfolio['ticker return'] = merged_portfolio['Adj Close'] / merged_portfolio['Unit Cost'] - 1
merged_portfolio
merged_portfolio.head()

merged_portfolio

這取決于你對pandas的熟悉程度,這將是非常直接的到稍微令人不知所措的。下面,作者解釋了這些命令行在做的事情:

  • The overall approach you are taking is an example of split-apply-combine (note this downloads a PDF).
  • The all_data[['Adj Close']] line creates a new dataframe with only the columns provided in the list; here Adj Close is the only item provided in the list.
  • Using this line of code, adj_close[adj_close['Date']==end_of_last_year], you are filtering the adj_close dataframe to only the row where the data’s Date column equals the date which you earlier specified in the end_of_last_yearvariable (2017, 12, 29).
  • You also set the index of the adj_close_latest and portfolio_dfdataframes. I did this because this is how you’ll merge the two dataframes. The merge function, very similar to SQL joins, is an extremely useful function which I use very often.
  • Within the merge function, you specify the left dataframe ( portfolio_df ) and our right dataframe ( adj_close_latest ). By specifying left_index and right_index equal True, you are stating that the two dataframes share a common index and you will join both on this.
  • Last, you create a new column called 'ticker return' . This calculates the percent return for each stock position by dividing the Adj Close by the Unit Cost (initial purchase price for stock) and subtracting 1. This is similar to calculating a formula in excel and carrying it down, but in pandasthis is accomplished with one-line of code.
    你已經(jīng)獲得了標準普爾500指數(shù)和個股的單個數(shù)據(jù)框,你開始開發(fā)一個‘master’ dataframe,我們將使用它來進行計算、可視化和任何進一步的分析。接下來,將繼續(xù)在這個‘master’ dataframe上構(gòu)建,并進一步使用pandas merge 函數(shù)。下面,將重置當前dataframe的索引,并開始將較小的dataframe與‘master’ dataframe連接起來。下面的代碼塊再次在Jupyter notebook中進一步展開;在這里,作者采取與前面類似的方法,將共享下面的代碼,然后分解代碼塊下面的關鍵標注。
merged_portfolio.reset_index(inplace=True)
# Here we are merging the new dataframe with the sp500 adjusted closes since the sp start price based on 
# each ticker's acquisition date and sp500 close date.
merged_portfolio_sp = pd.merge(merged_portfolio, sp_500_adj_close, left_on='Acquisition Date', right_on='Date')
# .set_index('Ticker')
# We will delete the additional date column which is created from this merge.
# We then rename columns to Latest Date and then reflect Ticker Adj Close and SP 500 Initial Close.
del merged_portfolio_sp['Date_y']
merged_portfolio_sp.rename(columns={'Date_x': 'Latest Date', 'Adj Close_x': 'Ticker Adj Close'
                                    , 'Adj Close_y': 'SP 500 Initial Close'}, inplace=True)
# This new column determines what SP 500 equivalent purchase would have been at purchase date of stock.
merged_portfolio_sp['Equiv SP Shares'] = merged_portfolio_sp['Cost Basis'] / merged_portfolio_sp['SP 500 Initial Close']
merged_portfolio_sp.head()
# We are joining the developing dataframe with the sp500 closes again, this time with the latest close for SP.
merged_portfolio_sp_latest = pd.merge(merged_portfolio_sp, sp_500_adj_close, left_on='Latest Date', right_on='Date')
# Once again need to delete the new Date column added as it's redundant to Latest Date.  
# Modify Adj Close from the sp dataframe to distinguish it by calling it the SP 500 Latest Close.
del merged_portfolio_sp_latest['Date']
merged_portfolio_sp_latest.rename(columns={'Adj Close': 'SP 500 Latest Close'}, inplace=True)
merged_portfolio_sp_latest.head()
  • 可以在merged_portfolio上使用reset_index,以便扁平化主數(shù)據(jù)框并連接較小的數(shù)據(jù)框的相關列。
  • merged_portfolio_sp行中,您可以將當前主數(shù)據(jù)框(merged_portfolio)與sp_500_adj_close合并;這樣做是為了使標準普爾在每個position的采購日期的收盤價-這允許您跟蹤每個position持有的同一時間期(從收購日期到最近的市場收盤日期)的標準普爾業(yè)績。
  • 這里的合并與以前略有不同,因為我們在左側(cè)數(shù)據(jù)框的Acquisition Date列和右側(cè)數(shù)據(jù)框的Date列上連接。
  • 完成此合并后,您將擁有不需要的額外列-由于我們的主數(shù)據(jù)框最終將有相當數(shù)量的列用于分析,因此在此過程中刪除重復和不必要的列非常重要。
  • 有幾種方法可以刪除不必要的列并執(zhí)行各種列名清理;為簡單起見,作者使用python del,然后使用pandas rename方法重命名幾個列,通過重命名為Ticker Adj Close來澄清股票代碼的Adj Close列;您可以使用SP 500 Initial Close來區(qū)分標準普爾指數(shù)的初始調(diào)整收盤。
  • 在計算merged_portfolio_sp['Equiv SP Shares']時,這樣做是為了能夠計算標準普爾500指數(shù)在收購每個股票position的當天收盤時的等價值:如果你花5,000美元購買一個新的股票position,你可以花5,000美元購買標準普爾500指數(shù);繼續(xù)這個例子,如果標準普爾500指數(shù)在購買時的交易價格是每股2,500美元,你就可以購買2股股票。稍后,如果標準普爾500指數(shù)的交易價格為每股3,000美元,你的股份將價值6,000美元(2股等值股票*每股3,000美元),在這段可比的時間段內(nèi),你將獲得1,000美元的賬面利潤。
  • 在代碼塊的其余部分,接下來執(zhí)行類似的合并,這一次加入了標準普爾500指數(shù)的最新收盤價-這提供了計算標準普爾500指數(shù)相對于每個position持有期的可比回報所需的第二個部分:每個股票收購日的標準普爾500指數(shù)價格和標準普爾500指數(shù)的最新收盤價。

現(xiàn)在,您已經(jīng)使用以下內(nèi)容進一步開發(fā)了您的主數(shù)據(jù)框:

  • 每個投資組合position的價格、股票和position收購日的價值,以及最新的市場收盤價。
  • 每個股票的等值position收購日的等值標準普爾500指數(shù)價格、股票和價值,以及標準普爾500指數(shù)的最新收盤價。

鑒于上述情況,接下來您將執(zhí)行必要的計算,以相對于標準普爾500指數(shù)的可比美元投資和持有時間,比較每個position的表現(xiàn),以及這一策略/一籃子股票的整體表現(xiàn)。

下面是您要添加到“主”數(shù)據(jù)框中的新列的摘要。

  • 在第一列['SP Return']中,您創(chuàng)建了一個列,該列計算標準普爾500指數(shù)在每個position持有期間的絕對百分比回報(注意,這是絕對回報,而不是年化回報)。在第二列(['Abs. Return Compare'])中,您將在同一時間段內(nèi)將['ticker return'](每個位置的回報)與['SP Return']進行比較。
  • 在接下來的三欄['Ticker Share Value'], ['SP 500 Value'] and ['Abs Value Compare']中,我們根據(jù)我們持有的股票乘以最新調(diào)整后的收盤價來計算美元價值(市值)等值(并從股票代碼中減去標準普爾回報,以計算超出/(低于)業(yè)績)。
  • 最后,['Stock Gain / (Loss)']['SP 500 Gain / (Loss)']列計算我們每個position的未實現(xiàn)美元收益/虧損和可比的標準普爾500收益/虧損;這使我們能夠比較每個position的價值影響與簡單地將這些美元投資于標準普爾500指數(shù)。
# Percent return of SP from acquisition date of position through latest trading day.
merged_portfolio_sp_latest['SP Return'] = merged_portfolio_sp_latest['SP 500 Latest Close'] / merged_portfolio_sp_latest['SP 500 Initial Close'] - 1
# This is a new column which takes the tickers return and subtracts the sp 500 equivalent range return.
merged_portfolio_sp_latest['Abs. Return Compare'] = merged_portfolio_sp_latest['ticker return'] - merged_portfolio_sp_latest['SP Return']
# This is a new column where we calculate the ticker's share value by multiplying the original quantity by the latest close.
merged_portfolio_sp_latest['Ticker Share Value'] = merged_portfolio_sp_latest['Quantity'] * merged_portfolio_sp_latest['Ticker Adj Close']
# We calculate the equivalent SP 500 Value if we take the original SP shares * the latest SP 500 share price.
merged_portfolio_sp_latest['SP 500 Value'] = merged_portfolio_sp_latest['Equiv SP Shares'] * merged_portfolio_sp_latest['SP 500 Latest Close']
# This is a new column where we take the current market value for the shares and subtract the SP 500 value.
merged_portfolio_sp_latest['Abs Value Compare'] = merged_portfolio_sp_latest['Ticker Share Value'] - merged_portfolio_sp_latest['SP 500 Value']
# This column calculates profit / loss for stock position.
merged_portfolio_sp_latest['Stock Gain / (Loss)'] = merged_portfolio_sp_latest['Ticker Share Value'] - merged_portfolio_sp_latest['Cost Basis']
# This column calculates profit / loss for SP 500.
merged_portfolio_sp_latest['SP 500 Gain / (Loss)'] = merged_portfolio_sp_latest['SP 500 Value'] - merged_portfolio_sp_latest['Cost Basis']
merged_portfolio_sp_latest.head()

現(xiàn)在你有了需要的東西,可以將你的投資組合的表現(xiàn)與同等投資于標準普爾500指數(shù)的投資組合進行比較。接下來的兩個代碼塊部分允許您:i)比較每個position相對于標準普爾500指數(shù)的YTD表現(xiàn)(衡量動能和您的position的走勢);ii)比較每個投資組合position的最新收盤價與其最近的收盤高點(這允許您評估position是否觸發(fā)了跟蹤止損,例如,收盤時比收盤高點低25%)。
下面,作者將從YTD performance代碼塊開始,并在下面進一步提供有關代碼的詳細信息。

# Merge the overall dataframe with the adj close start of year dataframe for YTD tracking of tickers.
merged_portfolio_sp_latest_YTD = pd.merge(merged_portfolio_sp_latest, adj_close_start, on='Ticker')
# , how='outer'
# Deleting date again as it's an unnecessary column.  Explaining that new column is the Ticker Start of Year Close.
del merged_portfolio_sp_latest_YTD['Date']
merged_portfolio_sp_latest_YTD.rename(columns={'Adj Close': 'Ticker Start Year Close'}, inplace=True)
# Join the SP 500 start of year with current dataframe for SP 500 ytd comparisons to tickers.
merged_portfolio_sp_latest_YTD_sp = pd.merge(merged_portfolio_sp_latest_YTD, sp_500_adj_close_start
                                             , left_on='Start of Year', right_on='Date')
# Deleting another unneeded Date column.
del merged_portfolio_sp_latest_YTD_sp['Date']
# Renaming so that it's clear this column is SP 500 start of year close.
merged_portfolio_sp_latest_YTD_sp.rename(columns={'Adj Close': 'SP Start Year Close'}, inplace=True)
# YTD return for portfolio position.
merged_portfolio_sp_latest_YTD_sp['Share YTD'] = merged_portfolio_sp_latest_YTD_sp['Ticker Adj Close'] / merged_portfolio_sp_latest_YTD_sp['Ticker Start Year Close'] - 1
# YTD return for SP to run compares.
merged_portfolio_sp_latest_YTD_sp['SP 500 YTD'] = merged_portfolio_sp_latest_YTD_sp['SP 500 Latest Close'] / merged_portfolio_sp_latest_YTD_sp['SP Start Year Close'] - 1
  • 在創(chuàng)建merged_portfolio_sp_latest_YTD數(shù)據(jù)框時,您現(xiàn)在正在將主數(shù)據(jù)框與adj_close_start數(shù)據(jù)框合并;作為提醒,您是通過篩選adj_close數(shù)據(jù)框創(chuàng)建此數(shù)據(jù)框的,其中'Date'列等于變量end_of_last_year;這樣做是因為YTD(年初至今)股票和指數(shù)表現(xiàn)是通過它來衡量的;去年的結(jié)束收盤價是下一年的結(jié)束收盤價;您這樣做是因為它是衡量YTD(年初至今)股票和指數(shù)表現(xiàn)的方式;去年的結(jié)束收盤價就是下一年的結(jié)束收盤價;您這樣做是因為它是衡量YTD(年初至今)股票和指數(shù)表現(xiàn)的方式;去年的結(jié)束收盤價是次年的結(jié)束日期.
  • 在這里,我們再次使用del刪除不必要的列,并使用rename方法來澄清主數(shù)據(jù)框新添加的列。
  • 最后,我們提取每個Ticker(在['Ticker Adj Close']列中),并計算每個Ticker的YTD回報(SP 500 Latest Close列中的每個值也有一個標準普爾500等效值)。

在下面的代碼塊中,您使用sort_values方法對‘主’數(shù)據(jù)框重新排序,然后計算累計投資組合投資(持倉成本之和),以及投資組合頭寸的累計價值和理論標準普爾500指數(shù)投資的累計價值。這使你能夠看到你的總投資組合(在整個時期的不同時間對頭寸進行的投資)與簡單地投資于指數(shù)的策略進行了整體比較。稍后,您將使用['Cum Ticker ROI Mult']來幫助您可視化每項投資對總體投資回報(ROI)的貢獻或減少程度。

merged_portfolio_sp_latest_YTD_sp = merged_portfolio_sp_latest_YTD_sp.sort_values(by='Ticker', ascending=True)
# Cumulative sum of original investment
merged_portfolio_sp_latest_YTD_sp['Cum Invst'] = merged_portfolio_sp_latest_YTD_sp['Cost Basis'].cumsum()
# Cumulative sum of Ticker Share Value (latest FMV based on initial quantity purchased).
merged_portfolio_sp_latest_YTD_sp['Cum Ticker Returns'] = merged_portfolio_sp_latest_YTD_sp['Ticker Share Value'].cumsum()
# Cumulative sum of SP Share Value (latest FMV driven off of initial SP equiv purchase).
merged_portfolio_sp_latest_YTD_sp['Cum SP Returns'] = merged_portfolio_sp_latest_YTD_sp['SP 500 Value'].cumsum()
# Cumulative CoC multiple return for stock investments
merged_portfolio_sp_latest_YTD_sp['Cum Ticker ROI Mult'] = merged_portfolio_sp_latest_YTD_sp['Cum Ticker Returns'] / merged_portfolio_sp_latest_YTD_sp['Cum Invst']
merged_portfolio_sp_latest_YTD_sp.head()

您現(xiàn)在已經(jīng)接近最后沖刺階段,幾乎準備好開始可視化您的數(shù)據(jù),并評估您的投資組合的單個報價器和整體策略性能的優(yōu)勢和劣勢。
與前面一樣,作者已經(jīng)包含了主代碼塊,用于確定倉位相對于最近收盤高點的交易位置;然后,并將在下面進一步解釋代碼。

# Need to factor in that some positions were purchased much more recently than others.
# Join adj_close dataframe with portfolio in order to have acquisition date.
portfolio_df.reset_index(inplace=True)
adj_close_acq_date = pd.merge(adj_close, portfolio_df, on='Ticker')
# delete_columns = ['Quantity', 'Unit Cost', 'Cost Basis', 'Start of Year']
del adj_close_acq_date['Quantity']
del adj_close_acq_date['Unit Cost']
del adj_close_acq_date['Cost Basis']
del adj_close_acq_date['Start of Year']
# Sort by these columns in this order in order to make it clearer where compare for each position should begin.
adj_close_acq_date.sort_values(by=['Ticker', 'Acquisition Date', 'Date'], ascending=[True, True, True], inplace=True)
# Anything less than 0 means that the stock close was prior to acquisition.
adj_close_acq_date['Date Delta'] = adj_close_acq_date['Date'] - adj_close_acq_date['Acquisition Date']
adj_close_acq_date['Date Delta'] = adj_close_acq_date[['Date Delta']].apply(pd.to_numeric)
# Modified the dataframe being evaluated to look at highest close which occurred after Acquisition Date (aka, not prior to purchase).
adj_close_acq_date_modified = adj_close_acq_date[adj_close_acq_date['Date Delta']>=0]
# This pivot table will index on the Ticker and Acquisition Date, and find the max adjusted close.
adj_close_pivot = adj_close_acq_date_modified.pivot_table(index=['Ticker', 'Acquisition Date'], values='Adj Close', aggfunc=np.max)
adj_close_pivot.reset_index(inplace=True)
# Merge the adj close pivot table with the adj_close table in order to grab the date of the Adj Close High (good to know).
adj_close_pivot_merged = pd.merge(adj_close_pivot, adj_close
                                             , on=['Ticker', 'Adj Close'])
# Merge the Adj Close pivot table with the master dataframe to have the closing high since you have owned the stock.
merged_portfolio_sp_latest_YTD_sp_closing_high = pd.merge(merged_portfolio_sp_latest_YTD_sp, adj_close_pivot_merged
                                             , on=['Ticker', 'Acquisition Date'])
# Renaming so that it's clear that the new columns are closing high and closing high date.
merged_portfolio_sp_latest_YTD_sp_closing_high.rename(columns={'Adj Close': 'Closing High Adj Close', 'Date': 'Closing High Adj Close Date'}, inplace=True)
merged_portfolio_sp_latest_YTD_sp_closing_high['Pct off High'] = merged_portfolio_sp_latest_YTD_sp_closing_high['Ticker Adj Close'] / merged_portfolio_sp_latest_YTD_sp_closing_high['Closing High Adj Close'] - 1 
merged_portfolio_sp_latest_YTD_sp_closing_high

這是一個相當重大的提升,現(xiàn)在是我們期待已久的可視化的時候了。如果您在自己的筆記本中繼續(xù)跟蹤,您現(xiàn)在就有了一個非常豐富的數(shù)據(jù)框架,其中包含許多經(jīng)過計算的投資組合指標,如下圖所示:


部分未顯示完全

可視化

# Ploty is an outstanding resource for interactive charts.

trace1 = go.Bar(
    x = merged_portfolio_sp_latest_YTD_sp['Ticker'][0:10],
    y = merged_portfolio_sp_latest_YTD_sp['Share YTD'][0:10],
    name = 'Ticker YTD')

trace2 = go.Scatter(
    x = merged_portfolio_sp_latest_YTD_sp['Ticker'][0:10],
    y = merged_portfolio_sp_latest_YTD_sp['SP 500 YTD'][0:10],
    name = 'SP500 YTD')
    
data = [trace1, trace2]

layout = go.Layout(title = 'YTD Return vs S&P 500 YTD'
    , barmode = 'group'
    , yaxis=dict(title='Returns', tickformat=".2%")
    , xaxis=dict(title='Ticker')
    , legend=dict(x=.8,y=1)
    )

fig = go.Figure(data=data, layout=layout)
iplot(fig)
YTD Return vs S&P 500 YTD
# Current Share Price versus Closing High Since Purchased

trace1 = go.Bar(
    x = merged_portfolio_sp_latest_YTD_sp_closing_high['Ticker'][0:10],
    y = merged_portfolio_sp_latest_YTD_sp_closing_high['Pct off High'][0:10],
    name = 'Pct off High')
    
data = [trace1]

layout = go.Layout(title = 'Adj Close % off of High'
    , barmode = 'group'
    , yaxis=dict(title='% Below Adj Close High', tickformat=".2%")
    , xaxis=dict(title='Ticker')
    , legend=dict(x=.8,y=1)
    )

fig = go.Figure(data=data, layout=layout)
iplot(fig)
Adj Close % off of High

Total Return Comparison Charts

trace1 = go.Bar(
    x = merged_portfolio_sp_latest_YTD_sp_closing_high['Ticker'][0:10],
    y = merged_portfolio_sp_latest_YTD_sp_closing_high['ticker return'][0:10],
    name = 'Ticker Total Return')

trace2 = go.Scatter(
    x = merged_portfolio_sp_latest_YTD_sp_closing_high['Ticker'][0:10],
    y = merged_portfolio_sp_latest_YTD_sp_closing_high['SP Return'][0:10],
    name = 'SP500 Total Return')
    
data = [trace1, trace2]

layout = go.Layout(title = 'Total Return vs S&P 500'
    , barmode = 'group'
    , yaxis=dict(title='Returns', tickformat=".2%")
    , xaxis=dict(title='Ticker', tickformat=".2%")
    , legend=dict(x=.8,y=1)
    )

fig = go.Figure(data=data, layout=layout)
iplot(fig)
Total Return vs S&P 500

Cumulative Returns Over Time

trace1 = go.Bar(
    x = merged_portfolio_sp_latest_YTD_sp_closing_high['Ticker'][0:10],
    y = merged_portfolio_sp_latest_YTD_sp_closing_high['Stock Gain / (Loss)'][0:10],
    name = 'Ticker Total Return ($)')

trace2 = go.Bar(
    x = merged_portfolio_sp_latest_YTD_sp_closing_high['Ticker'][0:10],
    y = merged_portfolio_sp_latest_YTD_sp_closing_high['SP 500 Gain / (Loss)'][0:10],
    name = 'SP 500 Total Return ($)')

trace3 = go.Scatter(
    x = merged_portfolio_sp_latest_YTD_sp_closing_high['Ticker'][0:10],
    y = merged_portfolio_sp_latest_YTD_sp_closing_high['ticker return'][0:10],
    name = 'Ticker Total Return %',
    yaxis='y2')

data = [trace1, trace2, trace3]

layout = go.Layout(title = 'Gain / (Loss) Total Return vs S&P 500'
    , barmode = 'group'
    , yaxis=dict(title='Gain / (Loss) ($)')
    , yaxis2=dict(title='Ticker Return', overlaying='y', side='right', tickformat=".2%")
    , xaxis=dict(title='Ticker')
    , legend=dict(x=.75,y=1)
    )

fig = go.Figure(data=data, layout=layout)
iplot(fig)

Gain / (Loss) Total Return vs S&P 500交互圖
trace1 = go.Bar(
    x = merged_portfolio_sp_latest_YTD_sp_closing_high['Ticker'],
    y = merged_portfolio_sp_latest_YTD_sp_closing_high['Cum Invst'],
    # mode = 'lines+markers',
    name = 'Cum Invst')

trace2 = go.Bar(
    x = merged_portfolio_sp_latest_YTD_sp_closing_high['Ticker'],
    y = merged_portfolio_sp_latest_YTD_sp_closing_high['Cum SP Returns'],
    # mode = 'lines+markers',
    name = 'Cum SP500 Returns')

trace3 = go.Bar(
    x = merged_portfolio_sp_latest_YTD_sp_closing_high['Ticker'],
    y = merged_portfolio_sp_latest_YTD_sp_closing_high['Cum Ticker Returns'],
    # mode = 'lines+markers',
    name = 'Cum Ticker Returns')

trace4 = go.Scatter(
    x = merged_portfolio_sp_latest_YTD_sp_closing_high['Ticker'],
    y = merged_portfolio_sp_latest_YTD_sp_closing_high['Cum Ticker ROI Mult'],
    # mode = 'lines+markers',
    name = 'Cum ROI Mult'
    , yaxis='y2')


data = [trace1, trace2, trace3, trace4]

layout = go.Layout(title = 'Total Cumulative Investments Over Time'
    , barmode = 'group'
    , yaxis=dict(title='Returns')
    , xaxis=dict(title='Ticker')
    , legend=dict(x=.4,y=1)
    , yaxis2=dict(title='Cum ROI Mult', overlaying='y', side='right')               
    )

fig = go.Figure(data=data, layout=layout)
iplot(fig)
Total Cumulative Investments Over Time

References:

https://www.datacamp.com/community/tutorials/finance-python-trading

https://github.com/datacamp/datacamp-community-tutorials/blob/master/Python%20Finance%20Tutorial%20For%20Beginners/Python%20For%20Finance%20Beginners%20Tutorial.ipynb

https://pypi.python.org/pypi/fix-yahoo-finance

http://www.learndatasci.com/python-finance-part-yahoo-finance-api-pandas-matplotlib/

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,321評論 6 543
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,559評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,442評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,835評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,581評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,922評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,931評論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 43,096評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,639評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 41,374評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,591評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,104評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,789評論 3 349
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,196評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,524評論 1 295
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,554評論 2 379

推薦閱讀更多精彩內(nèi)容

  • 金融知識大全(最全整理版) 本文實戰(zhàn)財經(jīng) 轉(zhuǎn)自 優(yōu)維金融空間,轉(zhuǎn)載請注明來源! 第一部分:銀行系金融知識大全! 1...
    cnnjzc閱讀 6,505評論 0 14
  • 通過第一、二章的學習,對于投資新手來說,已經(jīng)認識和理解下面兩點: 1. 什么才是真正的資產(chǎn),能夠為我們“生錢”的就...
    CheneyChe閱讀 539評論 1 5
  • 今天是什么日子 起床:5點10分 就寢:11點 天氣:晴–7---2°C PM37 任務清單 昨日完成的任務,最...
    simon_5c61閱讀 121評論 0 0
  • 我了解大多數(shù)人迷惑的眼神,并非因為上帝的遮蔽;也不是睡夢尚未離去。 那是眼前的一塊幕布,它阻擋了原本明亮的雙眸。 ...
    反百閱讀 304評論 2 2
  • 看著每天做任務都有借鉆,每天都忙的不亦樂乎。 任務里有昨天總收益,累計收益,但是這個累計收益的量遠遠大于目前本身擁...
    河水徜徉閱讀 282評論 1 3