用 Pandas 进行金融市场数据分析

3,074 阅读26分钟

Pandas是对金融市场数据进行时间序列分析的一个伟大工具。因为pandasDataFrames和Series 与基于日期/时间的指数工作得很好,它们可以有效地用于分析历史数据。我所说的金融市场数据是指公开交易的金融工具的历史价格信息等数据。然而,任何种类的历史金融信息都可以被分析。

时间序列数据将以不同的频率提供。有些金融工具的流动性很强,我们几乎可以在任何时间点查看价格。价格或其他一些特征几乎不断变化。一个工具具有流动性的想法意味着在正常的交易时间内,有愿意交易的买家和卖家准备与之交易。他们愿意购买和出售足够的工具来满足你的交易需求。对于在交易所交易的工具,买家和卖家愿意支付或提供的价格在该工具的正常交易时间内都可以得到。

由于大量的金融数据是可用的,其中大部分是免费的,所以它是学习更多关于分析和处理时间序列数据的绝佳场所。通过这些数据,我们可以学到很多关于公共市场和熊猫的东西。在这篇文章中,我将指导你如何做一些简单的分析。在这个过程中,我们可能会发现一些关于股票市场和熊猫的事情。

对于这篇文章,我们将看看金融市场中常见的一个效应:季节性。季节性是时间序列数据的一个特点,在这里可以看到基于时间的规律性影响。它可能是一年的时间,一个月的时间,一天的时间,甚至是每小时的分钟。我将向你展示如何下载一些数据(在这种情况下是每日股票数据),然后使用pandas和matplotlib来验证数据是否看起来合理。然后,我们将调查数据中是否存在每月的季节性。在这一过程中,我将浏览一下代码,并提供一些指针,如果你有兴趣的话,可以去了解更多。

在未来的文章中,我将研究在每日(每天一个数据点)和日内(每天许多数据点)数据中看到的一些其他有趣的效果。

获取数据

处理市场数据的最大挫折之一是首先要掌握它。金融数据可能是有价值的,因此拥有这些数据的人通常会对其收费。数据在广度、时间和质量等多个方面变得更有价值。

数据的广度

在广度方面,我们有关于一个金融工具的信息。考虑一个这样的金融工具,一个公司的公开交易的普通股。我们可以获取该股票的价格,但哪个价格?对于每日的股票价格数据,这个价格通常是该股票在16:00美国/纽约的收盘价。但我们也可能想知道该股票在美国/纽约9:30开盘时的价格。

也许我们还想知道当天的最高价格和最低价格。最后,我们可能想知道在整个一天的过程中,有多少股交易(称为成交量)。在这个每日数据的例子中,广度维度包含5个信息:最后交易价格(或收盘价),第一个交易价格(或开盘价),最高交易价格(高)和最低交易价格(低),以及当天的总成交量。

数据越详细,成本就越高。除了交易价格,我们还可以得到买入和卖出价格。这些是有人愿意为一个给定的工具支付(出价)或出售(要价)的价格。除了价格,我们可能想知道买方要求的股票数量(出价大小)或卖方的报价(要价大小)。这些数据可以在极其细微的层次上提供(通常称为tick数据),也可以汇总成摘要数据(通常称为条形)。

我们何时和多长时间得到更新,以及多少历史数据?

让我们考虑一下时间维度。我们处理的是每日数据,但我们能得到多少天的数据?你能得到的历史数据越多,对于分析过去的趋势和影响就越有价值,所以我们要尽可能地回到过去,在市场上寻找信息。

现在我们也可以通过缩小时间维度来改变它。与其每天看数据,我们可以每小时看一次,或每15分钟看一次,或每分钟看一次。我们甚至可能想看每一次的数据更新。事实证明,每天的数据是很容易得到的,但更细化的数据却很难免费得到。

在时间维度上,我们还应该考虑数据交付的速度。对于这篇文章,我们只看历史数据,但对于软件或人需要对数据做出反应的用例,数据越早到达,就越有价值。

数据的质量如何?

质量对于金融数据也非常重要。如果数据缺失或不准确,可能会产生不正确的结果。通常情况下,数据分析会确保数据是合理的,但检测所有的错误可能是非常困难的。

一个好的数据要看一看

