Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save jeromeku/343cb0f7c104e4f37902843e0810ce0d to your computer and use it in GitHub Desktop.
Save jeromeku/343cb0f7c104e4f37902843e0810ce0d to your computer and use it in GitHub Desktop.
MeanReversionTutorial_4_azur.ipynb
Display the source blob
Display the rendered blob
Raw
{
"nbformat": 4,
"nbformat_minor": 0,
"metadata": {
"colab": {
"name": "MeanReversionTutorial_4_azur.ipynb",
"version": "0.3.2",
"provenance": [],
"collapsed_sections": []
},
"kernelspec": {
"name": "python2",
"display_name": "Python 2"
}
},
"cells": [
{
"cell_type": "markdown",
"metadata": {
"id": "view-in-github",
"colab_type": "text"
},
"source": [
"[View in Colaboratory](https://colab.research.google.com/gist/gjlr2000/8ee782c374b4b014674b635668d09d5c/meanreversiontutorial_4_azur.ipynb)"
]
},
{
"metadata": {
"id": "OlbdQwIzFwFY",
"colab_type": "text"
},
"cell_type": "markdown",
"source": [
"# Trading Mean Reversion\n",
"\n",
"Before even thinking of trading a mean reverting signal, read [Why I won't teach pair trading to my students](https://www.marketwatch.com/story/why-i-wont-teach-pair-trading-to-my-students-2012-10-01). \n",
"\n",
"Never forget the fundamental drivers which should drive the mean reversion, and scenarios when the drivers can dissapear.\n"
]
},
{
"metadata": {
"id": "-ZMYfNfsIK3V",
"colab_type": "text"
},
"cell_type": "markdown",
"source": [
"\n",
"## The trigger strategies\n",
"\n",
"As the name implies, a 'trigger' represents a level of the mean reverting signal that is used by the trader to enter and exit a trade. \n",
"\n",
"### Standard Deviation multiples\n",
"\n",
"A common tactic ([see quantopian](https://www.quantopian.com/posts/trading-strategy-mean-reversion)) is to check how far away from the mean the current value is, and be long (or short) if the trade is more than a predefined multiple of standard deviations (one, two or more) and setting up a stop loss at another multiple. The academic [literature](https://arxiv.org/ftp/arxiv/papers/1602/1602.05858.pdf) usually verifies the behaviour of this trading strategy out-of-sample, but the decision tends to be very binary.\n",
"\n",
"Using the 'pca' signal from our past [notebook](https://drive.google.com/file/d/1QNtBrFLsn7sqayEKgJVXg0UJz1JNYgVI/view?usp=sharing), we have computed the following parameters:"
]
},
{
"metadata": {
"id": "cH2jPzYc9WTI",
"colab_type": "code",
"colab": {}
},
"cell_type": "code",
"source": [
"# Request: allow HTTP-post API calls\n",
"import requests\n",
"# Json: to format as json calls to API\n",
"import json \n",
"# Pandas: to plot the timeseries\n",
"import pandas as pd\n",
"# Datetime: to convert different date formats\n",
"import datetime as dt\n",
"# Import the Time Series library\n",
"import statsmodels.tsa.stattools as ts\n",
"\n",
"from numpy import cumsum, log, polyfit, sqrt, std, subtract, sign, exp, max"
],
"execution_count": 0,
"outputs": []
},
{
"metadata": {
"id": "rSGGpxYpVN94",
"colab_type": "code",
"colab": {
"base_uri": "https://localhost:8080/",
"height": 34
},
"outputId": "d9c5f3d4-8d95-46f6-b2e0-490a81c38af0"
},
"cell_type": "code",
"source": [
"\n",
"# The numbers below were obtained from a previous notebook,\n",
"# I hardwire them instead of linking the previous data\n",
"currentValue = -0.11513815488258894\n",
"mu = -0.26912068157010066\n",
"theta = 2.826060587147701\n",
"sigma = 0.2638419146133024\n",
"\n",
"# derived from the numbers above\n",
"half_life = 89.58477707480525\n",
"std_band = 0.11097829743754704\n",
"\n",
"# Number of standard deviations from the mean\n",
"print (\"Std's from mean:\",(currentValue - mu)/std_band)\n"
],
"execution_count": 0,
"outputs": [
{
"output_type": "stream",
"text": [
"(\"Std's from mean:\", 1.3875012524332992)\n"
],
"name": "stdout"
}
]
},
{
"metadata": {
"id": "CluiC8_aAp5n",
"colab_type": "text"
},
"cell_type": "markdown",
"source": [
"At the time of writing the pca signal was about 1.39 std's from away from the mean - so somewhere in the start of a trigger (although the most preferred multiple seems to be 2, so most likely we would be sitting out this trade if we were followers of this rule)\n",
"\n",
"### Ex-ante Sharpe Ratio\n",
"However, there is no reason to limit the trigger to be standard deviation multiples. We could instead compute the ex-ante Sharpe Ratio using a known holding period, and set up a threshold in terms of Sharpe Ratio (which is more intuitive for some financial professionals) - quoting from [InvestoPedia](https://www.investopedia.com/ask/answers/010815/what-good-sharpe-ratio.asp):\n",
"\n",
"\n",
"> Usually, any Sharpe ratio greater than 1 is considered acceptable to good by investors. A ratio higher than 2 is rated as very good, and a ratio of 3 or higher is considered excellent. The basic purpose of the Sharpe ratio is to allow an investor to analyze how much greater a return he or she is obtaining in relation to the level of additional risk taken to generate that return.\n",
"\n",
"notes \n",
"* the number computed below is actually the Expected Return divided by the Standard Deviation of the simulation, we need to substract the risk free rate divided by the Standard Deviation, and \n",
"* it assumes the trader will **hold** the trade from entry and will only exit at the horizon date (regadless of the path the tarde takes in between). This assumption is made by practitioners because there is an easy formula to use, **but** does not reflect reality (more below).\n",
"\n",
"\n",
"\n"
]
},
{
"metadata": {
"id": "xd03YuPbOYVs",
"colab_type": "code",
"colab": {
"base_uri": "https://localhost:8080/",
"height": 68
},
"outputId": "72d664d5-073c-403d-ce8b-e0d24ccdb7a7"
},
"cell_type": "code",
"source": [
"# Using the formulas to compute the forward expected mean and standard devition at some time\n",
"# t in the future (today being time '0', and expressed in terms of a year being one unit)\n",
"def OUmu_t(currentValue, mu, theta, t):\n",
" return mu + (currentValue - mu)*exp(-theta * t)\n",
"\n",
"def OUsigma_t(sigma, theta, t):\n",
" return sqrt ( ( 1 - exp(-2 * theta * t)) * sigma * sigma / (2 * theta) )\n",
"\n",
"# Set up the forward horizon\n",
"# Above, the pca halflife was 89.58, so we take roughly\n",
"# 2 x halflife = 180\n",
"NumberFP = 180\n",
"forward_period = NumberFP / 365.25\n",
"\n",
"# Let's compute what will be the expected mean at NumberFP calendar days,\n",
"ExpectedMu = OUmu_t(currentValue, mu, theta, forward_period)\n",
"# and the expected standard deviation as well\n",
"ExpectedSigma = OUsigma_t(sigma, theta, forward_period)\n",
"\n",
"print (\"Mean after \",NumberFP,\" days:\",ExpectedMu)\n",
"print (\"Std after \",NumberFP,\" days:\",ExpectedSigma)\n",
"\n",
"# Sharpe (assuming risk free rate is 0):\n",
"AnnualisedSharpe = (currentValue - ExpectedMu)/ ExpectedSigma * sqrt(1 / forward_period)\n",
"\n",
"print ('Sharpe (annual)',AnnualisedSharpe) "
],
"execution_count": 0,
"outputs": [
{
"output_type": "stream",
"text": [
"('Mean after ', 180, ' days:', -0.23087160812325994)\n",
"('Std after ', 180, ' days:', 0.1074999995836604)\n",
"('Sharpe (annual)', 1.533590144519143)\n"
],
"name": "stdout"
}
]
},
{
"metadata": {
"id": "royluJbNTY-t",
"colab_type": "text"
},
"cell_type": "markdown",
"source": [
"The ex-ante annualised Sharpe Ratio of 1.5 is a 'good' one (as defined by Investopedia), but as practitioners assume a degradation of the ex-post SharpeRatio they tend to increase the threshold accordingly - the trading strategy would require a Sharpe roughly 50% higher than what the practitioner wants to achieve. Hence, using this trigger method would have us sitting out of the trade.\n",
"\n",
"However, this formula has some unpleasant sideffects: it strongly assumes the trader will buy and hold the position to the horizon, regardless if it passes through the mean way before the end of the horizon ! (in real life the trader would have taken profits as it get closer to the mean).\n",
"\n",
"### Ex ante 'real life' Sharpe Ratio (include profit target, stop loss)\n",
"\n",
"Getting analytical formulas Sharpe Ratios (or other statistics like 'hitting times') of mean reverting stochastic processes is quite [complicated](https://pdfs.semanticscholar.org/755b/1db0077f87103aef38ab5e1167f1745b6140.pdf), but the magic of Moore's law allows us to run MonteCarlo simulators (provided by our Markov API) to compute the parameters (in the past MonteCarlo was usually dismissed as too compute-intensive). \n",
"\n",
"Our simulator allows us to introduce more complicated strategies: add stop loss, profit take, and (in the premium version - not available in this API) even level-driven dynamic strategies.\n",
"\n",
"So let's compute a 'real life' Sharpe Ratio by setting up the mean of the process as the profit target: "
]
},
{
"metadata": {
"id": "ek013zZSXM5q",
"colab_type": "code",
"colab": {
"base_uri": "https://localhost:8080/",
"height": 34
},
"outputId": "a76e9c62-18f1-4695-9672-1876fc6edfae"
},
"cell_type": "code",
"source": [
"\n",
"# defining the api-endpoint \n",
"def MarkovSimulator(Mu, Theta, Sigma, currentValue, ProcessType = \"MeanReverting\", NumberSimulations = 100000, \n",
" NumberForwardPeriods = 182, UpperStop = 999999.0, LowerStop = -999999.0,\n",
" PointValue = 1.0, InitialNumberContracts = 1, DynamicStrategy = []): \n",
" #API_ENDPOINT = \"http://simfunctionapp20180222.azurewebsites.net/api/SimFunction?code=/7CGIJPawdfzJ0kV6Q8ETOHUpiz8r6be2D7irr2Lc2mJJ3Yu/Uzw6w==\"\n",
" API_ENDPOINT = \"https://gmaatest.azurewebsites.net/api/HttpTriggerCSharpSimulate?code=n80MHATfRjmM6ZelyL11DtLpkX7hxa5sTFlOrYiZnkAxuPs1zWahCA==\" \n",
" NumberForwardPeriodsBD = int(NumberForwardPeriods * 252 / 365.25)\n",
" currentDateString = \"2018-01-02T00:00:00\"\n",
" dateStringMask = '%Y-%m-%dT%H:%M:%S'\n",
" current_datetime_object = dt.datetime.strptime(currentDateString, dateStringMask )\n",
" lastTradeDate_object = current_datetime_object + dt.timedelta(days = NumberForwardPeriods)\n",
" lastTradeDateString = lastTradeDate_object.strftime(dateStringMask)\n",
" \n",
" json_api = {\n",
" \"calibrationResult\": {\n",
" \"Mu\": Mu,\t\t\t\t\n",
" \"Sigma\": Sigma,\t\t\t\t\n",
" \"Lambda\": Theta,\t\t\t\n",
" \"ProcessType\": ProcessType,\t\t\t\n",
" \"DaysPerYear\": 365.25,\n",
" \"LogLikelihood\": 0.0,\n",
" \"LastPrice\": 0.0 \n",
" },\n",
" \"simulationParams\" : {\n",
" \"CurrentDate\": currentDateString,\n",
" \"LastTradeDate\": lastTradeDateString,\n",
" \"CurrentValue\": currentValue,\t\t\t\n",
" \"NumSimulations\": NumberSimulations,\t\t\n",
" \"NumForwardDays\": NumberForwardPeriodsBD,\n",
" \"UpperStop\": UpperStop,\t\t\t\t\n",
" \"LowerStop\": LowerStop,\t\t\t\n",
" \"PointValue\": PointValue,\t\t\t\n",
" \"NumConracts\": InitialNumberContracts,\n",
" \"IndexValue\": 1.0,\n",
"# \"ThresholdLossProbability\": 10000,\t\n",
"# \"DrawdownPercent\": 0.5,\t\t\t\n",
"# \"DrawdownProbability\": 0,\t\t\n",
" \"dynamicStrategy\": DynamicStrategy\n",
" }\n",
" }\n",
" # data to be sent to api\n",
" # import dynamic_strategy_10_peter\n",
" # r = requests.post(url = API_ENDPOINT, json = dynamic_strategy_10_peter.json_api)\n",
" r = requests.post(url = API_ENDPOINT, json = json_api)\n",
" r_json = r.json() \n",
" return r_json\n",
"\n",
"# Simulation:\n",
"# Long if currentValue < mu, \n",
"# Short if currentValue > mu: \n",
"long_short = sign(mu - currentValue)\n",
"in_number_ct = int( long_short )\n",
"\n",
"profit_level = (mu)\n",
"stop_loss = -999999.0\n",
"\n",
"# Let's set up the stop levels:\n",
"if (long_short * 1 > 0):\n",
" # we are long, UpperStop is profit target\n",
" UpperStop = profit_level\n",
" LowerStop = stop_loss\n",
"else:\n",
" # we are short, LowerStop is the target\n",
" LowerStop = profit_level\n",
" UpperStop = -stop_loss\n",
"\n",
"# Simulate \n",
"r_json = MarkovSimulator(mu, theta, sigma, currentValue,\n",
" NumberForwardPeriods = NumberFP, \n",
" UpperStop = UpperStop,\n",
" LowerStop = LowerStop,\n",
" InitialNumberContracts = in_number_ct)\n",
"\n",
"# Results (in r_json) - the following are the useful ones for the Sharpe\n",
"# print (r_json)\n",
"ExpectedMean = r_json['simStatistics']['PnlStatistics']['Mean']\n",
"StdDev = r_json['simStatistics']['PnlStatistics']['StdDev']\n",
"UpperStopProbability = r_json['simStatistics']['PnlStatistics']['UpperStopProbability']\n",
"LowerStopProbability = r_json['simStatistics']['PnlStatistics']['LowerStopProbability']\n",
"MeanMaxDrowdown = r_json['drawdownStatistics']['MeanMaxDrawdown']\n",
"MedianMaxDrowdown = r_json['drawdownStatistics']['MedianMaxDrawdown']\n",
"\n",
"Sharpe = ExpectedMean / StdDev * sqrt (365.25 / NumberFP)\n",
"\n",
"print (\"Sharpe ratio: (annual)\",Sharpe)\n"
],
"execution_count": 0,
"outputs": [
{
"output_type": "stream",
"text": [
"('Sharpe ratio: (annual)', 2.184940108395822)\n"
],
"name": "stdout"
}
]
},
{
"metadata": {
"id": "76vc0JsID2Pv",
"colab_type": "text"
},
"cell_type": "markdown",
"source": [
"Now the ex-ante Sharpe Ratio is close to 2.10! \n",
"\n",
"A previously 'mediocre' signal has been 'upgraded' from 'good' to 'very good' - still, practitioners will downgrade the signal to allow for ex-post Sharpe Ratio degradation.\n",
"\n",
"For a case study, read [How I maximise my Sharpe Ratio for a mean reverting strategy](https://www.markov.finance/single-post/2018/01/25/How-I-maximise-my-Sharpe-Ratio-for-a-mean-reverting-strategy). Spoiler alert: if you have a 'take profit at mean' you should compensate with a 'stop loss', which will decrease your ex ante Sharpe Ratio - the idea is to move the profit level somewhere between the curren level and the mean and see where it maximizes the Sharpe.\n"
]
},
{
"metadata": {
"id": "mfptMdSpTO2t",
"colab_type": "text"
},
"cell_type": "markdown",
"source": [
"#### Setting up stop losses\n",
"\n",
"**Important: **In a previous [notebook cell](https://colab.research.google.com/drive/1dEjjPPpJ6FVzgZaqp1EjYENSwE7Q9TU8#scrollTo=dz4mcP_1iRcH&line=35&uniqifier=1) I presented a typical mean reverting series with 2 and 3 standard deviation bands plus the Expected forward path and Expected 2 Standard deviation bands. Please open it and see it (I am avoiding recreating it here to reduce notebook code complexity)\n",
"\n",
"Profit-taking targets are quite simple -- the target is usually a number on the way to the mean (and we can simulate many times).\n",
"\n",
"On the other hand, there is a counter-intuitive meaning to the forward expected paths **given** that we start at a level above the 2 standard deviation band: see the expected forward 2 standard deviation graph, and notice how it goes **higher** that the 3 standard (in this case roughly 90bps). Some traders will put the stop loss somewhat lower than the 3 standard deviation band with the belief that there is less than a 0.18% probability of it ever arriving there - and if it were to happen, then they would think the mean reversion was broken.\n",
"\n",
"By looking at the conditional path **given** we start from a high level, at one moment in time the probability of the trade going above 90bps will only be a 2 standard deviation event (roughly 2.3% - see [the 68-95-99.7 rule](https://en.wikipedia.org/wiki/68%E2%80%9395%E2%80%9399.7_rule)) - so still 'unlucky', but **not** a black swan. \n",
" "
]
},
{
"metadata": {
"id": "iT9SAWJ_VEIe",
"colab_type": "text"
},
"cell_type": "markdown",
"source": [
"\n",
"## Kelly criterion lense\n",
"\n",
"I explained in [Sizing mean reverting trades using the Kelly criterion](https://www.markov.finance/single-post/2018/02/20/Sizing-mean-reverting-trades-using-the-Kelly-criterion) a method to look at mean reversion from a 'biased coin' perspective (which allow you to use Kelly to weight how much to allocate to a mean reverting trade). Below you can see a method to compute the probabilities:"
]
},
{
"metadata": {
"id": "D5C55o-BD2t-",
"colab_type": "code",
"colab": {
"base_uri": "https://localhost:8080/",
"height": 51
},
"outputId": "12fae718-8279-425d-c248-73b6d151994b"
},
"cell_type": "code",
"source": [
"\n",
"# set the horizon at the half life,\n",
"# upper stop and lower stop simmetrically, at the standard devition /2 to make the band\n",
"# 'tight' enough to force stops.\n",
"r_json = MarkovSimulator(mu, theta, sigma, currentValue, \n",
" NumberForwardPeriods = int(half_life), \n",
" UpperStop = currentValue + std_band/2, \n",
" LowerStop = currentValue - std_band/2)\n",
"\n",
"# The simulator runs the process, and counts the number of times it has reached the\n",
"# upper stop to compute the frequency:\n",
"UpperStopProbability = r_json['simStatistics']['PnlStatistics']['UpperStopProbability']\n",
"\n",
"# and similarly for the lower stop:\n",
"LowerStopProbability = r_json['simStatistics']['PnlStatistics']['LowerStopProbability']\n",
"\n",
"print ('Biased probabilities:', UpperStopProbability, LowerStopProbability)\n",
"\n",
"# pick up the maximum probability - we will 'bet' on it\n",
"max_prob = max( [UpperStopProbability, LowerStopProbability])\n",
"\n",
"# Kelly criterion for biased coins (but which the market offers 1:1 odds):\n",
"print ('Kelly =', max_prob * 2 - 1)"
],
"execution_count": 0,
"outputs": [
{
"output_type": "stream",
"text": [
"('Biased probabilities:', 0.29709, 0.69039)\n",
"('Kelly =', 0.3807799999999999)\n"
],
"name": "stdout"
}
]
},
{
"metadata": {
"id": "LEu3I8EmmHNo",
"colab_type": "text"
},
"cell_type": "markdown",
"source": [
"in this example (because the last value is above the mean) the LowerStopProbability has a 69% probability, which corresponds to a 38% Kelly ratio. As it is often said:\n",
"\n",
"\n",
"\n",
"> [[quote](https://books.google.co.uk/books?id=A5pI1fewuAoC&pg=PA243&lpg=PA243&dq=edge+over+the+market+51%25&source=bl&ots=hBmmjWFItU&sig=smewovQrSPp66esV6DR7GXzPh8o&hl=en&sa=X&ved=0ahUKEwjdpLfQk4raAhXPblAKHblcCPYQ6AEIJzAA#v=onepage&q=edge%20over%20the%20market%2051%25&f=false)] Just being right 51% is enough to give you an edge over the market - 70% makes you an all-time legend. But that still means super-legends must be comfortable being wrong 30% of the time.\n",
"\n",
"A practitioner would see the Kelly allocation as \"[the red line that should never be crossed](http://www.financial-math.org/blog/2013/10/two-tales-of-the-kelly-formula/)\" and would usually allocate a fraction of Kelly (\"half the kelly [=] three-quarters of the return with half the volatility\" - same [link](https://)).\n",
"\n",
"What happens if there are many simultaneous opportunities ? if they are independent, we can use [Kelly Criterion for multiple simultaneous events](https://www.linkedin.com/pulse/kelly-criterion-multiple-simultaneous-events-gerardo-lemus/) - if the other signals offer better payouts this weight will decrease).\n",
" \n",
"\n",
"## Remarks\n",
"\n",
"Initially, we would disregard the pca trade due to it being arbitrarily below a 2 multiple ratio, and having a so-so 1.5 ex-ante SharpeRatio, but \n",
"* after forward simulations, we found the 'real life' ex-ante sharpe could change the perspective of entering trades (by adding the profit targets and stop losses explicitely), and\n",
"* we also have a formula (Kelly) to optimally allocate the risk in the case if the trader want to maximize wealth. \n",
"\n",
"Dedicated traders can now use this (in sample) triggers and levels and test the (out of sample) performance in an algorithmic strategy, or use it as inputs from a discretionary book.\n",
"\n",
"\n"
]
}
]
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment