Home Basic Data Analysis Trading Strategy Analysis using Python and the FFN Package – Part 1

Trading Strategy Analysis using Python and the FFN Package – Part 1

by Stuart Jamieson

In this post I will be reviewing and running through examples of using the brilliant python module, “ffn – Financial Functions for Python“, which has been created by Philippe Morissette and released on the MIT license. The github page can be found here (http://pmorissette.github.io/ffn/index.html)

The module helps quickly carry out analysis of trading strategies and financial asset price series/history. It can deal with single series using the “PerformanceStats” class, or multiple combined assets simultaneously using the “GroupStats” class.

Let’s begin with the simple, one asset/strategy case and run through some examples.

We begin by importing the relevant modules (and if working in a Jupyter notebook, the call to allow matplotlib objects to be rendered in the browser).

import pandas as pd
import numpy as np
import ffn
%matplotlib inline

Now lets generate a set of random performance data, spanning a 1000 day period. We start by setting the number of days we want (i.e. 1000), then we use the numpy “randn” call to generate an array of 1000 numbers drawn from the normal distribution. To give our data a slight upward bias, I have then added to these figures a random value chosen from the uniform distribution with a maximum value of 0.2 and a minimum value of 0.0. These random figures represent the daily returns of our trading strategy, or stock price.

I have then created a Pandas DataFrame from this “returns” data and added a column of the cumulative sum of those returns, adding 100 to represent starting capital – these represent the strategy “equity” or the amount of cash we have in our trading account.

The ffn package does allow the direct download of historic stock price data from Yahoo Finance but thought i would use random data in the first instance. I will go over the download of data later in the post.

num_days = 1000
data = (np.random.randn(num_days) + np.random.uniform(low=0.0, high=0.2, size=num_days))
index = pd.date_range('01/01/2010',periods=num_days, freq='D')
data = pd.DataFrame(data,index=index,columns=['Returns'])
data['Equity'] = data.cumsum() + 100
data.iloc[0] = 100

So the “Equity” column of our DataFrame is the one we are most interested in as this is our theoretical trading account equity. We start by using the “calc_stats” method on this data and assigning that to a variable called “perf”

perf = data['Equity'].calc_stats()

If we lok at what “perf” actually is by looking at its “type”, we can see it is a ffn.core.PerformanceStats object. This means we can use any of the PerformanceStats object methods on it.

type(perf)

“ffn.core.PerformanceStats”

Let’s start by plotting the equity curve. This is very similar to just calling the usual “plot()” syntax from Pandas.

perf.plot()

Great stuff, so we now have a visual representation of our equity curve. Let’s move on to some statistics; we can very easily do this using ffn by calling the “display()” method. This will print out the following set of comprehensive statistics.

perf.display()

That is a pretty comprehensive set of statistics – the Sharpe Ratio, Calmar Ratio, Total Returns, CAGR, Max Drawdown, Periodic Returns plus more…that’s not a bad time saver – if we were to have to calculate these all ourselves it could take quite some time.

The next ffn method we might be interested in is that to represent a table of monthly returns. Again, it’s as easy as this:

perf.display_monthly_returns()

How about plotting a visual representation of our strategy drawdown series…it can be carried out with a single line of code:

ffn.to_drawdown_series(data['Equity']).plot(figsize=(15,7),grid=True)

And plotting a histogram of returns is again as simple as:

perf.plot_histogram()

If we want to get hold of the set of comprehensive analysis statistics mentioned above, but this time in a Pandas series, rather than printed out as a table, we can get hold of it as follows:

perf.stats

This Series object is index-able, just like any other Pandas Series so we can pick out any relevant items we need using the following syntax – for example if we wanted the Yearly Sharpe Ratio:

perf.stats['yearly_sharpe']

“1.9283569892842471”

Finally for the “PerformanceStats” object, we can extract the series of “Lookback Returns” as simply as follows (this can also be indexed similarly to the “perf.stats” object mentioned above).

perf.display_lookback_returns()

I’ll leave it here for this post, and in the upcoming post will deal with using the FFN package when considering multiple price/equity series simultaneously using the “GroupStats” object.

Until next time!

You may also like

28 comments

publiclunchresearch 14 March 2018 - 03:39

so nice thanks

Reply
Tian 20 June 2018 - 09:57

If I use monthly return, how can I use ffn package to do the similar analysis? Thanks.

Reply
s666 20 June 2018 - 17:18

Hi Tian – what I tend to do if I have monthly returns/price series is just feed in that data as normal and ignore any stats that are presented at less than monthly frequency – so disregard stats calculated on a “daily” basis and just accept the stats named as “monthly” or longer time period.

Reply
Tian 20 June 2018 - 09:58

If I use monthly return, how can I use ffn package to do the similar analysis? Thank you.

Reply
LS 29 June 2018 - 14:10

Hi, thanks for your blog and all your articles. I am combing the moving average strategy part 1 with this library and I am getting equity curve but the stats is incomplete. I guess that’s because return of this strategy is very low. Would you just indicate how to adapt the strategy to be combined with the FFN ?
Thanks
LS

Reply
s666 1 July 2018 - 09:09

Hi there – I can definitely try to help. What parts of the stats are incomplete? Do you have enough history of the equity curve to calculate all the stats?

Reply
LS 10 July 2018 - 18:00

Hi, thanks for your help. I am sharing the jupyter file here: http://nbviewer.jupyter.org/gist/Ludek1234/57283adcb4d00467f5fc90a847fbd71c

Reply
Pierpaolo 10 January 2019 - 21:57

Hi, nice article and superb blog. I was trying to use this package but, when using calc_stats() I consistently get the error:

negative_returns = np.minimum(returns, 0.)
RuntimeWarning: invalid value encountered in minimum

even when I replicated your code above. Any guidance? Thanks

Reply
s666 10 January 2019 - 22:14

That’s strange – have you replicated the code exactly? What about if you use the exact code below:

import pandas as pd
import numpy as np
import ffn
%matplotlib inline
num_days = 1000
data = (np.random.randn(num_days) + np.random.uniform(low=0.0, high=0.2, size=num_days))
index = pd.date_range('01/01/2010',periods=num_days, freq='D')
data = pd.DataFrame(data,index=index,columns=['Returns'])
data['Equity'] = data.cumsum() + 100
data.iloc[0] = 100
perf = data['Equity'].calc_stats()
perf.display()
Reply
Pierpaolo 11 January 2019 - 07:35

Copied and pasted that exact code (except for “%matplotlib inline” as I am running Python 3.6.8 under IDLE environment). This is the annoying warning I am always getting:

Warning (from warnings module):
File “C:\Python36-64\lib\site-packages\ffn\core.py”, line 2054
negative_returns = np.minimum(returns, 0.)
RuntimeWarning: invalid value encountered in minimum

Warning (from warnings module):
File “C:\Python36-64\lib\site-packages\ffn\core.py”, line 2056
res = np.divide(er.mean(), std)
RuntimeWarning: divide by zero encountered in true_divide

Should I switch from IDLE to Anaconda environment?

Reply
Pierpaolo 11 January 2019 - 10:37

After some research and trials on your exact code, I’ve managed to get rid of the first of the two warnings (the one connected with np.minimum(returns, 0.)) by using the ffn package from github instead of the one coming from pip install. As far as the second warning is concerned (np.divide(er.mean(), std)), the only solution was to suppress the warning by adding the line np.seterr(all=’ignore’) to your code. Looks like a bug in the ffn package still unresolved.

Reply
s666 11 January 2019 - 17:37

Interesting – so now you have suppressed the warning, the code can be run and the calculations are being made/displayed?

Reply
Pierpaolo 11 January 2019 - 18:14

Yes, the entire replicated code could be run and calculations were displayed. Thanks for you reply.

Reply
s666 14 January 2019 - 17:23

Great stuff – glad to hear it’s working!!

Reply
dirk 3 December 2019 - 13:05

Hi – Why don’t the monthly returns add up to the YTD in your table above ?

Reply
s666 6 December 2019 - 06:23

Hi dirk, rather than simply adding the returns in a “simple arithmetic return” approach, the “proper” method is actually to calculate the monthly compounded, time weighted return by adding 1 to each monthly return figure and then multiplying all the monthly values together, resulting in a final value, which we then subtract the 1 from to end at the monthly compounded time weighted return.

Reply
dirk 13 December 2019 - 13:46

Thanks for taking the time to answer.

Reply
efueyo 16 June 2021 - 12:30

Thanks for this article.. I want to learn how to use this package and I run into the problem that, at startup, I get the following error:

/home/enri/anaconda3/envs/enri/lib/python3.8/site-packages/ffn/core.py:2299: RuntimeWarning: divide by zero encountered in true_divide
res = np.divide(er.mean(), std)

How can I fix this problem?

Reply
efueyo 16 June 2021 - 12:38

Dear all. with perf.display () we get a very complete table of statistics. How could I associate each of them individually to a variable, to use it in another part of the program? I will appreciate help

Reply
efueyo 16 June 2021 - 12:52

I was hasty in asking the question above. I have just seen that perf.stats is a list, so it is possible to access each element of it in the usual way. Thanks

Reply
efueyo 16 June 2021 - 17:38

FFN is very practical, and only what I have learned in these two articles allows me to design with much less code. Where can I find a complete tutorial or a description of all the functions, in addition to those explained in these articles, and their parameters? Thank you

Reply
S666 17 June 2021 - 02:04

Hi there, the link is given at the top of the post for the package homepage… For convenience this is it also http://pmorissette.github.io/ffn/index.html

Reply
efueyo 17 June 2021 - 11:12

All the statements in this article are executed correctly when I replicate them in my jupyter lab. But when doing the same with another database of quotes, the sentence “perf.display_monthly_returns()”, it returns the error llll

AttributeError: ‘GroupStats’ object has no attribute ‘display_monthly_returns’

What could be my problem? Thanks for your help

Reply
efueyo 17 June 2021 - 11:21

Indeed, in the API of “ffn – Financial Functions for Python”, there are all the functions. Thanks

Reply
efueyo 17 June 2021 - 16:45

Dear all. What is the reason for the sentence “data.iloc[0] = 100” ? In my opinion the graph looks better without it. Is it necessary for subsequent scripts, that in the first row the df have 100 in the two columns? Thanks in advanced.

Reply
S666 17 June 2021 - 18:00

It explicitly rebases the start of the price series to 100 on “day 1”. Its not absolutely necessary just be careful to note that your series will have a first value of 100 plus whatever the return was on the first day. I wouldn’t worry too much about it if you are unsure of the logic behind it. If you use real price series (I created to dummy data here rather than using real data) then you should be OK. Maybe just have a Google regarding rebasing /reindexing a time series.

Reply
rich perez 31 August 2022 - 16:01

Nice job!! A question: can the calc_stats() be setup to display a year range other than 2010? For example from 2020 to 2023? Thanks!

Reply
Stuart Jamieson 31 August 2022 - 17:32

If all you want are stats between certain dates or for a certain time period, then the easiest way is just to feed in data to the function from between those dates. So rather than just pass in the entire dataframe, assuming your df index is a datetime index, you could do something like:


df.loc['2010-01-01':].calc_stats().display()

Reply

Leave a Reply

This website uses cookies to improve your experience. We'll assume you're ok with this, but you can opt-out if you wish. Accept Read More

%d bloggers like this: