|
19 | 19 | import pandas as pd
|
20 | 20 | import numpy as np
|
21 | 21 | from math import pow
|
22 |
| -from scipy import stats, optimize |
| 22 | +from scipy import stats, optimize, interpolate |
23 | 23 | from six import iteritems
|
24 | 24 | from sys import float_info
|
25 | 25 |
|
@@ -405,6 +405,118 @@ def max_drawdown(returns, out=None):
|
405 | 405 | roll_max_drawdown = _create_unary_vectorized_roll_function(max_drawdown)
|
406 | 406 |
|
407 | 407 |
|
| 408 | +def expected_max_drawdown(mu, sigma, t, gbm=False): |
| 409 | + """ |
| 410 | + Determines the expected maximum drawdown of a brownian motion, |
| 411 | + given drift and diffusion |
| 412 | + |
| 413 | + If a geometric Brownian motion with stochastic differential equation |
| 414 | + dS(t) = Mu0 * S(t) * dt + Sigma0 * S(t) * dW(t) , |
| 415 | + it converts to the form here by Ito's Lemma with X(t) = log(S(t)) such that |
| 416 | + Mu = Mu0 - 0.5 * Sigma0^2 |
| 417 | + Sigma = Sigma0 . |
| 418 | +
|
| 419 | + Parameters |
| 420 | + ---------- |
| 421 | + mu : float |
| 422 | + The drift term of a Brownian motion with drift. |
| 423 | + sigma : float |
| 424 | + The diffusion term of a Brownian motion with drift. |
| 425 | + t : float |
| 426 | + A time period of interest |
| 427 | + gbp : bool |
| 428 | + If true, compute for geometric brownian motion |
| 429 | +
|
| 430 | + Returns |
| 431 | + ------- |
| 432 | + expected_max_drawdown : float |
| 433 | +
|
| 434 | + Note |
| 435 | + ----- |
| 436 | + See http://www.cs.rpi.edu/~magdon/ps/journal/drawdown_journal.pdf for more details. |
| 437 | + """ |
| 438 | + |
| 439 | + if gbm: |
| 440 | + new_mu = mu - 0.5 * sigma**2 |
| 441 | + else: |
| 442 | + new_mu = mu |
| 443 | + |
| 444 | + def emdd_qp(x): |
| 445 | + """ Return value for Q functions based on lookup table, for positive drifts""" |
| 446 | + A = [ 0.0005, 0.001, 0.0015, 0.002, 0.0025, 0.005, |
| 447 | + 0.0075, 0.01, 0.0125, 0.015, 0.0175, 0.02, |
| 448 | + 0.0225, 0.025, 0.0275, 0.03, 0.0325, 0.035, |
| 449 | + 0.0375, 0.04, 0.0425, 0.045, 0.0475, 0.05, |
| 450 | + 0.055, 0.06, 0.065, 0.07, 0.075, 0.08, |
| 451 | + 0.085, 0.09, 0.095, 0.1, 0.15, 0.2, |
| 452 | + 0.25, 0.3, 0.35, 0.4, 0.45, 0.5, |
| 453 | + 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, |
| 454 | + 4.0, 4.5, 5.0, 10.0, 15.0, 20.0, |
| 455 | + 25.0, 30.0, 35.0, 40.0, 45.0, 50.0, |
| 456 | + 100.0, 150.0, 200.0, 250.0, 300.0, 350.0, |
| 457 | + 400.0, 450.0, 500.0, 1000.0, 1500.0, 2000.0, |
| 458 | + 2500.0, 3000.0, 3500.0, 4000.0, 4500.0, 5000.0 ] |
| 459 | + B = [ 0.01969, 0.027694, 0.033789, 0.038896, 0.043372, 0.060721, |
| 460 | + 0.073808, 0.084693, 0.094171, 0.102651, 0.110375, 0.117503, |
| 461 | + 0.124142, 0.130374, 0.136259, 0.141842, 0.147162, 0.152249, |
| 462 | + 0.157127, 0.161817, 0.166337, 0.170702, 0.174924, 0.179015, |
| 463 | + 0.186842, 0.194248, 0.201287, 0.207999, 0.214421, 0.220581, |
| 464 | + 0.226505, 0.232212, 0.237722, 0.24305, 0.288719, 0.325071, |
| 465 | + 0.355581, 0.382016, 0.405415, 0.426452, 0.445588, 0.463159, |
| 466 | + 0.588642, 0.668992, 0.72854, 0.775976, 0.815456, 0.849298, |
| 467 | + 0.878933, 0.905305, 0.92907, 1.088998, 1.184918, 1.253794, |
| 468 | + 1.307607, 1.351794, 1.389289, 1.42186, 1.450654, 1.476457, |
| 469 | + 1.647113, 1.747485, 1.818873, 1.874323, 1.919671, 1.958037, |
| 470 | + 1.991288, 2.02063, 2.046885, 2.219765, 2.320983, 2.392826, |
| 471 | + 2.448562, 2.494109, 2.532622, 2.565985, 2.595416, 2.621743 ] |
| 472 | + if x > 5000: |
| 473 | + return 0.25 * np.log(x) + 0.49088 |
| 474 | + elif x < 0.0005: |
| 475 | + return 0.5 * np.sqrt(np.pi * x) |
| 476 | + else: |
| 477 | + return interpolate.interp1d(A, B)([x])[0] |
| 478 | + |
| 479 | + def emdd_qn(x): |
| 480 | + """ Return value for Q functions based on lookup table, for negative drifts""" |
| 481 | + A = [ 0.0005, 0.001, 0.0015, 0.002, 0.0025, 0.005, |
| 482 | + 0.0075, 0.01, 0.0125, 0.015, 0.0175, 0.02, |
| 483 | + 0.0225, 0.025, 0.0275, 0.03, 0.0325, 0.035, |
| 484 | + 0.0375, 0.04, 0.0425, 0.045, 0.0475, 0.05, |
| 485 | + 0.055, 0.06, 0.065, 0.07, 0.075, 0.08, |
| 486 | + 0.085, 0.09, 0.095, 0.1, 0.15, 0.2, |
| 487 | + 0.25, 0.3, 0.35, 0.4, 0.45, 0.5, |
| 488 | + 0.75, 1.0, 1.25, 1.5, 1.75, 2.0, |
| 489 | + 2.25, 2.5, 2.75, 3.0, 3.25, 3.5, |
| 490 | + 3.75, 4.0, 4.25, 4.5, 4.75, 5.0 ] |
| 491 | + B = [ 0.019965, 0.028394, 0.034874, 0.040369, 0.045256, 0.064633, |
| 492 | + 0.079746, 0.092708, 0.104259, 0.114814, 0.124608, 0.133772, |
| 493 | + 0.142429, 0.150739, 0.158565, 0.166229, 0.173756, 0.180793, |
| 494 | + 0.187739, 0.194489, 0.201094, 0.207572, 0.213877, 0.220056, |
| 495 | + 0.231797, 0.243374, 0.254585, 0.265472, 0.27607, 0.286406, |
| 496 | + 0.296507, 0.306393, 0.316066, 0.325586, 0.413136, 0.491599, |
| 497 | + 0.564333, 0.633007, 0.698849, 0.762455, 0.824484, 0.884593, |
| 498 | + 1.17202, 1.44552, 1.70936, 1.97074, 2.22742, 2.48396, |
| 499 | + 2.73676, 2.99094, 3.24354, 3.49252, 3.74294, 3.99519, |
| 500 | + 4.24274, 4.49238, 4.73859, 4.99043, 5.24083, 5.49882 ] |
| 501 | + if x > 5: |
| 502 | + return x + 0.5 |
| 503 | + elif x < 0.0005: |
| 504 | + return 0.5 * np.sqrt(np.pi * x) |
| 505 | + else: |
| 506 | + return interpolate.interp1d(A, B)([x])[0] |
| 507 | + |
| 508 | + if ((not np.isfinite(new_mu)) | (not np.isfinite(sigma)) | (sigma <= 0)): |
| 509 | + return np.nan |
| 510 | + else: |
| 511 | + alpha = new_mu / (2 * sigma**2) |
| 512 | + if new_mu == 0: |
| 513 | + return - np.sqrt(np.pi / 2) * sigma * np.sqrt(t) |
| 514 | + elif new_mu > 0: |
| 515 | + return - emdd_qp(alpha * new_mu * t) / alpha |
| 516 | + else: |
| 517 | + return emdd_qn(alpha * new_mu * t) / alpha |
| 518 | + |
| 519 | + |
408 | 520 | def annual_return(returns, period=DAILY, annualization=None):
|
409 | 521 | """
|
410 | 522 | Determines the mean annual growth rate of returns. This is equivilent
|
|
0 commit comments