现在,让我们先来看看股票代码为SPY的交易所交易基金(ETF)的一些历史数据。SPY有点特别。SPY不是一家公司的股票,而是一种特殊的工具,大致反映了整个标普500指数的价值。这个指数中有大约500家最大的美国上市公司的股票,而且这个指数会定期更新。如果公司是盈利的(大部分),并且大致在市值前500名的公司,就会被纳入该指数。ETF的价格在一个正常的交易日期间,将以综合股票乘以其指数权重的相同速度上下波动(大致如此)。我们现在使用它的主要原因是,它是一个很好的方式,可以(免费)粗略了解美国股市在一年中任何一天的表现。它还具有高度的流动性,所以准确的价格几乎总是可用的,许多市场参与者愿意在任何时候购买和出售它。

一个数据源

获取历史价格数据有多种途径,我以前写过 一些。对于这篇文章,我将使用Alpha Vantage,一个金融市场数据的供应商。你可以获得一个免费的API密钥,自己跟着做。

请注意,我不会在这里(或其他地方)向你提供这些数据,因为我并不拥有这些数据。一般来说,当你想与金融数据打交道时,你将需要找到一家被授权向你提供这些信息的公司。历史上越是完整、准确和精确的信息,你将期望必须支付更多的费用来获得这些信息的使用。就本文而言,这些数据是相当普遍的,所以你可以免费获得它。

下载历史数据

我会在Alpha Vantage允许的范围内获得尽可能多的SPY ETF的历史数据。你可以阅读Alpha Vantage的文档,看看其他的选项,但现在,我们只是查询他们的每日时间序列数据。

import io

import pandas as pd
import requests

import matplotlib.pyplot as plt
API_KEY='demo' # replace with your API_KEY from Alpha Vantage

数据可以用CSV(逗号分隔值)或JSON(JavaScript对象符号)格式从Alpha Vantage获取。 我们先来看看CSV。你通过对他们的服务端点进行HTTP调用来获取结果。如果我们按照他们的建议使用requests 库,结果会以字节的形式存储在content 变量中。我们可以把它变成一个字符串,然后检查前几行,看看数据是什么样子。

# full history of daily data
function = "TIME_SERIES_DAILY_ADJUSTED" # daily data
outputsize = "full"                     # all of it
datatype = "csv"                        # CSV - comma separated values
url = f"https://www.alphavantage.co/query?function={function}&symbol=SPY&outputsize=full&apikey={API_KEY}&datatype={datatype}"
res = requests.get(url)

# the CSV file content is all available in the reponse
res.content.decode().split("\r\n")[0:2]
['timestamp,open,high,low,close,adjusted_close,volume,dividend_amount,split_coefficient', '2021-11-29,464.07,466.56,461.73,464.6,464.6,82666545,0.0000,1.0']

由于该服务首先返回最近的数据,我们将使用pandas解析csv,并确保我们对索引进行排序,以便最早的数据在我们的DataFrame 。我们需要给read_csv 一些提示,告诉它将第一列解析为日期时间数据,然后将该列作为DataFrame 的索引。

因为最近的一行是第一个元素,我们也将通过它的索引对DataFrame 进行排序,因为我们希望数据按时间顺序排列。

注意我在这里也使用了io.StringIO ,因为我已经把数据都放在了内存中。稍后你会看到,如果我们想的话,完全可以在pandas中更快地完成这一切。

你有关于pandas索引的问题吗?你可以从这里开始了解他们的一切。

spy_daily = pd.read_csv(io.StringIO(res.content.decode()),
                        parse_dates=['timestamp'],
                        index_col='timestamp').sort_index()
spy_daily.head()
                  open        high         low       close  adjusted_close  \
timestamp                                                                    
1999-11-01  136.500000  137.000000  135.562500  135.562500       90.318326   
1999-11-02  135.968704  137.250000  134.593704  134.593704       89.672867   
1999-11-03  136.000000  136.375000  135.125000  135.500000       90.276685   
1999-11-04  136.750000  137.359299  135.765594  136.531204       90.963724   
1999-11-05  138.625000  139.109299  136.781204  137.875000       91.859026   

             volume  dividend_amount  split_coefficient  
timestamp                                                
1999-11-01  4006500              0.0                1.0  
1999-11-02  6516900              0.0                1.0  
1999-11-03  7222300              0.0                1.0  
1999-11-04  7907500              0.0                1.0  
1999-11-05  7431500              0.0                1.0  

我们可以看到,Alpha Vantage给我们提供的数据可以一直追溯到1999年,不错!我们有20多年的数据。拥有超过20年的数据是很有帮助的。

正如我之前所说,我们可以在pandas中一步完成这些工作(也就是说,不需要使用requests 库或StringIO ),只需将网址传到上面的read_csv 调用中,像这样。

spy_daily = pd.read_csv(url, parse_dates=['timestamp'], index_col='timestamp').sort_index()
spy_daily.tail()
              open     high     low   close  adjusted_close     volume  \
timestamp                                                                
2021-11-22  470.89  473.540  467.35  467.57          467.57   72761954   
2021-11-23  467.22  469.095  464.45  468.19          468.19   73206538   
2021-11-24  466.06  469.570  465.19  469.44          469.44   61858813   
2021-11-26  462.34  463.900  457.77  458.97          458.97  112669636   
2021-11-29  464.07  466.560  461.73  464.60          464.60   82666545   

            dividend_amount  split_coefficient  
timestamp                                       
2021-11-22              0.0                1.0  
2021-11-23              0.0                1.0  
2021-11-24              0.0                1.0  
2021-11-26              0.0                1.0  
2021-11-29              0.0                1.0  
spy_daily.dtypes
open                 float64
high                 float64
low                  float64
close                float64
adjusted_close       float64
volume                 int64
dividend_amount      float64
split_coefficient    float64
dtype: object

Pandas注意到了第一个参数是一个URL,获取了结果,然后将数据转化为DataFrame 。它甚至将数据转换为有效类型。

那JSON呢?

由于数据也是以JSON格式提供的,让我们利用这个机会看看如何将JSON数据也读到DataFrame

datatype = "json"   # everything else the same, but let's get JSON instead
url = f"https://www.alphavantage.co/query?function={function}&symbol=SPY&outputsize=full&apikey={API_KEY}&datatype={datatype}"

res = requests.get(url)

返回的数据与csv格式有一些不同。它是一个有两个成员的JSON对象。第一个是一些元数据,我们在上面的csv格式中没有得到。请注意,数据的时间戳是US/Eastern ,美国的证券交易所就在那里。

res.json().keys()
dict_keys(['Meta Data', 'Time Series (Daily)'])
res.json()['Meta Data']
{'1. Information': 'Daily Time Series with Splits and Dividend Events',
 '2. Symbol': 'SPY',
 '3. Last Refreshed': '2021-11-29',
 '4. Output Size': 'Full size',
 '5. Time Zone': 'US/Eastern'}
res.json()['Time Series (Daily)']['2021-11-22']
{'1. open': '470.89',
 '2. high': '473.54',
 '3. low': '467.35',
 '4. close': '467.57',
 '5. adjusted close': '467.57',
 '6. volume': '72761954',
 '7. dividend amount': '0.0000',
 '8. split coefficient': '1.0'}

由于时间序列中的每个元素都是单日的,所以数值是以DataFrame的时间序列索引为导向。我们可以使用pandasDataFrame.from_dict 方法,从这些值中创建一个有效的DataFrame 。你需要确保你将数据的方向定为index 。默认选项是columns ,它假定每个对象是一整列数据。而我们得到的是每一行的一个对象,关键是索引,在我们的例子中是日期。

spy_daily = pd.DataFrame.from_dict(res.json()['Time Series (Daily)'], orient='index')
spy_daily.head()
           1. open  2. high  3. low 4. close 5. adjusted close  6. volume  \
2021-11-29  464.07   466.56  461.73    464.6             464.6   82666545   
2021-11-26  462.34    463.9  457.77   458.97            458.97  112669636   
2021-11-24  466.06   469.57  465.19   469.44            469.44   61858813   
2021-11-23  467.22  469.095  464.45   468.19            468.19   73206538   
2021-11-22  470.89   473.54  467.35   467.57            467.57   72761954   

           7. dividend amount 8. split coefficient  
2021-11-29             0.0000                  1.0  
2021-11-26             0.0000                  1.0  
2021-11-24             0.0000                  1.0  
2021-11-23             0.0000                  1.0  
2021-11-22             0.0000                  1.0  
spy_daily.index
Index(['2021-11-29', '2021-11-26', '2021-11-24', '2021-11-23', '2021-11-22',       '2021-11-19', '2021-11-18', '2021-11-17', '2021-11-16', '2021-11-15',       ...       '1999-11-12', '1999-11-11', '1999-11-10', '1999-11-09', '1999-11-08',       '1999-11-05', '1999-11-04', '1999-11-03', '1999-11-02', '1999-11-01'],
      dtype='object', length=5556)

但我们还没有完成。首先,索引还没有被解析为一个日期,注意它的dtype 是对象。不过我们可以很容易地解决这个问题。在pandas中我们有几个转换类型的选项,我将使用to_datetime

spy_daily = spy_daily.set_index(pd.to_datetime(spy_daily.index)).sort_index()

我们要清理的第二件事是列名。你可以使用重命名方法来做这件事,我们只需提供一个函数,接收旧的列名并返回一个新的。如果我们在空格上分割旧名称,删除第一个标记,并用下划线连接剩余的标记,我们可以创建与csv格式相同的列。

def convert_name(name):
    tokens = name.split()
    return "_".join(tokens[1:])

convert_name("1. open") # see how this works?
'open'
# normally, I'd do this in one go like this: spy_daily.rename(columns=lambda c: "_".join(c.split()[1:]))
spy_daily = spy_daily.rename(columns=convert_name) 
spy_daily.head()
                  open        high         low       close adjusted_close  \
1999-11-01       136.5       137.0    135.5625    135.5625  90.3183258389   
1999-11-02  135.968704      137.25  134.593704  134.593704  89.6728668602   
1999-11-03       136.0     136.375     135.125       135.5  90.2766853014   
1999-11-04      136.75  137.359299  135.765594  136.531204  90.9637235227   
1999-11-05     138.625  139.109299  136.781204     137.875  91.8590257264   

             volume dividend_amount split_coefficient  
1999-11-01  4006500          0.0000               1.0  
1999-11-02  6516900          0.0000               1.0  
1999-11-03  7222300          0.0000               1.0  
1999-11-04  7907500          0.0000               1.0  
1999-11-05  7431500          0.0000               1.0  

最后,你看到JSON数据是以字符串格式出现的。仔细看看这些数据。如果你开始使用DataFrame ,你很快就会发现它的行为并不正常。

spy_daily.dtypes
open                 object
high                 object
low                  object
close                object
adjusted_close       object
volume               object
dividend_amount      object
split_coefficient    object
dtype: object

如果我们不把这些值转换为数字类型,我们就会有问题。在某些情况下,你会得到一个答案(将是错误的),也许只是一个错误。比如说。

spy_daily['high'].max()
'99.879997'

这是一个字符串,绝对不是SPY的最高价格。我们需要将其转换为一个数字值。同样,你可以在这篇文章中阅读更多关于pandas的类型转换,但是一个简单的方法是在每一列上使用to_numeric

for c in spy_daily.columns:
    spy_daily[c] = pd.to_numeric(spy_daily[c])

spy_daily.dtypes
open                 float64
high                 float64
low                  float64
close                float64
adjusted_close       float64
volume                 int64
dividend_amount      float64
split_coefficient    float64
dtype: object

我们什么时候可以使用这些数据?

与仅仅使用csv格式相比,这看起来有很多额外的工作,但是我选择了这两种方式,所以你可以看到只需要花一点功夫就可以处理不同的数据源格式。请注意,pandas也有一个read_json 方法,可以用于从JSON源创建DataFrame ,但是由于这个数据的实际数据埋藏在对象的深处,所以使用上面的方法更有意义。就像pandas的情况一样,有不止一种方法可以做!也许你能找到一个更好的方法来做。

初始验证

通常情况下,快速验证价格数据的最好方法之一就是直接看它。使用DataFrame.plot ,是快速完成这一工作的好方法。

spy_daily[['open', 'high', 'low', 'close']].plot()

plot of high, low, open, close of SPY

完整数据集的SPY高点、低点、开盘、收盘图

首先,我们可以看到我们的数据可以追溯到2020年,它是连续的,而且根据我们之前看到的情况,看起来很像SPY的价格。我绘制了所有4个价格(开盘价、最高价、最低价和收盘价),以确保数据中没有任何0或疯狂的不准确的价格。数据中也没有明显的缺口,所以这是一个好兆头。

如果数据缺失,你会看到两个日期之间有一条直线(只要差距足够大)。如果有野生值,如0或无穷大,你肯定会在图上注意到这一点。

我们可以编写整本关于数据验证的书,所以现在我们将继续了解数据。

柱状图,或称蜡烛图

通过上面的图表,你可以看到很难分辨出四条线之间的区别。即使只是几天,当所有四条线都显示出来时,也不是完全清晰的阅读。

spy_daily[-20:][['high', 'low', 'open', 'close']].plot()

subset of data for SPY

我们可以用一种更有用的方式来显示数据,通常称为条形图,或蜡烛图。在我的专业经验中,我一直把价格数据的滚动时间段称为条形,但它们也经常被称为蜡烛,因为它们在绘制时的样子。我们可以使用matplotlib的条形图轻松地做到这一点。

如果价格在当天上涨,每个条形图的颜色为绿色,如果下跌,则为红色。柱状图的 "烛台 "部分是以开盘价和收盘价为界。蜡烛的 "烛芯 "部分以最高价和最低价为界限。从这张图中,你可以很快看到每天发生了什么--也就是说,价格在这一天中是否有很大的波动?它是否有一个广泛的范围,但在开盘和收盘之间的移动很少?价格是上升还是下降?

def plot_bars(bars, show_lines=False):
    plt.figure(figsize=(10,8))

    # pick the up (or green) bars
    up = bars.loc[bars['close'] >= bars['open']]
    # pick the down (or red) bars
    down = bars.loc[bars['close'] < bars['open']]

    # both up and down periods will have a thin bar between
    # the high and low, the "wick" of the candle
    plt.bar(up.index, up.high - up.low, bottom=up.low, color="green", width=0.05)
    plt.bar(down.index, down.high - down.low, bottom=down.low, color="red", width=0.05)

    # then, we plot the thicker candle part. 
    plt.bar(up.index, up.close - up.open, bottom=up.open, color="green", width=0.25)
    plt.bar(down.index, down.open - down.close, bottom=down.close, color="red", width=0.25)

    plt.xticks(rotation=45)
    plt.title(f'Price of SPY from {bars.index[0]:%Y-%m-%d} to {bars.index[-1]:%Y-%m-%d}')

    if show_lines:
        plt.plot(bars['high'], color='green', alpha=0.25, label='high')
        plt.plot(bars['low'], color='red', alpha=0.25, label='low')
        plt.plot(bars['open'], color='black', ls=':', alpha=0.25, label='open')
        plt.plot(bars['close'], color='black', ls='--', alpha=0.25, label='close')
        plt.legend()

plot_bars(spy_daily.iloc[-20:])

Bar plot of SPY

现在这不是更好吗?这需要一点练习来快速阅读,但一目了然,信息量更大。如果你把高点/低点/开盘/收盘显示成线,你可以看到它们与蜡烛上的点是如何对应的(而且条形图比线更清晰)。

plot_bars(spy_daily.iloc[-20:], show_lines=True)

Bar plot of SPY with high, low, open, close

为了明确起见,如果收盘价高于开盘价,条形图将是绿色的。如果收盘价低于开盘价,它将是红色的。而条形图中较细的部分显示的是当天的最高和最低价格。

现在我们有超过20年的SPY价格可以看,每个交易日都有一个单条,就像上面的数值。作为一个练习,你可以把更多的数据绘制成条状。

季节性

为了保持简单,我们将在本文中只看季节性的想法,我们将只使用一天结束时的价格,收盘价。现在,我们也将忽略股息和闰年。

首先,让我们谈谈什么是季节性。季节性背后的想法是,存在某种有规律的季节性模式比影响股票价格。如果存在季节性影响,我们认为可以通过观察所有年份的平均回报率来感受一下,看看回报率在一年的不同阶段是否有不同的趋势。

回报

我们需要做的第一件事是获得SPY的回报。我们这样做是因为比较各年的价格是没有意义的。我们想看到的是SPY的百分比变化,而不是价格的差异。有多种方法来计算回报率,但在pandas中计算回报率百分比的一个简单方法是使用pct_change

spy_daily['daily_return'] = spy_daily['close'].pct_change()
spy_daily.head()
                  open        high         low       close  adjusted_close  \
1999-11-01  136.500000  137.000000  135.562500  135.562500       90.318326   
1999-11-02  135.968704  137.250000  134.593704  134.593704       89.672867   
1999-11-03  136.000000  136.375000  135.125000  135.500000       90.276685   
1999-11-04  136.750000  137.359299  135.765594  136.531204       90.963724   
1999-11-05  138.625000  139.109299  136.781204  137.875000       91.859026   

             volume  dividend_amount  split_coefficient  daily_return  
1999-11-01  4006500              0.0                1.0           NaN  
1999-11-02  6516900              0.0                1.0     -0.007146  
1999-11-03  7222300              0.0                1.0      0.006734  
1999-11-04  7907500              0.0                1.0      0.007610  
1999-11-05  7431500              0.0                1.0      0.009842  

你可以看到,daily_return 列有每天的价格增减百分比。我们可以使用cumsum 方法来查看一段时间内的总回报率是什么样的。

spy_daily['daily_return'].cumsum().plot()

SPY returns

现在我们有了简单的百分比回报,我们看到图表从0开始,然后上升到150%以上的回报。这模仿了上面的价格图,但我们现在可以把我们的年份放在上面。做到这一点的一个方法是,只看一年中每一天的平均回报。

我们该如何做呢?首先,我们可以从我们的指数中的日期得到一年中的哪一天。

spy_daily.index.day_of_year
Int64Index([305, 306, 307, 308, 309, 312, 313, 314, 315, 316,
            ...
            319, 320, 321, 322, 323, 326, 327, 328, 330, 333],
           dtype='int64', length=5556)

下一步是对我们的回报进行分组,使每年的第一天在一起,每年的第二天,以此类推。这就是DataFrame.groupby 方法所做的。它为传入函数的每一个值建立一个组,并可以对该组应用聚合函数。我们可以通过使用mean ,只看daily_return ,得到整个数据集中当年那一天的平均日收益。

spy_daily.groupby(spy_daily.index.day_of_year).mean()['daily_return']
2      0.006570
3      0.004355
4     -0.002164
5     -0.001901
6      0.000482
         ...   
362   -0.000548
363    0.001660
364   -0.002844
365   -0.000749
366    0.008551
Name: daily_return, Length: 365, dtype: float64

这只是给了我们一个单一的系列,一年中的每一天都包含了平均值。请注意,数据从第2天开始,因为股票市场每年元旦都会关闭。它有366天,因为有闰年(我之前说过会忽略这一点)。现在我们可以绘制平均每日回报的累积总和,看看平均数是什么样子的。

spy_daily.groupby(spy_daily.index.day_of_year).mean()['daily_return'].cumsum().plot()

Daily mean returns cumsum

嗯,这看起来很有趣。似乎年初的回报率很低,春天的回报率很高,秋天的回报率很低,而年底的回报率很高。

看数据的另一个有趣的方法是看每月的平均回报。我们可以再次用groupby ,但使用柱状图更有意义。

spy_daily.groupby(spy_daily.index.month).mean()['daily_return'].plot.bar()

Monthly mean returns

在这里,看起来9月是一个可怕的月份,而4月是伟大的。

现在股票收益可能有非常大的离群值,这些肯定会影响结果,所以使用平均值可能不是最好的主意。这些异常值也很难(不可能)预测,大多数投资者不会无法避免坏的异常值而参与好的异常值。看看中位数回报是什么样子的,可能更有意义。

spy_daily.groupby(spy_daily.index.month).median()['daily_return'].plot.bar()

Monthly median SPY returns

从中位数回报来看,我们可以看到每个月都有正的中位数回报,除了9月。看起来确实可能有一点季节性的影响,但没有像看平均值那样明显。

这意味着什么呢?

现在我们不会从这个快速的分析中得出任何结论,但是股票回报的季节性通常被讨论,有些人说你应该在5月卖掉你的股票,等到10月再入市。我们甚至可以看到这样的观点:5月、6月和9月的回报率不如一年中的其他时间强劲。但这只是平均水平,一年中仍有正的回报(其中7月和8月相当高)。如果你卖掉你的股票,并在10月份重新买入,你会失去所有这些回报,另外你还必须支付交易成本来进出市场。

也许我们可以看看每一年的数据,看看是否有任何年份倾向于突出,这样我们就可以了解这些年份的异常值是什么?我们可以通过迭代各年的数据,用布尔表达式抓取该年的部分DataFrame ,并绘制该年的回报率来做到这一点。你可以在这里阅读更多关于布尔索引的信息。

for year in spy_daily.index.year.unique():
    sub = spy_daily.loc[spy_daily.index.year == year]
    plt.plot(sub.index.day_of_year, sub['daily_return'].cumsum())

annual returns of SPY

让我印象深刻的是那些有很大跌幅的年份。上升的大峰值较少。(这符合关于股票的古老谚语,它是一个向上的扶梯,但也是一个向下的电梯)。让我们来看看这些是什么。如果我们做一个简单的函数来寻找某一年的最低累计回报值,我们就可以看到上面的图中有哪些年份的低点。

def min_return(rets):
    # get the index of the minimum value, and the value itself
    return rets.loc[rets.idxmin()], rets.idxmin()

for year in spy_daily.index.year.unique():
    sub = spy_daily.loc[spy_daily.index.year == year]
    minimum, dt = min_return(sub['daily_return'].cumsum())
    print(f"{year} {minimum * 100:6.2f}% on {dt:%Y-%m-%d}")
1999  -0.71% on 1999-11-02
2000 -12.35% on 2000-12-20
2001 -28.00% on 2001-09-21
2002 -35.37% on 2002-10-09
2003  -8.74% on 2003-03-11
2004  -3.64% on 2004-08-06
2005  -5.85% on 2005-04-20
2006  -1.32% on 2006-06-13
2007  -2.93% on 2007-03-05
2008 -59.01% on 2008-11-20
2009 -26.86% on 2009-03-09
2010  -7.63% on 2010-07-02
2011 -11.63% on 2011-10-03
2012   1.59% on 2012-01-03
2013   2.22% on 2013-01-08
2014  -5.77% on 2014-02-03
2015  -8.72% on 2015-08-25
2016 -10.61% on 2016-02-11
2017   0.77% on 2017-01-03
2018 -11.66% on 2018-12-24
2019  -2.28% on 2019-01-03
2020 -33.64% on 2020-03-23
2021  -1.36% on 2021-01-04

好吧,我们可以看到2001年和2002年(科技泡沫),2008年、2009年(大金融危机),以及2020年(Covid)都在这里突出。让我们看看这些东西本身是什么样子的。

for year in [2020, 2001, 2002, 2008, 2009]:
    sub = spy_daily[f"{year}":f"{year}"]
    plt.plot(sub.index.day_of_year, sub['daily_return'].cumsum(), label=year)
plt.legend()

spy annual returns for select years

因此,我们可以把2008年、2009年和2020年这几个大的异常年份从我们先前的结果中删除,只是为了看看 "正常 "的季节性是什么,特别是在日历年的开始和结束。请注意,在建立交易模型时,这不是一个好主意,但现在我们只是试图探索和理解数据。我在pandas中通过使用另一个布尔表达式来做到这一点,但使用逆运算符~isin 来取消选择这些年份的所有值。

spy_daily_red = spy_daily.loc[~spy_daily.index.year.isin([2001, 2002, 2008, 2019, 2020])]
spy_daily_red.groupby(spy_daily_red.index.day_of_year).mean()['daily_return'].cumsum().plot()

spy mean returns select years removed

现在,这就更顺畅了。注意到回报只是有点向上和向右移动,没有任何向上或向下的趋势?有了这些信息,存在持续的多月季节性的想法似乎不像以前那样站得住脚。

用这个减少的数据集看每月的结果如何呢?

spy_daily_red.groupby(spy_daily_red.index.month).mean()['daily_return'].plot.bar()

spy monthly returns select years removed

有趣的是,即使去掉最糟糕的年份,9月和6月的平均利润仍然不高。

我们可以从这里做什么?

我们不打算从这个简短的探索中得出明确的结论,为投资或交易的决策提供参考,但这个初步的调查可以把事情推向一些有趣的方向。也许你在阅读这篇文章时想到了一些问题,你想考虑一下你可以探索的其他信息。

更多数据

如果你能得到更多的历史数据,这将是很有价值的。在我们的免费数据集中,我们只能追溯到20年前,但如果有更多的历史数据,我们也许能看到更多显示季节性的一致的月份。也许过去的效果更强,我们可以比较不同的几十年。虽然我们可以用目前的数据做一些事情,但如果有更多的数据,我们会更有信心。

另一个选择是获得更细化的数据,也许是以1分钟或1小时为单位。这将使我们能够研究日内的季节性,是否有一天中的某些时间,市场倾向于比其他时间更容易上涨?或者用我们目前的数据,也许季节性是在每月的基础上可见的?许多月度事件影响市场,包括重要数据的发布或月初和月底存入银行账户的工资。在年度基础上,缴税和分配退税会影响市场。在未来的文章中,我将研究这些想法中的一些。

如果你喜欢这篇文章,请注册我的通讯,当我发布新的文章时,我会告诉你。

The postFinancial market data analysis with pandasappeared first onwrighters.io.