[
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n**Describe the bug**\nA clear and concise description of what the bug is.\n\n**To Reproduce**\nSteps to reproduce the behavior:\n1. Go to '...'\n2. Click on '....'\n3. Scroll down to '....'\n4. See error\n\n**Expected behavior**\nA clear and concise description of what you expected to happen.\n\n**Screenshots**\nIf applicable, add screenshots to help explain your problem.\n\n**Desktop (please complete the following information):**\n - OS: [e.g. iOS]\n - Browser [e.g. chrome, safari]\n - Version [e.g. 22]\n\n**Additional context**\nAdd any other context about the problem here.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "content": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n**Is your feature request related to a problem? Please describe.**\nA clear and concise description of what the problem is. Ex. I'm always frustrated when X happens [...]\n\n**Describe the solution you'd like**\nA clear and concise description of what you want to happen.\n\n**Describe alternatives you've considered**\nA clear and concise description of any alternative solutions or features you've considered.\n\n**Additional context**\nAdd any other context or screenshots about the feature request here.\n"
  },
  {
    "path": ".github/workflows/python-publish.yml",
    "content": "name: Publish to PyPI.org\non:\n  release:\n    types: [published]\njobs:\n  pypi:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v3\n        with:\n          fetch-depth: 0\n      - run: python3 -m pip install --upgrade build && python3 -m build\n      - name: Publish package\n        uses: pypa/gh-action-pypi-publish@release/v1\n        with:\n          password: ${{ secrets.PYPI_API_TOKEN }}\n"
  },
  {
    "path": "EigenLedger/__init__.py",
    "content": "from .main import *\nfrom .modules.empyrical import *"
  },
  {
    "path": "EigenLedger/main.py",
    "content": "import numpy as np\nimport pandas as pd\nimport datetime as dt\nimport quantstats as qs\nfrom IPython.display import display\nimport matplotlib.pyplot as plt\nimport copy\nimport yfinance as yf\nfrom fpdf import FPDF\nimport warnings\nimport logging\nfrom modules.empyrical import (\n    cagr,\n    cum_returns,\n    stability_of_timeseries,\n    max_drawdown,\n    sortino_ratio,\n    alpha_beta,\n    tail_ratio,\n)\nfrom pypfopt import (\n    EfficientFrontier,\n    risk_models,\n    expected_returns,\n    HRPOpt,\n    objective_functions,\n    # black_litterman,\n    # BlackLittermanModel,\n)\n\nwarnings.filterwarnings(\"ignore\")\nlogging.getLogger('matplotlib.font_manager').disabled = True\nlogging.getLogger('matplotlib.legend').disabled = True\nTODAY = dt.date.today()\nBENCHMARK = [\"SPY\"]\nDAYS_IN_YEAR = 365\n\nrebalance_periods = {\n    \"daily\": DAYS_IN_YEAR / 365,\n    \"weekly\": DAYS_IN_YEAR / 52,\n    \"monthly\": DAYS_IN_YEAR / 12,\n    \"month\": DAYS_IN_YEAR / 12,\n    \"m\": DAYS_IN_YEAR / 12,\n    \"quarterly\": DAYS_IN_YEAR / 4,\n    \"quarter\": DAYS_IN_YEAR / 4,\n    \"q\": DAYS_IN_YEAR / 4,\n    \"6m\": DAYS_IN_YEAR / 2,\n    \"2q\": DAYS_IN_YEAR / 2,\n    \"1y\": DAYS_IN_YEAR,\n    \"year\": DAYS_IN_YEAR,\n    \"y\": DAYS_IN_YEAR,\n    \"2y\": DAYS_IN_YEAR * 2,\n}\n\n#defining colors for the allocation pie\nCS = [\n          \"#ff9999\",\n          \"#66b3ff\",\n          \"#99ff99\",\n          \"#ffcc99\",\n          \"#f6c9ff\",\n          \"#a6fff6\",\n          \"#fffeb8\",\n          \"#ffe1d4\",\n          \"#cccdff\",\n          \"#fad6ff\",\n      ]\n\nclass Engine:\n    def __init__(\n        self,\n        start_date,\n        portfolio,\n        weights=None,\n        rebalance=None,\n        benchmark=None,\n        end_date=TODAY,\n        optimizer=None,\n        max_vol=0.15,\n        diversification=1,\n        expected_returns=None,\n        risk_model=None,\n        # confidences=None,\n        # view=None,\n        min_weights=None,\n        max_weights=None,\n        risk_manager=None,\n        data=pd.DataFrame(),\n        benchmark_data=pd.DataFrame(),\n    ):\n        if benchmark is None:\n            benchmark = BENCHMARK\n\n        self.start_date = start_date\n        self.end_date = end_date\n        self.portfolio = portfolio\n        self.weights = weights\n        self.benchmark = benchmark\n        self.optimizer = optimizer\n        self.rebalance = rebalance\n        self.max_vol = max_vol\n        self.diversification = diversification\n        self.expected_returns = expected_returns\n        if expected_returns is not None:\n            assert expected_returns in [\"mean_historical_return\", \"ema_historical_return\", \"capm_return\"], f\"Expected return method: {expected_returns} not supported yet! \\n Set an appropriate expected returns parameter to your portfolio: mean_historical_return, ema_historical_return or capm_return.\"\n        self.risk_model = risk_model\n        if risk_model is not None:\n            assert risk_model in [\"sample_cov\", \"semicovariance\", \"exp_cov\", \"ledoit_wolf\", \"ledoit_wolf_constant_variance\", \"ledoit_wolf_single_factor\", \"ledoit_wolf_constant_correlation\", \"oracle_approximating\"], f\"Risk model: {risk_model} not supported yet! \\n Set an appropriate risk model to your portfolio: sample_cov, semicovariance, exp_cov, ledoit_wolf, ledoit_wolf_constant_variance, ledoit_wolf_single_factor, ledoit_wolf_constant_correlation, oracle_approximating.\"\n        self.max_weights = max_weights\n        self.min_weights = min_weights\n        self.risk_manager = risk_manager\n        self.data = data\n        self.benchmark_data = benchmark_data # To hold benchmark data\n\n        optimizers = {\n            \"EF\": efficient_frontier,\n            \"MEANVAR\": mean_var,\n            \"HRP\": hrp,\n            \"MINVAR\": min_var,\n        }\n        if self.optimizer is None and self.weights is None:\n            self.weights = [1.0 / len(self.portfolio)] * len(self.portfolio)\n        elif self.optimizer in optimizers.keys():\n            if self.optimizer == \"MEANVAR\":\n                self.weights = optimizers.get(self.optimizer)(self, vol_max=max_vol, perf=False)\n            else:\n                self.weights = optimizers.get(self.optimizer)(self, perf=False)\n\n        if self.rebalance is not None:\n            self.rebalance = make_rebalance(\n                self.start_date,\n                self.end_date,\n                self.optimizer,\n                self.portfolio,\n                self.rebalance,\n                self.weights,\n                self.max_vol,\n                self.diversification,\n                self.min_weights,\n                self.max_weights,\n                self.expected_returns,\n                self.risk_model\n            )\n            \n    def fetch_benchmark_data(self):\n        \"\"\"Fetch benchmark data using Yahoo Finance or validate custom benchmark data.\"\"\"\n        if isinstance(self.benchmark, str):\n            self.benchmark = [self.benchmark]  # Convert to list for consistency\n\n        # If `self.data` has benchmark columns, use them\n        if not self.data.empty and all(b in self.data.columns for b in self.benchmark):\n            self.benchmark_data = self.data[self.benchmark]\n        else:\n            # Fetch data from Yahoo Finance\n            try:\n                self.benchmark_data = yf.download(self.benchmark, start=self.start_date)[\"Adj Close\"]\n            except Exception as e:\n                print(f\"Error fetching benchmark data: {e}\")\n                self.benchmark_data = pd.DataFrame()\n                \nclass PortfolioAnalysisResult:\n    pass\n\ndef get_returns(stocks, wts, start_date, end_date=TODAY):\n    logging.info(\"Entering get_returns function with stocks: %s\", stocks)\n    \n    # Ensure `stocks` is a list\n    if isinstance(stocks, str):\n        stocks = [stocks]\n    \n    # Validate weights length\n    if len(wts) != len(stocks):\n        logging.error(\"Length mismatch: stocks=%s, weights=%s\", len(stocks), len(wts))\n        raise ValueError(\"Weights and stocks lists must have the same length.\")\n    \n    # Initialize lists for tracking available and missing stocks\n    available_stocks = []\n    missing_stocks = []\n    \n    # Check each stock ticker and attempt to download its data\n    for stock in stocks:\n        try:\n            asset = yf.download(stock, start=start_date, end=end_date, progress=False)[\"Adj Close\"]\n            if not asset.empty:\n                available_stocks.append(stock)\n            else:\n                missing_stocks.append(stock)\n        except Exception as e:\n            logging.error(\"Error downloading data for %s: %s\", stock, e)\n            missing_stocks.append(stock)\n    \n    # Raise an error if any stocks are missing\n    if missing_stocks:\n        logging.error(\"Missing stock(s): %s\", missing_stocks)\n        raise ValueError(f\"Some stock(s) are missing in the downloaded data: {missing_stocks}\")\n    \n    # Proceed with downloading and processing data for available stocks\n    logging.info(\"Available stocks for processing: %s\", available_stocks)\n    if len(available_stocks) > 1:\n        assets = yf.download(available_stocks, start=start_date, end=end_date, progress=False)[\"Adj Close\"]\n        assets = assets.filter(available_stocks)\n        \n        # Calculate initial allocation\n        initial_alloc = wts / assets.iloc[0]\n        logging.debug(\"Initial allocation calculated: %s\", initial_alloc)\n        \n        if initial_alloc.isna().any():\n            logging.error(\"Some stock is not available at initial state for: %s\", available_stocks)\n            raise ValueError(\"Some stock is not available at initial state!\")\n        \n        # Calculate portfolio value and returns\n        portfolio_value = (assets * initial_alloc).sum(axis=1)\n        logging.debug(\"Portfolio value: %s\", portfolio_value)\n        \n        returns = portfolio_value.pct_change().dropna()\n        logging.info(\"Returning returns with multiple stocks.\")\n        return returns\n    \n    elif len(available_stocks) == 1:\n        df = yf.download(available_stocks[0], start=start_date, end=end_date, progress=False)[\"Adj Close\"]\n        df = pd.DataFrame(df)\n        returns = df.pct_change().dropna()\n        logging.info(\"Returning returns for single stock.\")\n        return returns\n    \n    else:\n        logging.error(\"No valid stocks found for download.\")\n        raise ValueError(\"No valid stocks were found in the provided list.\")\n\n\n\ndef get_returns_from_data(data, wts, stocks):\n    assets = data.filter(stocks)\n    initial_alloc = wts/assets.iloc[0]\n    if initial_alloc.isna().any():\n        raise ValueError(\"Some stock is not available at initial state!\")\n    portfolio_value = (assets * initial_alloc).sum(axis=1)\n    returns = portfolio_value.pct_change()[1:]\n    return returns\n\ndef get_returns_from_benchmark_data(data, wts, stocks):\n    if wts is None:\n        wts = [1]\n        \n    assets = data.filter(stocks)\n    initial_alloc = wts/assets.iloc[0]\n    if initial_alloc.isna().any():\n        raise ValueError(\"Some stock is not available at initial state!\")\n    benchmark_value = (assets * initial_alloc).sum(axis=1)\n    returns = benchmark_value.pct_change()[1:]\n    return returns\n\ndef calculate_information_ratio(returns, benchmark_returns, days=252) -> float:\n    return_difference = returns - benchmark_returns\n    volatility = return_difference.std() * np.sqrt(days)\n    information_ratio_result = return_difference.mean() / volatility\n    return information_ratio_result\n\n\ndef graph_allocation(my_portfolio):\n    fig1, ax1 = plt.subplots()\n    ax1.pie(\n        my_portfolio.weights,\n        labels=my_portfolio.portfolio,\n        autopct=\"%1.1f%%\",\n        shadow=False,\n    )\n    ax1.axis(\"equal\")  # Equal aspect ratio ensures that pie is drawn as a circle.\n    plt.title(\"Portfolio's allocation\")\n    plt.show()\n\n\ndef portfolio_analysis(my_portfolio, rf=0.0, sigma_value=1, confidence_value=0.95, report=False, filename=\"report.pdf\"):\n    # Fetch benchmark data\n    my_portfolio.fetch_benchmark_data()\n    \n    result = PortfolioAnalysisResult()\n    \n    # Handling rebalance data and getting returns\n    if isinstance(my_portfolio.rebalance, pd.DataFrame):\n        # we want to get the dataframe with the dates and weights\n        rebalance_schedule = my_portfolio.rebalance\n\n        columns = []\n\n        for date in rebalance_schedule.columns:\n            date = date[0:10]\n            columns.append(date)\n        rebalance_schedule.columns = columns\n\n        # then want to make a list of the dates and start with our first date\n        dates = [my_portfolio.start_date]\n\n        # then our rebalancing dates into that list\n        dates = dates + rebalance_schedule.columns.to_list()\n\n        datess = []\n        for date in dates:\n            date = date[0:10]\n            datess.append(date)\n        dates = datess\n        # this will hold returns\n        returns = pd.Series()\n\n        # then we want to be able to call the dates like tuples\n        for i in range(len(dates) - 1):\n            # get our weights\n            weights = rebalance_schedule[str(dates[i + 1])]\n\n            # then we want to get the returns\n\n            add_returns = get_returns(\n                my_portfolio.portfolio,\n                weights,\n                start_date=dates[i],\n                end_date=dates[i + 1],\n            )\n\n            # then append those returns\n            returns = returns._append(add_returns)\n    else:\n        if not my_portfolio.data.empty:\n            returns = get_returns_from_data(my_portfolio.data, my_portfolio.weights, my_portfolio.portfolio)\n        else:\n            returns = get_returns(\n                my_portfolio.portfolio,\n                my_portfolio.weights,\n                start_date=my_portfolio.start_date,\n                end_date=my_portfolio.end_date,\n            )\n\n    creturns = (returns + 1).cumprod()\n\n    # risk manager\n    try:\n        if list(my_portfolio.risk_manager.keys())[0] == \"Stop Loss\":\n\n            values = []\n            for r in creturns:\n                if r <= 1 + my_portfolio.risk_manager[\"Stop Loss\"]:\n                    values.append(r)\n                else:\n                    pass\n\n            try:\n                date = creturns[creturns == values[0]].index[0]\n                date = str(date.to_pydatetime())\n                my_portfolio.end_date = date[0:10]\n                returns = returns[: my_portfolio.end_date]\n\n            except Exception as e:\n                pass\n\n        if list(my_portfolio.risk_manager.keys())[0] == \"Take Profit\":\n\n            values = []\n            for r in creturns:\n                if r >= 1 + my_portfolio.risk_manager[\"Take Profit\"]:\n                    values.append(r)\n                else:\n                    pass\n\n            try:\n                date = creturns[creturns == values[0]].index[0]\n                date = str(date.to_pydatetime())\n                my_portfolio.end_date = date[0:10]\n                returns = returns[: my_portfolio.end_date]\n\n            except Exception as e:\n                pass\n\n        if list(my_portfolio.risk_manager.keys())[0] == \"Max Drawdown\":\n\n            drawdown = qs.stats.to_drawdown_series(returns)\n\n            values = []\n            for r in drawdown:\n                if r <= my_portfolio.risk_manager[\"Max Drawdown\"]:\n                    values.append(r)\n                else:\n                    pass\n\n            try:\n                date = drawdown[drawdown == values[0]].index[0]\n                date = str(date.to_pydatetime())\n                my_portfolio.end_date = date[0:10]\n                returns = returns[: my_portfolio.end_date]\n\n            except Exception as e:\n                pass\n\n    except Exception as e:\n        pass\n    print(\"Start date: \" + str(my_portfolio.start_date))\n    print(\"End date: \" + str(my_portfolio.end_date))\n        \n    if not my_portfolio.benchmark_data.empty:\n        print(\"Portfolio Data (my_portfolio.data):\")\n        print(my_portfolio.data.head())\n        print(\"Portfolio Columns:\", my_portfolio.portfolio)\n        print(\"Weights:\", my_portfolio.weights)\n        print(\"\\nBenchmark Data (my_portfolio.benchmark_data):\")\n        print(my_portfolio.benchmark_data.head())  # Raw benchmark data (prices)\n        \n        # Check if benchmark_data is a Series or DataFrame and print accordingly\n        if isinstance(my_portfolio.benchmark_data, pd.Series):\n            print(\"benchmark Columns:\", my_portfolio.benchmark_data.tolist())\n        elif isinstance(my_portfolio.benchmark_data, pd.DataFrame):\n            print(\"Benchmark Columns:\", my_portfolio.benchmark_data.columns.tolist())  \n        \n        wts = [1]\n        \n        # Get benchmark returns from portfolio data\n        benchmark = get_returns_from_benchmark_data(my_portfolio.data, my_portfolio.weights, my_portfolio.portfolio)\n        \n        print(\"Benchmark Returns from Portfolio Data:\")\n        print(benchmark.head())  # Inspect the first few rows of returns\n    else:\n        print(\"Benchmark Tickers (my_portfolio.benchmark):\", my_portfolio.benchmark)\n        print(\"Start Date:\", my_portfolio.start_date)\n        print(\"End Date:\", my_portfolio.end_date)\n        \n        benchmark = get_returns(\n            my_portfolio.benchmark,\n            wts=[1],\n            start_date=my_portfolio.start_date,\n            end_date=my_portfolio.end_date,\n        ).dropna()\n        \n        print(\"Benchmark Returns from Yahoo Finance (or fallback):\")\n        print(benchmark.head())\n        \n    \n    # # Fetch benchmark returns\n    # benchmark = get_returns(\n    #     my_portfolio.benchmark,\n    #     wts=[1],\n    #     start_date=my_portfolio.start_date,\n    #     end_date=my_portfolio.end_date,\n    # )\n    # benchmark = benchmark.dropna()\n\n    CAGR = cagr(returns, period='daily', annualization=None)\n    # CAGR = round(CAGR, 2)\n    # CAGR = CAGR.tolist()\n    CAGR = str(round(CAGR * 100, 2)) + \"%\"\n\n    CUM = cum_returns(returns, starting_value=0, out=None) * 100\n    CUM = CUM.iloc[-1]\n    CUM = CUM.tolist()\n    CUM = str(round(CUM, 2)) + \"%\"\n\n    VOL = qs.stats.volatility(returns, annualize=True)\n    \n    VOL = VOL.tolist()\n    VOL = str(round(VOL * 100, 2)) + \" %\"\n\n    SR = qs.stats.sharpe(returns, rf=rf)\n    SR = np.round(SR, decimals=2)\n    SR = str(SR)\n\n    result.SR = SR\n\n    CR = qs.stats.calmar(returns)\n    CR = CR.tolist()\n    CR = str(round(CR, 2))\n\n    result.CR = CR\n\n    STABILITY = stability_of_timeseries(returns)\n    STABILITY = round(STABILITY, 2)\n    STABILITY = str(STABILITY)\n\n    MD = max_drawdown(returns, out=None)\n    MD = str(round(MD * 100, 2)) + \" %\"\n\n    \"\"\"OR = omega_ratio(returns, risk_free=0.0, required_return=0.0)\n    OR = round(OR,2)\n    OR = str(OR)\n    print(OR)\"\"\"\n\n    SOR = sortino_ratio(returns, required_return=0, period='daily')\n    SOR = round(SOR, 2)\n    SOR = str(SOR)\n\n    SK = qs.stats.skew(returns)\n    SK = round(SK, 2)\n    if isinstance(SK, float):\n        SK = [SK]  \n    elif isinstance(SK, (list, np.ndarray)):\n        SK = SK.tolist() \n    SK = str(SK)\n\n    KU = qs.stats.kurtosis(returns)\n    KU = round(KU, 2)\n    if isinstance(KU, float):\n        KU = [KU]  \n    elif isinstance(KU, (list, np.ndarray)):\n        KU = KU.tolist() \n    KU = str(KU)\n\n    TA = tail_ratio(returns)\n    TA = round(TA, 2)\n    TA = str(TA)\n\n    CSR = qs.stats.common_sense_ratio(returns)\n    CSR = round(CSR, 2)\n    CSR = CSR.tolist()\n    CSR = str(CSR)\n\n    VAR = qs.stats.value_at_risk(\n        returns, sigma=sigma_value, confidence=confidence_value\n    )\n    VAR = np.round(VAR, decimals=2)\n    VAR = str(VAR * 100) + \" %\"\n\n    returns = returns.tz_localize(None)  # Making tz-naive\n    benchmark = benchmark.tz_localize(None) # Making tz-naive\n\n    alpha, beta = alpha_beta(returns, benchmark, risk_free=rf)\n    AL = round(alpha, 2)\n    BTA = round(beta, 2)\n\n    def condition(x):\n        return x > 0\n\n    win = sum(condition(x) for x in returns)\n    total = len(returns)\n    win_ratio = win / total\n    win_ratio = win_ratio * 100\n    win_ratio = round(win_ratio, 2)\n\n    # IR = calculate_information_ratio(returns, benchmark.iloc[:, 0])\n    \n    if isinstance(benchmark, pd.Series):\n        IR = calculate_information_ratio(returns, benchmark)\n    else:\n        IR = calculate_information_ratio(returns, benchmark.iloc[:, 0])\n\n    IR = round(IR, 2)\n\n    data = {\n        \"\": [\n            \"Annual return\",\n            \"Cumulative return\",\n            \"Annual volatility\",\n            \"Winning day ratio\",\n            \"Sharpe ratio\",\n            \"Calmar ratio\",\n            \"Information ratio\",\n            \"Stability\",\n            \"Max Drawdown\",\n            \"Sortino ratio\",\n            \"Skew\",\n            \"Kurtosis\",\n            \"Tail Ratio\",\n            \"Common sense ratio\",\n            \"Daily value at risk\",\n            \"Alpha\",\n            \"Beta\",\n        ],\n        \"Backtest\": [\n            CAGR,\n            CUM,\n            VOL,\n            f\"{win_ratio}%\",\n            SR,\n            CR,\n            IR,\n            STABILITY,\n            MD,\n            SOR,\n            SK,\n            KU,\n            TA,\n            CSR,\n            VAR,\n            AL,\n            BTA,\n        ],\n    }\n\n    # Create DataFrame\n    df = pd.DataFrame(data)\n    df.set_index(\"\", inplace=True)\n    df.style.set_properties(\n        **{\"background-color\": \"white\", \"color\": \"black\", \"border-color\": \"black\"}\n    )\n    display(df)\n\n    result.df = data\n\n    y = []\n    for x in returns:\n        y.append(x)\n\n    arr = np.array(y)\n    # arr\n    # returns.index\n    my_color = np.where(arr >= 0, \"blue\", \"grey\")\n    ret = plt.figure(figsize=(30, 8))\n    plt.vlines(x=returns.index, ymin=0, ymax=arr, color=my_color, alpha=0.4)\n    plt.title(\"Returns\")\n\n    result.returns = returns\n    result.creturns = creturns\n    result.benchmark = benchmark\n    result.CAGR = CAGR\n    result.CUM = CUM\n    result.VOL = VOL\n    result.SR = SR\n    result.win_ratio = win_ratio\n    result.CR = CR\n    result.IR = IR\n    result.STABILITY = STABILITY\n    result.MD = MD\n    result.SOR = SOR\n    result.SK = SK\n    result.KU = KU\n    result.TA = TA\n    result.CSR = CSR\n    result.VAR = VAR\n    result.AL = AL\n    result.BTA = BTA\n\n    try:\n        result.orderbook = make_rebalance.output\n    except Exception as e:\n        OrderBook = pd.DataFrame(\n            {\n                \"Assets\": my_portfolio.portfolio,\n                \"Allocation\": my_portfolio.weights,\n            }\n        )\n\n        result.orderbook = OrderBook.T\n\n    wts = copy.deepcopy(my_portfolio.weights)\n    indices = [i for i, x in enumerate(wts) if x == 0.0]\n\n    while 0.0 in wts:\n        wts.remove(0.0)\n\n    for i in sorted(indices, reverse=True):\n        del my_portfolio.portfolio[i]\n        \n    if not returns.empty:\n        if not report:\n            qs.plots.returns(returns, benchmark, cumulative=True)\n            qs.plots.yearly_returns(returns, benchmark),\n            qs.plots.monthly_heatmap(returns, benchmark)\n            qs.plots.drawdown(returns)\n            qs.plots.drawdowns_periods(returns)\n            # qs.plots.rolling_volatility(returns)\n            # qs.plots.rolling_sharpe(returns)\n            qs.plots.rolling_beta(returns, benchmark)\n            graph_opt(my_portfolio.portfolio, wts, pie_size=7, font_size=14)\n\n        else:\n            qs.plots.returns(returns, benchmark, cumulative=True, savefig=\"retbench.png\")\n            qs.plots.yearly_returns(returns, benchmark, savefig=\"y_returns.png\"),\n            qs.plots.monthly_heatmap(returns, benchmark, savefig=\"heatmap.png\")\n            qs.plots.drawdown(returns, savefig=\"drawdown.png\")\n            qs.plots.drawdowns_periods(returns, savefig=\"d_periods.png\")\n            # qs.plots.rolling_volatility(returns, savefig=\"rvol.png\")\n            qs.plots.rolling_sharpe(returns, savefig=\"rsharpe.png\")\n            qs.plots.rolling_beta(returns, benchmark, savefig=\"rbeta.png\")\n            graph_opt(my_portfolio.portfolio, wts, pie_size=7, font_size=14, save=True)\n            pdf = FPDF()\n            pdf.add_page()\n            pdf.set_font(\"arial\", \"B\", 14)\n            pdf.image(\n                \"https://user-images.githubusercontent.com/61618641/120909011-98f8a180-c670-11eb-8844-2d423ba3fa9c.png\",\n                x=None,\n                y=None,\n                w=45,\n                h=5,\n                type=\"\",\n                link=\"https://github.com/ssantoshp/E\",\n            )\n            pdf.cell(20, 15, f\"Report\", ln=1)\n            pdf.set_font(\"arial\", size=11)\n            pdf.image(\"allocation.png\", x=135, y=0, w=70, h=70, type=\"\", link=\"\")\n            pdf.cell(20, 7, f\"Start date: \" + str(my_portfolio.start_date), ln=1)\n            pdf.cell(20, 7, f\"End date: \" + str(my_portfolio.end_date), ln=1)\n            ret.savefig(\"ret.png\")\n\n            pdf.cell(20, 7, f\"\", ln=1)\n            pdf.cell(20, 7, f\"Annual return: \" + str(CAGR), ln=1)\n            pdf.cell(20, 7, f\"Cumulative return: \" + str(CUM), ln=1)\n            pdf.cell(20, 7, f\"Annual volatility: \" + str(VOL), ln=1)\n            pdf.cell(20, 7, f\"Winning day ratio: \" + str(win_ratio), ln=1)\n            pdf.cell(20, 7, f\"Sharpe ratio: \" + str(SR), ln=1)\n            pdf.cell(20, 7, f\"Calmar ratio: \" + str(CR), ln=1)\n            pdf.cell(20, 7, f\"Information ratio: \" + str(IR), ln=1)\n            pdf.cell(20, 7, f\"Stability: \" + str(STABILITY), ln=1)\n            pdf.cell(20, 7, f\"Max drawdown: \" + str(MD), ln=1)\n            pdf.cell(20, 7, f\"Sortino ratio: \" + str(SOR), ln=1)\n            pdf.cell(20, 7, f\"Skew: \" + str(SK), ln=1)\n            pdf.cell(20, 7, f\"Kurtosis: \" + str(KU), ln=1)\n            pdf.cell(20, 7, f\"Tail ratio: \" + str(TA), ln=1)\n            pdf.cell(20, 7, f\"Common sense ratio: \" + str(CSR), ln=1)\n            pdf.cell(20, 7, f\"Daily value at risk: \" + str(VAR), ln=1)\n            pdf.cell(20, 7, f\"Alpha: \" + str(AL), ln=1)\n            pdf.cell(20, 7, f\"Beta: \" + str(BTA), ln=1)\n\n            pdf.image(\"ret.png\", x=-20, y=None, w=250, h=80, type=\"\", link=\"\")\n            pdf.cell(20, 7, f\"\", ln=1)\n            pdf.image(\"y_returns.png\", x=-20, y=None, w=200, h=100, type=\"\", link=\"\")\n            pdf.cell(20, 7, f\"\", ln=1)\n            pdf.image(\"retbench.png\", x=None, y=None, w=200, h=100, type=\"\", link=\"\")\n            pdf.cell(20, 7, f\"\", ln=1)\n            pdf.image(\"heatmap.png\", x=None, y=None, w=200, h=80, type=\"\", link=\"\")\n            pdf.cell(20, 7, f\"\", ln=1)\n            pdf.image(\"drawdown.png\", x=None, y=None, w=200, h=80, type=\"\", link=\"\")\n            pdf.cell(20, 7, f\"\", ln=1)\n            pdf.image(\"d_periods.png\", x=None, y=None, w=200, h=80, type=\"\", link=\"\")\n            pdf.cell(20, 7, f\"\", ln=1)\n            pdf.image(\"rvol.png\", x=None, y=None, w=190, h=80, type=\"\", link=\"\")\n            pdf.cell(20, 7, f\"\", ln=1)\n            pdf.image(\"rsharpe.png\", x=None, y=None, w=190, h=80, type=\"\", link=\"\")\n            pdf.cell(20, 7, f\"\", ln=1)\n            pdf.image(\"rbeta.png\", x=None, y=None, w=190, h=80, type=\"\", link=\"\")\n\n            pdf.output(dest=\"F\", name=filename)\n            print(\"The PDF was generated successfully!\")\n\n    return result\n\n\ndef flatten(subject) -> list:\n    muster = []\n    for item in subject:\n        if isinstance(item, (list, tuple, set)):\n            muster.extend(flatten(item))\n        else:\n            muster.append(item)\n    return muster\n\n\ndef graph_opt(my_portfolio, my_weights, pie_size, font_size, save=False):\n    fig1, ax1 = plt.subplots()\n    fig1.set_size_inches(pie_size, pie_size)\n    ax1.pie(my_weights, labels=my_portfolio, autopct=\"%1.1f%%\", shadow=False, colors=CS)\n    ax1.axis(\"equal\")  # Equal aspect ratio ensures that pie is drawn as a circle.\n    plt.rcParams[\"font.size\"] = font_size\n    if save:\n      plt.savefig(\"allocation.png\")\n    plt.show()\n\n\ndef equal_weighting(my_portfolio) -> list:\n    return [1.0 / len(my_portfolio.portfolio)] * len(my_portfolio.portfolio)\n\ndef efficient_frontier(my_portfolio, perf=True) -> list:\n    # changed to take in desired timeline, the problem is that it would use all historical data\n    ohlc = yf.download(\n        my_portfolio.portfolio,\n        start=my_portfolio.start_date,\n        end=my_portfolio.end_date,\n        progress=False,\n    )\n    prices = ohlc[\"Adj Close\"].dropna(how=\"all\")\n    df = prices.filter(my_portfolio.portfolio)\n\n    # sometimes we will pick a date range where company isn't public we can't set price to 0 so it has to go to 1\n    df = df.fillna(1)\n    if my_portfolio.expected_returns == None:\n        my_portfolio.expected_returns = 'mean_historical_return'\n    if my_portfolio.risk_model == None:\n        my_portfolio.risk_model = 'sample_cov'\n    mu = expected_returns.return_model(df, method=my_portfolio.expected_returns)\n    S = risk_models.risk_matrix(df, method=my_portfolio.risk_model)\n\n    # optimize for max sharpe ratio\n    ef = EfficientFrontier(mu, S)\n    ef.add_objective(objective_functions.L2_reg, gamma=my_portfolio.diversification)\n    if my_portfolio.min_weights is not None:\n        ef.add_constraint(lambda x: x >= my_portfolio.min_weights)\n    if my_portfolio.max_weights is not None:\n        ef.add_constraint(lambda x: x <= my_portfolio.max_weights)\n    weights = ef.max_sharpe()\n    cleaned_weights = ef.clean_weights()\n    wts = cleaned_weights.items()\n\n    result = []\n    for val in wts:\n        a, b = map(list, zip(*[val]))\n        result.append(b)\n\n    if perf is True:\n        pred = ef.portfolio_performance(verbose=True)\n\n    return flatten(result)\n\n\ndef hrp(my_portfolio, perf=True) -> list:\n    # changed to take in desired timeline, the problem is that it would use all historical data\n\n    ohlc = yf.download(\n        my_portfolio.portfolio,\n        start=my_portfolio.start_date,\n        end=my_portfolio.end_date,\n        progress=False,\n    )\n    prices = ohlc[\"Adj Close\"].dropna(how=\"all\")\n    prices = prices.filter(my_portfolio.portfolio)\n\n    # sometimes we will pick a date range where company isn't public we can't set price to 0 so it has to go to 1\n    prices = prices.fillna(1)\n\n    rets = expected_returns.returns_from_prices(prices)\n    hrp = HRPOpt(rets)\n    hrp.optimize()\n    weights = hrp.clean_weights()\n\n    wts = weights.items()\n\n    result = []\n    for val in wts:\n        a, b = map(list, zip(*[val]))\n        result.append(b)\n\n    if perf is True:\n        hrp.portfolio_performance(verbose=True)\n\n    return flatten(result)\n\n\ndef mean_var(my_portfolio, vol_max=0.15, perf=True) -> list:\n    # changed to take in desired timeline, the problem is that it would use all historical data\n\n    ohlc = yf.download(\n        my_portfolio.portfolio,\n        start=my_portfolio.start_date,\n        end=my_portfolio.end_date,\n        progress=False,\n    )\n    prices = ohlc[\"Adj Close\"].dropna(how=\"all\")\n    prices = prices.filter(my_portfolio.portfolio)\n\n    # sometimes we will pick a date range where company isn't public we can't set price to 0 so it has to go to 1\n    prices = prices.fillna(1)\n\n    if my_portfolio.expected_returns == None:\n        my_portfolio.expected_returns = 'capm_return'\n    if my_portfolio.risk_model == None:\n        my_portfolio.risk_model = 'ledoit_wolf'\n\n    mu = expected_returns.return_model(prices, method=my_portfolio.expected_returns)\n    S = risk_models.risk_matrix(prices, method=my_portfolio.risk_model)\n\n    ef = EfficientFrontier(mu, S)\n    ef.add_objective(objective_functions.L2_reg, gamma=my_portfolio.diversification)\n    if my_portfolio.min_weights is not None:\n        ef.add_constraint(lambda x: x >= my_portfolio.min_weights)\n    if my_portfolio.max_weights is not None:\n        ef.add_constraint(lambda x: x <= my_portfolio.max_weights)\n    ef.efficient_risk(vol_max)\n    weights = ef.clean_weights()\n\n    wts = weights.items()\n\n    result = []\n    for val in wts:\n        a, b = map(list, zip(*[val]))\n        result.append(b)\n\n    if perf is True:\n        ef.portfolio_performance(verbose=True)\n\n    return flatten(result)\n\n\ndef min_var(my_portfolio, perf=True) -> list:\n    ohlc = yf.download(\n        my_portfolio.portfolio,\n        start=my_portfolio.start_date,\n        end=my_portfolio.end_date,\n        progress=False,\n    )\n    prices = ohlc[\"Adj Close\"].dropna(how=\"all\")\n    prices = prices.filter(my_portfolio.portfolio)\n\n    if my_portfolio.expected_returns == None:\n        my_portfolio.expected_returns = 'capm_return'\n    if my_portfolio.risk_model == None:\n            my_portfolio.risk_model = 'ledoit_wolf'\n\n    mu = expected_returns.return_model(prices, method=my_portfolio.expected_returns)\n    S = risk_models.risk_matrix(prices, method=my_portfolio.risk_model)\n\n    ef = EfficientFrontier(mu, S)\n    ef.add_objective(objective_functions.L2_reg, gamma=my_portfolio.diversification)\n    if my_portfolio.min_weights is not None:\n        ef.add_constraint(lambda x: x >= my_portfolio.min_weights)\n    if my_portfolio.max_weights is not None:\n        ef.add_constraint(lambda x: x <= my_portfolio.max_weights)\n    ef.min_volatility()\n    weights = ef.clean_weights()\n\n    wts = weights.items()\n\n    result = []\n    for val in wts:\n        a, b = map(list, zip(*[val]))\n        result.append(b)\n\n    if perf is True:\n        ef.portfolio_performance(verbose=True)\n\n    return flatten(result)\n\n\ndef optimize_portfolio(my_portfolio, vol_max=25, pie_size=5, font_size=14):\n    if my_portfolio.optimizer == None:\n        raise Exception(\"You didn't define any optimizer in your portfolio!\")\n    returns1 = get_returns(\n        my_portfolio.portfolio,\n        equal_weighting(my_portfolio),\n        start_date=my_portfolio.start_date,\n        end_date=my_portfolio.end_date,\n    )\n    creturns1 = (returns1 + 1).cumprod()\n\n    port = copy.deepcopy(my_portfolio.portfolio)\n\n    wts = [1.0 / len(my_portfolio.portfolio)] * len(my_portfolio.portfolio)\n\n    optimizers = {\n        \"EF\": efficient_frontier,\n        \"MEANVAR\": mean_var,\n        \"HRP\": hrp,\n        \"MINVAR\": min_var,\n    }\n\n    if my_portfolio.optimizer in optimizers.keys():\n        if my_portfolio.optimizer == \"MEANVAR\":\n            wts = optimizers.get(my_portfolio.optimizer)(my_portfolio, my_portfolio.max_vol)\n        else:\n            wts = optimizers.get(my_portfolio.optimizer)(my_portfolio)\n    else:\n        opt = my_portfolio.optimizer\n        my_portfolio.weights = opt()\n\n    print(\"\\n\")\n\n    indices = [i for i, x in enumerate(wts) if x == 0.0]\n\n    while 0.0 in wts:\n        wts.remove(0.0)\n\n    for i in sorted(indices, reverse=True):\n        del port[i]\n\n    graph_opt(port, wts, pie_size, font_size)\n\n    print(\"\\n\")\n\n    returns2 = get_returns(\n        port, wts, start_date=my_portfolio.start_date, end_date=my_portfolio.end_date\n    )\n    creturns2 = (returns2 + 1).cumprod()\n\n    plt.rcParams[\"font.size\"] = 13\n    plt.figure(figsize=(30, 10))\n    plt.xlabel(\"Portfolio vs Benchmark\")\n\n    ax1 = creturns1.plot(color=\"blue\", label=\"Without optimization\")\n    ax2 = creturns2.plot(color=\"red\", label=\"With optimization\")\n\n    h1, l1 = ax1.get_legend_handles_labels()\n    h2, l2 = ax2.get_legend_handles_labels()\n\n    plt.legend(l1 + l2, loc=2)\n    plt.show()\n\n\ndef check_schedule(rebalance) -> bool:\n    valid_schedule = False\n    if rebalance.lower() in rebalance_periods.keys():\n        valid_schedule = True\n    return valid_schedule\n\n\ndef valid_range(start_date, end_date, rebalance) -> tuple:\n\n    # make the start date to a datetime\n    start_date = dt.datetime.strptime(start_date, \"%Y-%m-%d\")\n\n    # custom dates don't need further chekings\n    if type(rebalance) is list:\n        return start_date, rebalance[-1]\n\n    # make the end date to a datetime\n    end_date = dt.datetime.strptime(str(end_date), \"%Y-%m-%d\")\n\n    # gets the number of days\n    days = (end_date - start_date).days\n\n    # checking that date range covers rebalance period\n    if rebalance in rebalance_periods.keys() and days <= (int(rebalance_periods[rebalance])):\n        raise KeyError(\"Date Range does not encompass rebalancing interval\")\n\n    # we will needs these dates later on so we'll return them back\n    return start_date, end_date\n\n\ndef get_date_range(start_date, end_date, rebalance) -> list:\n    # this will keep track of the rebalancing dates and we want to start on the first date\n    rebalance_dates = [start_date]\n    input_date = start_date\n\n    if rebalance in rebalance_periods.keys():\n        # run for an arbitrarily large number we'll resolve this by breaking when we break the equality\n        for i in range(1000):\n            days = rebalance_periods.get(rebalance, 0.0)\n            if days is None:\n                raise ValueError(\"Rebalance period cannot be None\")\n            \n            # Increment the date based on the selected period\n            input_date = input_date + timedelta(days=days)\n            if input_date <= end_date:\n                # Append the new date if it is earlier or equal to the final date\n                rebalance_dates.append(input_date)\n            else:\n                # Break when the next rebalance date is later than our end date\n                break\n\n    # then we want to return those dates\n    return rebalance_dates\n\ndef make_rebalance(\n    start_date,\n    end_date,\n    optimize,\n    portfolio_input,\n    rebalance,\n    allocation,\n    vol_max,\n    div,\n    min,\n    max,\n    expected_returns,\n    risk_model,\n) -> pd.DataFrame:\n    sdate = str(start_date)[:10]\n    if rebalance[0] != sdate:\n\n        # makes sure the start date matches the first element of the list of custom rebalance dates\n        if type(rebalance) is list:\n            raise KeyError(\"the rebalance dates and start date doesn't match\")\n\n        # makes sure that the value passed through for rebalancing is a valid one\n        valid_schedule = check_schedule(rebalance)\n\n        if valid_schedule is False:\n            raise KeyError(\"Not an accepted rebalancing schedule\")\n\n    # this checks to make sure that the date range given works for the rebalancing\n    start_date, end_date = valid_range(start_date, end_date, rebalance)\n\n    # this function will get us the specific dates\n    if rebalance[0] != sdate:\n        dates = get_date_range(start_date, end_date, rebalance)\n    else:\n        dates = rebalance\n\n    # we are going to make columns with the end date and the weights\n    columns = [\"end_date\"] + portfolio_input\n\n    # then make a dataframe with the index being the tickers\n    output_df = pd.DataFrame(index=portfolio_input)\n\n    for i in range(len(dates) - 1):\n\n        try:\n            portfolio = Engine(\n                start_date=dates[0],\n                end_date=dates[i + 1],\n                portfolio=portfolio_input,\n                weights=allocation,\n                optimizer=\"{}\".format(optimize),\n                max_vol=vol_max,\n                diversification=div,\n                min_weights=min,\n                max_weights=max,\n                expected_returns=expected_returns,\n                risk_model=risk_model,\n            )\n\n        except TypeError:\n            portfolio = Engine(\n                start_date=dates[0],\n                end_date=dates[i + 1],\n                portfolio=portfolio_input,\n                weights=allocation,\n                optimizer=optimize,\n                max_vol=vol_max,\n                diversification=div,\n                min_weights=min,\n                max_weights=max,\n                expected_returns=expected_returns,\n                risk_model=risk_model,\n            )\n\n        output_df[\"{}\".format(dates[i + 1])] = portfolio.weights\n\n    # we have to run it one more time to get what the optimization is for up to today's date\n    try:\n        portfolio = Engine(\n            start_date=dates[0],\n            portfolio=portfolio_input,\n            weights=allocation,\n            optimizer=\"{}\".format(optimize),\n            max_vol=vol_max,\n            diversification=div,\n            min_weights=min,\n            max_weights=max,\n            expected_returns=expected_returns,\n            risk_model=risk_model,\n        )\n\n    except TypeError:\n        portfolio = Engine(\n            start_date=dates[0],\n            portfolio=portfolio_input,\n            weights=allocation,\n            optimizer=optimize,\n            max_vol=vol_max,\n            diversification=div,\n            min_weights=min,\n            max_weights=max,\n            expected_returns=expected_returns,\n            risk_model=risk_model,\n        )\n\n    output_df[\"{}\".format(TODAY)] = portfolio.weights\n\n    make_rebalance.output = output_df\n    print(\"Rebalance schedule: \")\n    print(output_df)\n    return output_df\n\n"
  },
  {
    "path": "EigenLedger/modules/empyrical/.gitattributes",
    "content": "empyrical/_version.py export-subst\n"
  },
  {
    "path": "EigenLedger/modules/empyrical/.gitignore",
    "content": "# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packaging\n.Python\nenv/\nbuild/\ndevelop-eggs/\ndist/\ndownloads/\neggs/\n.eggs/\nlib/\nlib64/\nparts/\nsdist/\nvar/\n*.egg-info/\n.installed.cfg\n*.egg\n\n# PyInstaller\n#  Usually these files are written by a python script from a template\n#  before PyInstaller builds the exe, so as to inject date/other infos into it.\n*.manifest\n*.spec\n\n# Installer logs\npip-log.txt\npip-delete-this-directory.txt\n\n# Unit test / coverage reports\nhtmlcov/\n.tox/\n.coverage\n.coverage.*\n.cache\nnosetests.xml\ncoverage.xml\n*,cover\n.hypothesis/\n\n# Translations\n*.mo\n*.pot\n\n# Django stuff:\n*.log\n\n# Sphinx documentation\ndocs/_build/\n\n# PyBuilder\ntarget/\n\n#Ipython Notebook\n.ipynb_checkpoints\n\n# JetBrains\n.idea/\n"
  },
  {
    "path": "EigenLedger/modules/empyrical/.travis.yml",
    "content": "language: python\nsudo: false\n\nmatrix:\n  include:\n    - python: 2.7\n      env: PANDAS_VERSION=0.24.2 NUMPY_VERSION=1.12.1 SCIPY_VERSION=1.2.1 LIBGFORTRAN_VERSION=3.0\n    - python: 2.7\n      env: PANDAS_VERSION=0.20.1 NUMPY_VERSION=1.12.1 SCIPY_VERSION=0.19.0 LIBGFORTRAN_VERSION=3.0\n    - python: 3.6\n      env: PANDAS_VERSION=1.0.4 NUMPY_VERSION=1.18.4 SCIPY_VERSION=1.4.1 LIBGFORTRAN_VERSION=3.0\n    - python: 3.6\n      env: PANDAS_VERSION=0.20.1 NUMPY_VERSION=1.12.1 SCIPY_VERSION=0.19.0 LIBGFORTRAN_VERSION=3.0\n    - python: 3.6\n      env: PANDAS_VERSION=1.0.4 NUMPY_VERSION=1.18.4 SCIPY_VERSION=1.4.1 LIBGFORTRAN_VERSION=3.0\n    - python: 3.7\n      env: PANDAS_VERSION=1.0.4  NUMPY_VERSION=1.18.4 SCIPY_VERSION=1.4.1 LIBGFORTRAN_VERSION=3.0\n\nbefore_install:\n  # We do this conditionally because it saves us some downloading if the\n  # version is the same.\n  - if [[ \"$TRAVIS_PYTHON_VERSION\" == \"2.7\" ]]; then\n      wget https://repo.continuum.io/miniconda/Miniconda-latest-Linux-x86_64.sh -O miniconda.sh;\n    else\n      wget https://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh -O miniconda.sh;\n    fi\n  - bash miniconda.sh -b -p $HOME/miniconda\n  - export PATH=\"$HOME/miniconda/bin:$PATH\"\n  - conda config --set always_yes yes --set changeps1 no\n  - conda update -q conda\n\ninstall:\n  - conda create -n testenv --yes -c conda-forge pip python=$TRAVIS_PYTHON_VERSION numpy=$NUMPY_VERSION pandas=$PANDAS_VERSION scipy=$SCIPY_VERSION libgfortran=$LIBGFORTRAN_VERSION\n  - source activate testenv\n  - pip install -e .[dev]\n\nbefore_script:\n  - \"flake8 .\"\n\nscript:\n  - nosetests\n  - source deactivate\n\nnotifications:\n  email: false\n\nbranches:\n  only:\n    - master\n"
  },
  {
    "path": "EigenLedger/modules/empyrical/__init__.py",
    "content": "#\n# Copyright 2016 Quantopian, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# flake8: noqa\n\nfrom ._version import get_versions\n__version__ = get_versions()['version']\ndel get_versions\n\nfrom .stats import (\n    aggregate_returns,\n    alpha,\n    alpha_aligned,\n    alpha_beta,\n    alpha_beta_aligned,\n    annual_return,\n    annual_volatility,\n    beta,\n    beta_aligned,\n    cagr,\n    beta_fragility_heuristic,\n    beta_fragility_heuristic_aligned,\n    gpd_risk_estimates,\n    gpd_risk_estimates_aligned,\n    calmar_ratio,\n    capture,\n    conditional_value_at_risk,\n    cum_returns,\n    cum_returns_final,\n    down_alpha_beta,\n    down_capture,\n    downside_risk,\n    excess_sharpe,\n    max_drawdown,\n    omega_ratio,\n    roll_alpha,\n    roll_alpha_aligned,\n    roll_alpha_beta,\n    roll_alpha_beta,\n    roll_alpha_beta_aligned,\n    roll_annual_volatility,\n    roll_beta,\n    roll_beta_aligned,\n    roll_down_capture,\n    roll_max_drawdown,\n    roll_sharpe_ratio,\n    roll_sortino_ratio,\n    roll_up_capture,\n    roll_up_down_capture,\n    sharpe_ratio,\n    simple_returns,\n    sortino_ratio,\n    stability_of_timeseries,\n    tail_ratio,\n    up_alpha_beta,\n    up_capture,\n    up_down_capture,\n    value_at_risk,\n)\n\nfrom .periods import (\n    DAILY,\n    WEEKLY,\n    MONTHLY,\n    QUARTERLY,\n    YEARLY\n)\n\n\nfrom .perf_attrib import (\n    perf_attrib,\n    compute_exposures,\n)\n"
  },
  {
    "path": "EigenLedger/modules/empyrical/_version.py",
    "content": "\n# This file helps to compute a version number in source trees obtained from\n# git-archive tarball (such as those provided by githubs download-from-tag\n# feature). Distribution tarballs (built by setup.py sdist) and build\n# directories (produced by setup.py build) will contain a much shorter file\n# that just contains the computed version number.\n\n# This file is released into the public domain. Generated by\n# versioneer-0.16 (https://github.com/warner/python-versioneer)\n\n\"\"\"Git implementation of _version.py.\"\"\"\n\nimport errno\nimport os\nimport re\nimport subprocess\nimport sys\n\n\ndef get_keywords():\n    \"\"\"Get the keywords needed to look up the version information.\"\"\"\n    # these strings will be replaced by git during git-archive.\n    # setup.py/versioneer.py will grep for the variable names, so they must\n    # each be defined on a line of their own. _version.py will just call\n    # get_keywords().\n    git_refnames = \"$Format:%d$\"\n    git_full = \"$Format:%H$\"\n    keywords = {\"refnames\": git_refnames, \"full\": git_full}\n    return keywords\n\n\nclass VersioneerConfig:\n    \"\"\"Container for Versioneer configuration parameters.\"\"\"\n\n\ndef get_config():\n    \"\"\"Create, populate and return the VersioneerConfig() object.\"\"\"\n    # these strings are filled in when 'setup.py versioneer' creates\n    # _version.py\n    cfg = VersioneerConfig()\n    cfg.VCS = \"git\"\n    cfg.style = \"pep440\"\n    cfg.tag_prefix = \"\"\n    cfg.parentdir_prefix = \"empyrical-\"\n    cfg.versionfile_source = \"empyrical/_version.py\"\n    cfg.verbose = False\n    return cfg\n\n\nclass NotThisMethod(Exception):\n    \"\"\"Exception raised if a method is not valid for the current scenario.\"\"\"\n\n\nLONG_VERSION_PY = {}\nHANDLERS = {}\n\n\ndef register_vcs_handler(vcs, method):  # decorator\n    \"\"\"Decorator to mark a method as the handler for a particular VCS.\"\"\"\n    def decorate(f):\n        \"\"\"Store f in HANDLERS[vcs][method].\"\"\"\n        if vcs not in HANDLERS:\n            HANDLERS[vcs] = {}\n        HANDLERS[vcs][method] = f\n        return f\n    return decorate\n\n\ndef run_command(commands, args, cwd=None, verbose=False, hide_stderr=False):\n    \"\"\"Call the given command(s).\"\"\"\n    assert isinstance(commands, list)\n    p = None\n    for c in commands:\n        try:\n            dispcmd = str([c] + args)\n            # remember shell=False, so use git.cmd on windows, not just git\n            p = subprocess.Popen([c] + args, cwd=cwd, stdout=subprocess.PIPE,\n                                 stderr=(subprocess.PIPE if hide_stderr\n                                         else None))\n            break\n        except EnvironmentError:\n            e = sys.exc_info()[1]\n            if e.errno == errno.ENOENT:\n                continue\n            if verbose:\n                print(\"unable to run %s\" % dispcmd)\n                print(e)\n            return None\n    else:\n        if verbose:\n            print(\"unable to find command, tried %s\" % (commands,))\n        return None\n    stdout = p.communicate()[0].strip()\n    if sys.version_info[0] >= 3:\n        stdout = stdout.decode()\n    if p.returncode != 0:\n        if verbose:\n            print(\"unable to run %s (error)\" % dispcmd)\n        return None\n    return stdout\n\n\ndef versions_from_parentdir(parentdir_prefix, root, verbose):\n    \"\"\"Try to determine the version from the parent directory name.\n\n    Source tarballs conventionally unpack into a directory that includes\n    both the project name and a version string.\n    \"\"\"\n    dirname = os.path.basename(root)\n    if not dirname.startswith(parentdir_prefix):\n        if verbose:\n            print(\"guessing rootdir is '%s', but '%s' doesn't start with \"\n                  \"prefix '%s'\" % (root, dirname, parentdir_prefix))\n        raise NotThisMethod(\"rootdir doesn't start with parentdir_prefix\")\n    return {\"version\": dirname[len(parentdir_prefix):],\n            \"full-revisionid\": None,\n            \"dirty\": False, \"error\": None}\n\n\n@register_vcs_handler(\"git\", \"get_keywords\")\ndef git_get_keywords(versionfile_abs):\n    \"\"\"Extract version information from the given file.\"\"\"\n    # the code embedded in _version.py can just fetch the value of these\n    # keywords. When used from setup.py, we don't want to import _version.py,\n    # so we do it with a regexp instead. This function is not used from\n    # _version.py.\n    keywords = {}\n    try:\n        f = open(versionfile_abs, \"r\")\n        for line in f.readlines():\n            if line.strip().startswith(\"git_refnames =\"):\n                mo = re.search(r'=\\s*\"(.*)\"', line)\n                if mo:\n                    keywords[\"refnames\"] = mo.group(1)\n            if line.strip().startswith(\"git_full =\"):\n                mo = re.search(r'=\\s*\"(.*)\"', line)\n                if mo:\n                    keywords[\"full\"] = mo.group(1)\n        f.close()\n    except EnvironmentError:\n        pass\n    return keywords\n\n\n@register_vcs_handler(\"git\", \"keywords\")\ndef git_versions_from_keywords(keywords, tag_prefix, verbose):\n    \"\"\"Get version information from git keywords.\"\"\"\n    if not keywords:\n        raise NotThisMethod(\"no keywords at all, weird\")\n    refnames = keywords[\"refnames\"].strip()\n    if refnames.startswith(\"$Format\"):\n        if verbose:\n            print(\"keywords are unexpanded, not using\")\n        raise NotThisMethod(\"unexpanded keywords, not a git-archive tarball\")\n    refs = set([r.strip() for r in refnames.strip(\"()\").split(\",\")])\n    # starting in git-1.8.3, tags are listed as \"tag: foo-1.0\" instead of\n    # just \"foo-1.0\". If we see a \"tag: \" prefix, prefer those.\n    TAG = \"tag: \"\n    tags = set([r[len(TAG):] for r in refs if r.startswith(TAG)])\n    if not tags:\n        # Either we're using git < 1.8.3, or there really are no tags. We use\n        # a heuristic: assume all version tags have a digit. The old git %d\n        # expansion behaves like git log --decorate=short and strips out the\n        # refs/heads/ and refs/tags/ prefixes that would let us distinguish\n        # between branches and tags. By ignoring refnames without digits, we\n        # filter out many common branch names like \"release\" and\n        # \"stabilization\", as well as \"HEAD\" and \"master\".\n        tags = set([r for r in refs if re.search(r'\\d', r)])\n        if verbose:\n            print(\"discarding '%s', no digits\" % \",\".join(refs-tags))\n    if verbose:\n        print(\"likely tags: %s\" % \",\".join(sorted(tags)))\n    for ref in sorted(tags):\n        # sorting will prefer e.g. \"2.0\" over \"2.0rc1\"\n        if ref.startswith(tag_prefix):\n            r = ref[len(tag_prefix):]\n            if verbose:\n                print(\"picking %s\" % r)\n            return {\"version\": r,\n                    \"full-revisionid\": keywords[\"full\"].strip(),\n                    \"dirty\": False, \"error\": None\n                    }\n    # no suitable tags, so version is \"0+unknown\", but full hex is still there\n    if verbose:\n        print(\"no suitable tags, using unknown + full revision id\")\n    return {\"version\": \"0+unknown\",\n            \"full-revisionid\": keywords[\"full\"].strip(),\n            \"dirty\": False, \"error\": \"no suitable tags\"}\n\n\n@register_vcs_handler(\"git\", \"pieces_from_vcs\")\ndef git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command):\n    \"\"\"Get version from 'git describe' in the root of the source tree.\n\n    This only gets called if the git-archive 'subst' keywords were *not*\n    expanded, and _version.py hasn't already been rewritten with a short\n    version string, meaning we're inside a checked out source tree.\n    \"\"\"\n    if not os.path.exists(os.path.join(root, \".git\")):\n        if verbose:\n            print(\"no .git in %s\" % root)\n        raise NotThisMethod(\"no .git directory\")\n\n    GITS = [\"git\"]\n    if sys.platform == \"win32\":\n        GITS = [\"git.cmd\", \"git.exe\"]\n    # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty]\n    # if there isn't one, this yields HEX[-dirty] (no NUM)\n    describe_out = run_command(GITS, [\"describe\", \"--tags\", \"--dirty\",\n                                      \"--always\", \"--long\",\n                                      \"--match\", \"%s*\" % tag_prefix],\n                               cwd=root)\n    # --long was added in git-1.5.5\n    if describe_out is None:\n        raise NotThisMethod(\"'git describe' failed\")\n    describe_out = describe_out.strip()\n    full_out = run_command(GITS, [\"rev-parse\", \"HEAD\"], cwd=root)\n    if full_out is None:\n        raise NotThisMethod(\"'git rev-parse' failed\")\n    full_out = full_out.strip()\n\n    pieces = {}\n    pieces[\"long\"] = full_out\n    pieces[\"short\"] = full_out[:7]  # maybe improved later\n    pieces[\"error\"] = None\n\n    # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty]\n    # TAG might have hyphens.\n    git_describe = describe_out\n\n    # look for -dirty suffix\n    dirty = git_describe.endswith(\"-dirty\")\n    pieces[\"dirty\"] = dirty\n    if dirty:\n        git_describe = git_describe[:git_describe.rindex(\"-dirty\")]\n\n    # now we have TAG-NUM-gHEX or HEX\n\n    if \"-\" in git_describe:\n        # TAG-NUM-gHEX\n        mo = re.search(r'^(.+)-(\\d+)-g([0-9a-f]+)$', git_describe)\n        if not mo:\n            # unparseable. Maybe git-describe is misbehaving?\n            pieces[\"error\"] = (\"unable to parse git-describe output: '%s'\"\n                               % describe_out)\n            return pieces\n\n        # tag\n        full_tag = mo.group(1)\n        if not full_tag.startswith(tag_prefix):\n            if verbose:\n                fmt = \"tag '%s' doesn't start with prefix '%s'\"\n                print(fmt % (full_tag, tag_prefix))\n            pieces[\"error\"] = (\"tag '%s' doesn't start with prefix '%s'\"\n                               % (full_tag, tag_prefix))\n            return pieces\n        pieces[\"closest-tag\"] = full_tag[len(tag_prefix):]\n\n        # distance: number of commits since tag\n        pieces[\"distance\"] = int(mo.group(2))\n\n        # commit: short hex revision ID\n        pieces[\"short\"] = mo.group(3)\n\n    else:\n        # HEX: no tags\n        pieces[\"closest-tag\"] = None\n        count_out = run_command(GITS, [\"rev-list\", \"HEAD\", \"--count\"],\n                                cwd=root)\n        pieces[\"distance\"] = int(count_out)  # total number of commits\n\n    return pieces\n\n\ndef plus_or_dot(pieces):\n    \"\"\"Return a + if we don't already have one, else return a .\"\"\"\n    if \"+\" in pieces.get(\"closest-tag\", \"\"):\n        return \".\"\n    return \"+\"\n\n\ndef render_pep440(pieces):\n    \"\"\"Build up version string, with post-release \"local version identifier\".\n\n    Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you\n    get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty\n\n    Exceptions:\n    1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty]\n    \"\"\"\n    if pieces[\"closest-tag\"]:\n        rendered = pieces[\"closest-tag\"]\n        if pieces[\"distance\"] or pieces[\"dirty\"]:\n            rendered += plus_or_dot(pieces)\n            rendered += \"%d.g%s\" % (pieces[\"distance\"], pieces[\"short\"])\n            if pieces[\"dirty\"]:\n                rendered += \".dirty\"\n    else:\n        # exception #1\n        rendered = \"0+untagged.%d.g%s\" % (pieces[\"distance\"],\n                                          pieces[\"short\"])\n        if pieces[\"dirty\"]:\n            rendered += \".dirty\"\n    return rendered\n\n\ndef render_pep440_pre(pieces):\n    \"\"\"TAG[.post.devDISTANCE] -- No -dirty.\n\n    Exceptions:\n    1: no tags. 0.post.devDISTANCE\n    \"\"\"\n    if pieces[\"closest-tag\"]:\n        rendered = pieces[\"closest-tag\"]\n        if pieces[\"distance\"]:\n            rendered += \".post.dev%d\" % pieces[\"distance\"]\n    else:\n        # exception #1\n        rendered = \"0.post.dev%d\" % pieces[\"distance\"]\n    return rendered\n\n\ndef render_pep440_post(pieces):\n    \"\"\"TAG[.postDISTANCE[.dev0]+gHEX] .\n\n    The \".dev0\" means dirty. Note that .dev0 sorts backwards\n    (a dirty tree will appear \"older\" than the corresponding clean one),\n    but you shouldn't be releasing software with -dirty anyways.\n\n    Exceptions:\n    1: no tags. 0.postDISTANCE[.dev0]\n    \"\"\"\n    if pieces[\"closest-tag\"]:\n        rendered = pieces[\"closest-tag\"]\n        if pieces[\"distance\"] or pieces[\"dirty\"]:\n            rendered += \".post%d\" % pieces[\"distance\"]\n            if pieces[\"dirty\"]:\n                rendered += \".dev0\"\n            rendered += plus_or_dot(pieces)\n            rendered += \"g%s\" % pieces[\"short\"]\n    else:\n        # exception #1\n        rendered = \"0.post%d\" % pieces[\"distance\"]\n        if pieces[\"dirty\"]:\n            rendered += \".dev0\"\n        rendered += \"+g%s\" % pieces[\"short\"]\n    return rendered\n\n\ndef render_pep440_old(pieces):\n    \"\"\"TAG[.postDISTANCE[.dev0]] .\n\n    The \".dev0\" means dirty.\n\n    Eexceptions:\n    1: no tags. 0.postDISTANCE[.dev0]\n    \"\"\"\n    if pieces[\"closest-tag\"]:\n        rendered = pieces[\"closest-tag\"]\n        if pieces[\"distance\"] or pieces[\"dirty\"]:\n            rendered += \".post%d\" % pieces[\"distance\"]\n            if pieces[\"dirty\"]:\n                rendered += \".dev0\"\n    else:\n        # exception #1\n        rendered = \"0.post%d\" % pieces[\"distance\"]\n        if pieces[\"dirty\"]:\n            rendered += \".dev0\"\n    return rendered\n\n\ndef render_git_describe(pieces):\n    \"\"\"TAG[-DISTANCE-gHEX][-dirty].\n\n    Like 'git describe --tags --dirty --always'.\n\n    Exceptions:\n    1: no tags. HEX[-dirty]  (note: no 'g' prefix)\n    \"\"\"\n    if pieces[\"closest-tag\"]:\n        rendered = pieces[\"closest-tag\"]\n        if pieces[\"distance\"]:\n            rendered += \"-%d-g%s\" % (pieces[\"distance\"], pieces[\"short\"])\n    else:\n        # exception #1\n        rendered = pieces[\"short\"]\n    if pieces[\"dirty\"]:\n        rendered += \"-dirty\"\n    return rendered\n\n\ndef render_git_describe_long(pieces):\n    \"\"\"TAG-DISTANCE-gHEX[-dirty].\n\n    Like 'git describe --tags --dirty --always -long'.\n    The distance/hash is unconditional.\n\n    Exceptions:\n    1: no tags. HEX[-dirty]  (note: no 'g' prefix)\n    \"\"\"\n    if pieces[\"closest-tag\"]:\n        rendered = pieces[\"closest-tag\"]\n        rendered += \"-%d-g%s\" % (pieces[\"distance\"], pieces[\"short\"])\n    else:\n        # exception #1\n        rendered = pieces[\"short\"]\n    if pieces[\"dirty\"]:\n        rendered += \"-dirty\"\n    return rendered\n\n\ndef render(pieces, style):\n    \"\"\"Render the given version pieces into the requested style.\"\"\"\n    if pieces[\"error\"]:\n        return {\"version\": \"unknown\",\n                \"full-revisionid\": pieces.get(\"long\"),\n                \"dirty\": None,\n                \"error\": pieces[\"error\"]}\n\n    if not style or style == \"default\":\n        style = \"pep440\"  # the default\n\n    if style == \"pep440\":\n        rendered = render_pep440(pieces)\n    elif style == \"pep440-pre\":\n        rendered = render_pep440_pre(pieces)\n    elif style == \"pep440-post\":\n        rendered = render_pep440_post(pieces)\n    elif style == \"pep440-old\":\n        rendered = render_pep440_old(pieces)\n    elif style == \"git-describe\":\n        rendered = render_git_describe(pieces)\n    elif style == \"git-describe-long\":\n        rendered = render_git_describe_long(pieces)\n    else:\n        raise ValueError(\"unknown style '%s'\" % style)\n\n    return {\"version\": rendered, \"full-revisionid\": pieces[\"long\"],\n            \"dirty\": pieces[\"dirty\"], \"error\": None}\n\n\ndef get_versions():\n    \"\"\"Get version information or return default if unable to do so.\"\"\"\n    # I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have\n    # __file__, we can work backwards from there to the root. Some\n    # py2exe/bbfreeze/non-CPython implementations don't do __file__, in which\n    # case we can only use expanded keywords.\n\n    cfg = get_config()\n    verbose = cfg.verbose\n\n    try:\n        return git_versions_from_keywords(get_keywords(), cfg.tag_prefix,\n                                          verbose)\n    except NotThisMethod:\n        pass\n\n    try:\n        root = os.path.realpath(__file__)\n        # versionfile_source is the relative path from the top of the source\n        # tree (where the .git directory might live) to this file. Invert\n        # this to find the root from __file__.\n        for i in cfg.versionfile_source.split('/'):\n            root = os.path.dirname(root)\n    except NameError:\n        return {\"version\": \"0+unknown\", \"full-revisionid\": None,\n                \"dirty\": None,\n                \"error\": \"unable to find root of source tree\"}\n\n    try:\n        pieces = git_pieces_from_vcs(cfg.tag_prefix, root, verbose)\n        return render(pieces, cfg.style)\n    except NotThisMethod:\n        pass\n\n    try:\n        if cfg.parentdir_prefix:\n            return versions_from_parentdir(cfg.parentdir_prefix, root, verbose)\n    except NotThisMethod:\n        pass\n\n    return {\"version\": \"0+unknown\", \"full-revisionid\": None,\n            \"dirty\": None,\n            \"error\": \"unable to compute version\"}\n"
  },
  {
    "path": "EigenLedger/modules/empyrical/deprecate.py",
    "content": "\"\"\"Utilities for marking deprecated functions.\"\"\"\n# Copyright 2018 Quantopian, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\nimport warnings\nfrom functools import wraps\n\n\ndef deprecated(msg=None, stacklevel=2):\n    \"\"\"\n    Used to mark a function as deprecated.\n    Parameters\n    ----------\n    msg : str\n        The message to display in the deprecation warning.\n    stacklevel : int\n        How far up the stack the warning needs to go, before\n        showing the relevant calling lines.\n    Usage\n    -----\n    @deprecated(msg='function_a is deprecated! Use function_b instead.')\n    def function_a(*args, **kwargs):\n    \"\"\"\n    def deprecated_dec(fn):\n        @wraps(fn)\n        def wrapper(*args, **kwargs):\n            warnings.warn(\n                msg or \"Function %s is deprecated.\" % fn.__name__,\n                category=DeprecationWarning,\n                stacklevel=stacklevel\n            )\n            return fn(*args, **kwargs)\n        return wrapper\n    return deprecated_dec\n"
  },
  {
    "path": "EigenLedger/modules/empyrical/perf_attrib.py",
    "content": "from collections import OrderedDict\nimport pandas as pd\n\n\ndef perf_attrib(returns,\n                positions,\n                factor_returns,\n                factor_loadings):\n    \"\"\"\n    Attributes the performance of a returns stream to a set of risk factors.\n\n    Performance attribution determines how much each risk factor, e.g.,\n    momentum, the technology sector, etc., contributed to total returns, as\n    well as the daily exposure to each of the risk factors. The returns that\n    can be attributed to one of the given risk factors are the\n    `common_returns`, and the returns that _cannot_ be attributed to a risk\n    factor are the `specific_returns`. The `common_returns` and\n    `specific_returns` summed together will always equal the total returns.\n\n    Parameters\n    ----------\n    returns : pd.Series\n        Returns for each day in the date range.\n        - Example:\n            2017-01-01   -0.017098\n            2017-01-02    0.002683\n            2017-01-03   -0.008669\n\n    positions: pd.Series\n        Daily holdings in percentages, indexed by date.\n        - Examples:\n            dt          ticker\n            2017-01-01  AAPL      0.417582\n                        TLT       0.010989\n                        XOM       0.571429\n            2017-01-02  AAPL      0.202381\n                        TLT       0.535714\n                        XOM       0.261905\n\n    factor_returns : pd.DataFrame\n        Returns by factor, with date as index and factors as columns\n        - Example:\n                        momentum  reversal\n            2017-01-01  0.002779 -0.005453\n            2017-01-02  0.001096  0.010290\n\n    factor_loadings : pd.DataFrame\n        Factor loadings for all days in the date range, with date and ticker as\n        index, and factors as columns.\n        - Example:\n                               momentum  reversal\n            dt         ticker\n            2017-01-01 AAPL   -1.592914  0.852830\n                       TLT     0.184864  0.895534\n                       XOM     0.993160  1.149353\n            2017-01-02 AAPL   -0.140009 -0.524952\n                       TLT    -1.066978  0.185435\n                       XOM    -1.798401  0.761549\n\n    Returns\n    -------\n    tuple of (risk_exposures_portfolio, perf_attribution)\n\n    risk_exposures_portfolio : pd.DataFrame\n        df indexed by datetime, with factors as columns\n        - Example:\n                        momentum  reversal\n            dt\n            2017-01-01 -0.238655  0.077123\n            2017-01-02  0.821872  1.520515\n\n    perf_attribution : pd.DataFrame\n        df with factors, common returns, and specific returns as columns,\n        and datetimes as index\n        - Example:\n                        momentum  reversal  common_returns  specific_returns\n            dt\n            2017-01-01  0.249087  0.935925        1.185012          1.185012\n            2017-01-02 -0.003194 -0.400786       -0.403980         -0.403980\n\n    Note\n    ----\n    See https://en.wikipedia.org/wiki/Performance_attribution for more details.\n    \"\"\"\n\n    # Make risk data match time range of returns\n    start = returns.index[0]\n    end = returns.index[-1]\n    factor_returns = factor_returns.loc[start:end]\n    factor_loadings = factor_loadings.loc[start:end]\n\n    factor_loadings.index = factor_loadings.index.set_names(['dt', 'ticker'])\n\n    positions = positions.copy()\n    positions.index = positions.index.set_names(['dt', 'ticker'])\n\n    risk_exposures_portfolio = compute_exposures(positions,\n                                                 factor_loadings)\n\n    perf_attrib_by_factor = risk_exposures_portfolio.multiply(factor_returns)\n    common_returns = perf_attrib_by_factor.sum(axis='columns')\n\n    tilt_exposure = risk_exposures_portfolio.mean()\n    tilt_returns = factor_returns.multiply(tilt_exposure).sum(axis='columns')\n    timing_returns = common_returns - tilt_returns\n    specific_returns = returns - common_returns\n\n    returns_df = pd.DataFrame(OrderedDict([\n        ('total_returns', returns),\n        ('common_returns', common_returns),\n        ('specific_returns', specific_returns),\n        ('tilt_returns', tilt_returns),\n        ('timing_returns', timing_returns)\n        ]))\n\n    return (risk_exposures_portfolio,\n            pd.concat([perf_attrib_by_factor, returns_df], axis='columns'))\n\n\ndef compute_exposures(positions, factor_loadings):\n    \"\"\"\n    Compute daily risk factor exposures.\n\n    Parameters\n    ----------\n    positions: pd.Series\n        A series of holdings as percentages indexed by date and ticker.\n        - Examples:\n            dt          ticker\n            2017-01-01  AAPL      0.417582\n                        TLT       0.010989\n                        XOM       0.571429\n            2017-01-02  AAPL      0.202381\n                        TLT       0.535714\n                        XOM       0.261905\n\n    factor_loadings : pd.DataFrame\n        Factor loadings for all days in the date range, with date and ticker as\n        index, and factors as columns.\n        - Example:\n                               momentum  reversal\n            dt         ticker\n            2017-01-01 AAPL   -1.592914  0.852830\n                       TLT     0.184864  0.895534\n                       XOM     0.993160  1.149353\n            2017-01-02 AAPL   -0.140009 -0.524952\n                       TLT    -1.066978  0.185435\n                       XOM    -1.798401  0.761549\n\n    Returns\n    -------\n    risk_exposures_portfolio : pd.DataFrame\n        df indexed by datetime, with factors as columns\n        - Example:\n                        momentum  reversal\n            dt\n            2017-01-01 -0.238655  0.077123\n            2017-01-02  0.821872  1.520515\n    \"\"\"\n    risk_exposures = factor_loadings.multiply(positions, axis='rows')\n    return risk_exposures.groupby(level='dt').sum()\n"
  },
  {
    "path": "EigenLedger/modules/empyrical/periods.py",
    "content": "APPROX_BDAYS_PER_MONTH = 21\nAPPROX_BDAYS_PER_YEAR = 252\n\nMONTHS_PER_YEAR = 12\nWEEKS_PER_YEAR = 52\nQTRS_PER_YEAR = 4\n\nDAILY = 'daily'\nWEEKLY = 'weekly'\nMONTHLY = 'monthly'\nQUARTERLY = 'quarterly'\nYEARLY = 'yearly'\n\nANNUALIZATION_FACTORS = {\n    DAILY: APPROX_BDAYS_PER_YEAR,\n    WEEKLY: WEEKS_PER_YEAR,\n    MONTHLY: MONTHS_PER_YEAR,\n    QUARTERLY: QTRS_PER_YEAR,\n    YEARLY: 1\n}\n"
  },
  {
    "path": "EigenLedger/modules/empyrical/stats.py",
    "content": "#\n# Copyright 2016 Quantopian, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\nfrom __future__ import division\n\nimport math\nimport pandas as pd\nimport numpy as np\nfrom math import pow\nfrom scipy import stats, optimize\nfrom six import iteritems\nfrom sys import float_info\n\nfrom .utils import nanmean, nanstd, nanmin, up, down, roll, rolling_window\nfrom .periods import ANNUALIZATION_FACTORS, APPROX_BDAYS_PER_YEAR\nfrom .periods import DAILY, WEEKLY, MONTHLY, QUARTERLY, YEARLY\n\n\ndef _create_unary_vectorized_roll_function(function):\n    def unary_vectorized_roll(arr, window, out=None, **kwargs):\n        \"\"\"\n        Computes the {human_readable} measure over a rolling window.\n\n        Parameters\n        ----------\n        arr : array-like\n            The array to compute the rolling {human_readable} over.\n        window : int\n            Size of the rolling window in terms of the periodicity of the data.\n        out : array-like, optional\n            Array to use as output buffer.\n            If not passed, a new array will be created.\n        **kwargs\n            Forwarded to :func:`~empyrical.{name}`.\n\n        Returns\n        -------\n        rolling_{name} : array-like\n            The rolling {human_readable}.\n        \"\"\"\n        allocated_output = out is None\n\n        if len(arr):\n            out = function(\n                rolling_window(_flatten(arr), min(len(arr), window)).T,\n                out=out,\n                **kwargs\n            )\n        else:\n            out = np.empty(0, dtype='float64')\n\n        if allocated_output and isinstance(arr, pd.Series):\n            out = pd.Series(out, index=arr.index[-len(out):])\n\n        return out\n\n    unary_vectorized_roll.__doc__ = unary_vectorized_roll.__doc__.format(\n        name=function.__name__,\n        human_readable=function.__name__.replace('_', ' '),\n    )\n\n    return unary_vectorized_roll\n\n\ndef _create_binary_vectorized_roll_function(function):\n    def binary_vectorized_roll(lhs, rhs, window, out=None, **kwargs):\n        \"\"\"\n        Computes the {human_readable} measure over a rolling window.\n\n        Parameters\n        ----------\n        lhs : array-like\n            The first array to pass to the rolling {human_readable}.\n        rhs : array-like\n            The second array to pass to the rolling {human_readable}.\n        window : int\n            Size of the rolling window in terms of the periodicity of the data.\n        out : array-like, optional\n            Array to use as output buffer.\n            If not passed, a new array will be created.\n        **kwargs\n            Forwarded to :func:`~empyrical.{name}`.\n\n        Returns\n        -------\n        rolling_{name} : array-like\n            The rolling {human_readable}.\n        \"\"\"\n        allocated_output = out is None\n\n        if window >= 1 and len(lhs) and len(rhs):\n            out = function(\n                rolling_window(_flatten(lhs), min(len(lhs), window)).T,\n                rolling_window(_flatten(rhs), min(len(rhs), window)).T,\n                out=out,\n                **kwargs\n            )\n        elif allocated_output:\n            out = np.empty(0, dtype='float64')\n        else:\n            out[()] = np.nan\n\n        if allocated_output:\n            if out.ndim == 1 and isinstance(lhs, pd.Series):\n                out = pd.Series(out, index=lhs.index[-len(out):])\n            elif out.ndim == 2 and isinstance(lhs, pd.Series):\n                out = pd.DataFrame(out, index=lhs.index[-len(out):])\n        return out\n\n    binary_vectorized_roll.__doc__ = binary_vectorized_roll.__doc__.format(\n        name=function.__name__,\n        human_readable=function.__name__.replace('_', ' '),\n    )\n\n    return binary_vectorized_roll\n\n\ndef _flatten(arr):\n    return arr if not isinstance(arr, pd.Series) else arr.values\n\n\ndef _adjust_returns(returns, adjustment_factor):\n    \"\"\"\n    Returns the returns series adjusted by adjustment_factor. Optimizes for the\n    case of adjustment_factor being 0 by returning returns itself, not a copy!\n\n    Parameters\n    ----------\n    returns : pd.Series or np.ndarray\n    adjustment_factor : pd.Series or np.ndarray or float or int\n\n    Returns\n    -------\n    adjusted_returns : array-like\n    \"\"\"\n    if isinstance(adjustment_factor, (float, int)) and adjustment_factor == 0:\n        return returns\n    return returns - adjustment_factor\n\n\ndef annualization_factor(period, annualization):\n    \"\"\"\n    Return annualization factor from period entered or if a custom\n    value is passed in.\n\n    Parameters\n    ----------\n    period : str, optional\n        Defines the periodicity of the 'returns' data for purposes of\n        annualizing. Value ignored if `annualization` parameter is specified.\n        Defaults are::\n\n            'monthly':12\n            'weekly': 52\n            'daily': 252\n\n    annualization : int, optional\n        Used to suppress default values available in `period` to convert\n        returns into annual returns. Value should be the annual frequency of\n        `returns`.\n\n    Returns\n    -------\n    annualization_factor : float\n    \"\"\"\n    if annualization is None:\n        try:\n            factor = ANNUALIZATION_FACTORS[period]\n        except KeyError:\n            raise ValueError(\n                \"Period cannot be '{}'. \"\n                \"Can be '{}'.\".format(\n                    period, \"', '\".join(ANNUALIZATION_FACTORS.keys())\n                )\n            )\n    else:\n        factor = annualization\n    return factor\n\n\ndef simple_returns(prices):\n    \"\"\"\n    Compute simple returns from a timeseries of prices.\n\n    Parameters\n    ----------\n    prices : pd.Series, pd.DataFrame or np.ndarray\n        Prices of assets in wide-format, with assets as columns,\n        and indexed by datetimes.\n\n    Returns\n    -------\n    returns : array-like\n        Returns of assets in wide-format, with assets as columns,\n        and index coerced to be tz-aware.\n    \"\"\"\n    if isinstance(prices, (pd.DataFrame, pd.Series)):\n        out = prices.pct_change().iloc[1:]\n    else:\n        # Assume np.ndarray\n        out = np.diff(prices, axis=0)\n        np.divide(out, prices[:-1], out=out)\n\n    return out\n\n\ndef cum_returns(returns, starting_value=0, out=None):\n    \"\"\"\n    Compute cumulative returns from simple returns.\n\n    Parameters\n    ----------\n    returns : pd.Series, np.ndarray, or pd.DataFrame\n        Returns of the strategy as a percentage, noncumulative.\n         - Time series with decimal returns.\n         - Example::\n\n            2015-07-16   -0.012143\n            2015-07-17    0.045350\n            2015-07-20    0.030957\n            2015-07-21    0.004902\n\n         - Also accepts two dimensional data. In this case, each column is\n           cumulated.\n\n    starting_value : float, optional\n       The starting returns.\n    out : array-like, optional\n        Array to use as output buffer.\n        If not passed, a new array will be created.\n\n    Returns\n    -------\n    cumulative_returns : array-like\n        Series of cumulative returns.\n    \"\"\"\n    if len(returns) < 1:\n        return returns.copy()\n\n    nanmask = np.isnan(returns)\n    if np.any(nanmask):\n        returns = returns.copy()\n        returns[nanmask] = 0\n\n    allocated_output = out is None\n    if allocated_output:\n        out = np.empty_like(returns)\n\n    np.add(returns, 1, out=out)\n    out.cumprod(axis=0, out=out)\n\n    if starting_value == 0:\n        np.subtract(out, 1, out=out)\n    else:\n        np.multiply(out, starting_value, out=out)\n\n    if allocated_output:\n        if returns.ndim == 1 and isinstance(returns, pd.Series):\n            out = pd.Series(out, index=returns.index)\n        elif isinstance(returns, pd.DataFrame):\n            out = pd.DataFrame(\n                out, index=returns.index, columns=returns.columns,\n            )\n\n    return out\n\n\ndef cum_returns_final(returns, starting_value=0):\n    \"\"\"\n    Compute total returns from simple returns.\n\n    Parameters\n    ----------\n    returns : pd.DataFrame, pd.Series, or np.ndarray\n       Noncumulative simple returns of one or more timeseries.\n    starting_value : float, optional\n       The starting returns.\n\n    Returns\n    -------\n    total_returns : pd.Series, np.ndarray, or float\n        If input is 1-dimensional (a Series or 1D numpy array), the result is a\n        scalar.\n\n        If input is 2-dimensional (a DataFrame or 2D numpy array), the result\n        is a 1D array containing cumulative returns for each column of input.\n    \"\"\"\n    if len(returns) == 0:\n        return np.nan\n\n    if isinstance(returns, pd.DataFrame):\n        result = (returns + 1).prod()\n    else:\n        result = np.nanprod(returns + 1, axis=0)\n\n    if starting_value == 0:\n        result -= 1\n    else:\n        result *= starting_value\n\n    return result\n\n\ndef aggregate_returns(returns, convert_to):\n    \"\"\"\n    Aggregates returns by week, month, or year.\n\n    Parameters\n    ----------\n    returns : pd.Series\n       Daily returns of the strategy, noncumulative.\n        - See full explanation in :func:`~empyrical.stats.cum_returns`.\n    convert_to : str\n        Can be 'weekly', 'monthly', or 'yearly'.\n\n    Returns\n    -------\n    aggregated_returns : pd.Series\n    \"\"\"\n\n    def cumulate_returns(x):\n        return cum_returns(x).iloc[-1]\n\n    if convert_to == WEEKLY:\n        grouping = [lambda x: x.year, lambda x: x.isocalendar()[1]]\n    elif convert_to == MONTHLY:\n        grouping = [lambda x: x.year, lambda x: x.month]\n    elif convert_to == QUARTERLY:\n        grouping = [lambda x: x.year, lambda x: int(math.ceil(x.month/3.))]\n    elif convert_to == YEARLY:\n        grouping = [lambda x: x.year]\n    else:\n        raise ValueError(\n            'convert_to must be {}, {} or {}'.format(WEEKLY, MONTHLY, YEARLY)\n        )\n\n    return returns.groupby(grouping).apply(cumulate_returns)\n\n\ndef max_drawdown(returns, out=None):\n    \"\"\"\n    Determines the maximum drawdown of a strategy.\n\n    Parameters\n    ----------\n    returns : pd.Series or np.ndarray\n        Daily returns of the strategy, noncumulative.\n        - See full explanation in :func:`~empyrical.stats.cum_returns`.\n    out : array-like, optional\n        Array to use as output buffer.\n        If not passed, a new array will be created.\n\n    Returns\n    -------\n    max_drawdown : float\n\n    Note\n    -----\n    See https://en.wikipedia.org/wiki/Drawdown_(economics) for more details.\n    \"\"\"\n    allocated_output = out is None\n    if allocated_output:\n        out = np.empty(returns.shape[1:])\n\n    returns_1d = returns.ndim == 1\n\n    if len(returns) < 1:\n        out[()] = np.nan\n        if returns_1d:\n            out = out.item()\n        return out\n\n    returns_array = np.asanyarray(returns)\n\n    cumulative = np.empty(\n        (returns.shape[0] + 1,) + returns.shape[1:],\n        dtype='float64',\n    )\n    cumulative[0] = start = 100\n    cum_returns(returns_array, starting_value=start, out=cumulative[1:])\n\n    max_return = np.fmax.accumulate(cumulative, axis=0)\n\n    nanmin((cumulative - max_return) / max_return, axis=0, out=out)\n    if returns_1d:\n        out = out.item()\n    elif allocated_output and isinstance(returns, pd.DataFrame):\n        out = pd.Series(out)\n\n    return out\n\n\nroll_max_drawdown = _create_unary_vectorized_roll_function(max_drawdown)\n\n\ndef annual_return(returns, period=DAILY, annualization=None):\n    \"\"\"\n    Determines the mean annual growth rate of returns. This is equivilent\n    to the compound annual growth rate.\n\n    Parameters\n    ----------\n    returns : pd.Series or np.ndarray\n        Periodic returns of the strategy, noncumulative.\n        - See full explanation in :func:`~empyrical.stats.cum_returns`.\n    period : str, optional\n        Defines the periodicity of the 'returns' data for purposes of\n        annualizing. Value ignored if `annualization` parameter is specified.\n        Defaults are::\n\n            'monthly':12\n            'weekly': 52\n            'daily': 252\n\n    annualization : int, optional\n        Used to suppress default values available in `period` to convert\n        returns into annual returns. Value should be the annual frequency of\n        `returns`.\n\n    Returns\n    -------\n    annual_return : float\n        Annual Return as CAGR (Compounded Annual Growth Rate).\n\n    \"\"\"\n\n    if len(returns) < 1:\n        return np.nan\n\n    ann_factor = annualization_factor(period, annualization)\n    num_years = len(returns) / ann_factor\n    # Pass array to ensure index -1 looks up successfully.\n    ending_value = cum_returns_final(returns, starting_value=1)\n\n    return ending_value ** (1 / num_years) - 1\n\n\ndef cagr(returns, period=DAILY, annualization=None):\n    \"\"\"\n    Compute compound annual growth rate. Alias function for\n    :func:`~empyrical.stats.annual_return`\n\n    Parameters\n    ----------\n    returns : pd.Series or np.ndarray\n        Daily returns of the strategy, noncumulative.\n        - See full explanation in :func:`~empyrical.stats.cum_returns`.\n    period : str, optional\n        Defines the periodicity of the 'returns' data for purposes of\n        annualizing. Value ignored if `annualization` parameter is specified.\n        Defaults are::\n\n            'monthly':12\n            'weekly': 52\n            'daily': 252\n\n    annualization : int, optional\n        Used to suppress default values available in `period` to convert\n        returns into annual returns. Value should be the annual frequency of\n        `returns`.\n        - See full explanation in :func:`~empyrical.stats.annual_return`.\n\n    Returns\n    -------\n    cagr : float\n        The CAGR value.\n\n    \"\"\"\n    return annual_return(returns, period, annualization)\n\n\nroll_cagr = _create_unary_vectorized_roll_function(cagr)\n\n\ndef annual_volatility(returns,\n                      period=DAILY,\n                      alpha=2.0,\n                      annualization=None,\n                      out=None):\n    \"\"\"\n    Determines the annual volatility of a strategy.\n\n    Parameters\n    ----------\n    returns : pd.Series or np.ndarray\n        Periodic returns of the strategy, noncumulative.\n        - See full explanation in :func:`~empyrical.stats.cum_returns`.\n    period : str, optional\n        Defines the periodicity of the 'returns' data for purposes of\n        annualizing. Value ignored if `annualization` parameter is specified.\n        Defaults are::\n\n            'monthly':12\n            'weekly': 52\n            'daily': 252\n\n    alpha : float, optional\n        Scaling relation (Levy stability exponent).\n    annualization : int, optional\n        Used to suppress default values available in `period` to convert\n        returns into annual returns. Value should be the annual frequency of\n        `returns`.\n    out : array-like, optional\n        Array to use as output buffer.\n        If not passed, a new array will be created.\n\n    Returns\n    -------\n    annual_volatility : float\n    \"\"\"\n    allocated_output = out is None\n    if allocated_output:\n        out = np.empty(returns.shape[1:])\n\n    returns_1d = returns.ndim == 1\n\n    if len(returns) < 2:\n        out[()] = np.nan\n        if returns_1d:\n            out = out.item()\n        return out\n\n    ann_factor = annualization_factor(period, annualization)\n    nanstd(returns, ddof=1, axis=0, out=out)\n    out = np.multiply(out, ann_factor ** (1.0 / alpha), out=out)\n    if returns_1d:\n        out = out.item()\n    return out\n\n\nroll_annual_volatility = _create_unary_vectorized_roll_function(\n    annual_volatility,\n)\n\n\ndef calmar_ratio(returns, period=DAILY, annualization=None):\n    \"\"\"\n    Determines the Calmar ratio, or drawdown ratio, of a strategy.\n\n    Parameters\n    ----------\n    returns : pd.Series or np.ndarray\n        Daily returns of the strategy, noncumulative.\n        - See full explanation in :func:`~empyrical.stats.cum_returns`.\n    period : str, optional\n        Defines the periodicity of the 'returns' data for purposes of\n        annualizing. Value ignored if `annualization` parameter is specified.\n        Defaults are::\n\n            'monthly':12\n            'weekly': 52\n            'daily': 252\n\n    annualization : int, optional\n        Used to suppress default values available in `period` to convert\n        returns into annual returns. Value should be the annual frequency of\n        `returns`.\n\n\n    Returns\n    -------\n    calmar_ratio : float\n        Calmar ratio (drawdown ratio) as float. Returns np.nan if there is no\n        calmar ratio.\n\n    Note\n    -----\n    See https://en.wikipedia.org/wiki/Calmar_ratio for more details.\n    \"\"\"\n\n    max_dd = max_drawdown(returns=returns)\n    if max_dd < 0:\n        temp = annual_return(\n            returns=returns,\n            period=period,\n            annualization=annualization\n        ) / abs(max_dd)\n    else:\n        return np.nan\n\n    if np.isinf(temp):\n        return np.nan\n\n    return temp\n\n\ndef omega_ratio(returns, risk_free=0.0, required_return=0.0,\n                annualization=APPROX_BDAYS_PER_YEAR):\n    \"\"\"Determines the Omega ratio of a strategy.\n\n    Parameters\n    ----------\n    returns : pd.Series or np.ndarray\n        Daily returns of the strategy, noncumulative.\n        - See full explanation in :func:`~empyrical.stats.cum_returns`.\n    risk_free : int, float\n        Constant risk-free return throughout the period\n    required_return : float, optional\n        Minimum acceptance return of the investor. Threshold over which to\n        consider positive vs negative returns. It will be converted to a\n        value appropriate for the period of the returns. E.g. An annual minimum\n        acceptable return of 100 will translate to a minimum acceptable\n        return of 0.018.\n    annualization : int, optional\n        Factor used to convert the required_return into a daily\n        value. Enter 1 if no time period conversion is necessary.\n\n    Returns\n    -------\n    omega_ratio : float\n\n    Note\n    -----\n    See https://en.wikipedia.org/wiki/Omega_ratio for more details.\n\n    \"\"\"\n\n    if len(returns) < 2:\n        return np.nan\n\n    if annualization == 1:\n        return_threshold = required_return\n    elif required_return <= -1:\n        return np.nan\n    else:\n        return_threshold = (1 + required_return) ** \\\n            (1. / annualization) - 1\n\n    returns_less_thresh = returns - risk_free - return_threshold\n\n    numer = sum(returns_less_thresh[returns_less_thresh > 0.0])\n    denom = -1.0 * sum(returns_less_thresh[returns_less_thresh < 0.0])\n\n    if denom > 0.0:\n        return numer / denom\n    else:\n        return np.nan\n\n\ndef sharpe_ratio(returns,\n                 risk_free=0,\n                 period=DAILY,\n                 annualization=None,\n                 out=None):\n    \"\"\"\n    Determines the Sharpe ratio of a strategy.\n\n    Parameters\n    ----------\n    returns : pd.Series or np.ndarray\n        Daily returns of the strategy, noncumulative.\n        - See full explanation in :func:`~empyrical.stats.cum_returns`.\n    risk_free : int, float\n        Constant daily risk-free return throughout the period.\n    period : str, optional\n        Defines the periodicity of the 'returns' data for purposes of\n        annualizing. Value ignored if `annualization` parameter is specified.\n        Defaults are::\n\n            'monthly':12\n            'weekly': 52\n            'daily': 252\n\n    annualization : int, optional\n        Used to suppress default values available in `period` to convert\n        returns into annual returns. Value should be the annual frequency of\n        `returns`.\n    out : array-like, optional\n        Array to use as output buffer.\n        If not passed, a new array will be created.\n\n    Returns\n    -------\n    sharpe_ratio : float\n        nan if insufficient length of returns or if if adjusted returns are 0.\n\n    Note\n    -----\n    See https://en.wikipedia.org/wiki/Sharpe_ratio for more details.\n\n    \"\"\"\n    allocated_output = out is None\n    if allocated_output:\n        out = np.empty(returns.shape[1:])\n\n    return_1d = returns.ndim == 1\n\n    if len(returns) < 2:\n        out[()] = np.nan\n        if return_1d:\n            out = out.item()\n        return out\n\n    returns_risk_adj = np.asanyarray(_adjust_returns(returns, risk_free))\n    ann_factor = annualization_factor(period, annualization)\n\n    np.multiply(\n        np.divide(\n            nanmean(returns_risk_adj, axis=0),\n            nanstd(returns_risk_adj, ddof=1, axis=0),\n            out=out,\n        ),\n        np.sqrt(ann_factor),\n        out=out,\n    )\n    if return_1d:\n        out = out.item()\n\n    return out\n\n\nroll_sharpe_ratio = _create_unary_vectorized_roll_function(sharpe_ratio)\n\n\ndef sortino_ratio(returns,\n                  required_return=0,\n                  period=DAILY,\n                  annualization=None,\n                  out=None,\n                  _downside_risk=None):\n    \"\"\"\n    Determines the Sortino ratio of a strategy.\n\n    Parameters\n    ----------\n    returns : pd.Series or np.ndarray or pd.DataFrame\n        Daily returns of the strategy, noncumulative.\n        - See full explanation in :func:`~empyrical.stats.cum_returns`.\n    required_return: float / series\n        minimum acceptable return\n    period : str, optional\n        Defines the periodicity of the 'returns' data for purposes of\n        annualizing. Value ignored if `annualization` parameter is specified.\n        Defaults are::\n\n            'monthly':12\n            'weekly': 52\n            'daily': 252\n\n    annualization : int, optional\n        Used to suppress default values available in `period` to convert\n        returns into annual returns. Value should be the annual frequency of\n        `returns`.\n    _downside_risk : float, optional\n        The downside risk of the given inputs, if known. Will be calculated if\n        not provided.\n    out : array-like, optional\n        Array to use as output buffer.\n        If not passed, a new array will be created.\n\n    Returns\n    -------\n    sortino_ratio : float or pd.Series\n\n        depends on input type\n        series ==> float\n        DataFrame ==> pd.Series\n\n    Note\n    -----\n    See `<https://www.sunrisecapital.com/wp-content/uploads/2014/06/Futures_\n    Mag_Sortino_0213.pdf>`__ for more details.\n\n    \"\"\"\n    allocated_output = out is None\n    if allocated_output:\n        out = np.empty(returns.shape[1:])\n\n    return_1d = returns.ndim == 1\n\n    if len(returns) < 2:\n        out[()] = np.nan\n        if return_1d:\n            out = out.item()\n        return out\n\n    adj_returns = np.asanyarray(_adjust_returns(returns, required_return))\n\n    ann_factor = annualization_factor(period, annualization)\n\n    average_annual_return = nanmean(adj_returns, axis=0) * ann_factor\n    annualized_downside_risk = (\n        _downside_risk\n        if _downside_risk is not None else\n        downside_risk(returns, required_return, period, annualization)\n    )\n    np.divide(average_annual_return, annualized_downside_risk, out=out)\n    if return_1d:\n        out = out.item()\n    elif isinstance(returns, pd.DataFrame):\n        out = pd.Series(out)\n\n    return out\n\n\nroll_sortino_ratio = _create_unary_vectorized_roll_function(sortino_ratio)\n\n\ndef downside_risk(returns,\n                  required_return=0,\n                  period=DAILY,\n                  annualization=None,\n                  out=None):\n    \"\"\"\n    Determines the downside deviation below a threshold\n\n    Parameters\n    ----------\n    returns : pd.Series or np.ndarray or pd.DataFrame\n        Daily returns of the strategy, noncumulative.\n        - See full explanation in :func:`~empyrical.stats.cum_returns`.\n    required_return: float / series\n        minimum acceptable return\n    period : str, optional\n        Defines the periodicity of the 'returns' data for purposes of\n        annualizing. Value ignored if `annualization` parameter is specified.\n        Defaults are::\n\n            'monthly':12\n            'weekly': 52\n            'daily': 252\n\n    annualization : int, optional\n        Used to suppress default values available in `period` to convert\n        returns into annual returns. Value should be the annual frequency of\n        `returns`.\n    out : array-like, optional\n        Array to use as output buffer.\n        If not passed, a new array will be created.\n\n    Returns\n    -------\n    downside_deviation : float or pd.Series\n        depends on input type\n        series ==> float\n        DataFrame ==> pd.Series\n\n    Note\n    -----\n    See `<https://www.sunrisecapital.com/wp-content/uploads/2014/06/Futures_\n    Mag_Sortino_0213.pdf>`__ for more details, specifically why using the\n    standard deviation of the negative returns is not correct.\n    \"\"\"\n    allocated_output = out is None\n    if allocated_output:\n        out = np.empty(returns.shape[1:])\n\n    returns_1d = returns.ndim == 1\n\n    if len(returns) < 1:\n        out[()] = np.nan\n        if returns_1d:\n            out = out.item()\n        return out\n\n    ann_factor = annualization_factor(period, annualization)\n\n    downside_diff = np.clip(\n        _adjust_returns(\n            np.asanyarray(returns),\n            np.asanyarray(required_return),\n        ),\n        -np.inf,\n        0,\n    )\n\n    np.square(downside_diff, out=downside_diff)\n    nanmean(downside_diff, axis=0, out=out)\n    np.sqrt(out, out=out)\n    np.multiply(out, np.sqrt(ann_factor), out=out)\n\n    if returns_1d:\n        out = out.item()\n    elif isinstance(returns, pd.DataFrame):\n        out = pd.Series(out, index=returns.columns)\n    return out\n\n\nroll_downsize_risk = _create_unary_vectorized_roll_function(downside_risk)\n\n\ndef excess_sharpe(returns, factor_returns, out=None):\n    \"\"\"\n    Determines the Excess Sharpe of a strategy.\n\n    Parameters\n    ----------\n    returns : pd.Series or np.ndarray\n        Daily returns of the strategy, noncumulative.\n        - See full explanation in :func:`~empyrical.stats.cum_returns`.\n    factor_returns: float / series\n        Benchmark return to compare returns against.\n    out : array-like, optional\n        Array to use as output buffer.\n        If not passed, a new array will be created.\n\n    Returns\n    -------\n    excess_sharpe : float\n\n    Note\n    -----\n    The excess Sharpe is a simplified Information Ratio that uses\n    tracking error rather than \"active risk\" as the denominator.\n    \"\"\"\n    allocated_output = out is None\n    if allocated_output:\n        out = np.empty(returns.shape[1:])\n\n    returns_1d = returns.ndim == 1\n\n    if len(returns) < 2:\n        out[()] = np.nan\n        if returns_1d:\n            out = out.item()\n        return out\n\n    active_return = _adjust_returns(returns, factor_returns)\n    tracking_error = np.nan_to_num(nanstd(active_return, ddof=1, axis=0))\n\n    out = np.divide(\n        nanmean(active_return, axis=0, out=out),\n        tracking_error,\n        out=out,\n    )\n    if returns_1d:\n        out = out.item()\n    return out\n\n\nroll_excess_sharpe = _create_binary_vectorized_roll_function(excess_sharpe)\n\n\ndef _to_pandas(ob):\n    \"\"\"Convert an array-like to a pandas object.\n\n    Parameters\n    ----------\n    ob : array-like\n        The object to convert.\n\n    Returns\n    -------\n    pandas_structure : pd.Series or pd.DataFrame\n        The correct structure based on the dimensionality of the data.\n    \"\"\"\n    if isinstance(ob, (pd.Series, pd.DataFrame)):\n        return ob\n\n    if ob.ndim == 1:\n        return pd.Series(ob)\n    elif ob.ndim == 2:\n        return pd.DataFrame(ob)\n    else:\n        raise ValueError(\n            'cannot convert array of dim > 2 to a pandas structure',\n        )\n\n\ndef _aligned_series(*many_series):\n    \"\"\"\n    Return a new list of series containing the data in the input series, but\n    with their indices aligned. NaNs will be filled in for missing values.\n\n    Parameters\n    ----------\n    *many_series\n        The series to align.\n\n    Returns\n    -------\n    aligned_series : iterable[array-like]\n        A new list of series containing the data in the input series, but\n        with their indices aligned. NaNs will be filled in for missing values.\n\n    \"\"\"\n    head = many_series[0]\n    tail = many_series[1:]\n    n = len(head)\n    if (isinstance(head, np.ndarray) and\n            all(len(s) == n and isinstance(s, np.ndarray) for s in tail)):\n        # optimization: ndarrays of the same length are already aligned\n        return many_series\n\n    # dataframe has no ``itervalues``\n    return (\n        v\n        for _, v in iteritems(pd.concat(map(_to_pandas, many_series), axis=1))\n    )\n\n\ndef alpha_beta(returns,\n               factor_returns,\n               risk_free=0.0,\n               period=DAILY,\n               annualization=None,\n               out=None):\n    \"\"\"Calculates annualized alpha and beta.\n\n    Parameters\n    ----------\n    returns : pd.Series\n        Daily returns of the strategy, noncumulative.\n        - See full explanation in :func:`~empyrical.stats.cum_returns`.\n    factor_returns : pd.Series\n         Daily noncumulative returns of the factor to which beta is\n         computed. Usually a benchmark such as the market.\n         - This is in the same style as returns.\n    risk_free : int, float, optional\n        Constant risk-free return throughout the period. For example, the\n        interest rate on a three month us treasury bill.\n    period : str, optional\n        Defines the periodicity of the 'returns' data for purposes of\n        annualizing. Value ignored if `annualization` parameter is specified.\n        Defaults are::\n\n            'monthly':12\n            'weekly': 52\n            'daily': 252\n\n    annualization : int, optional\n        Used to suppress default values available in `period` to convert\n        returns into annual returns. Value should be the annual frequency of\n        `returns`.\n    out : array-like, optional\n        Array to use as output buffer.\n        If not passed, a new array will be created.\n\n    Returns\n    -------\n    alpha : float\n    beta : float\n    \"\"\"\n    returns, factor_returns = _aligned_series(returns, factor_returns)\n\n    return alpha_beta_aligned(\n        returns,\n        factor_returns,\n        risk_free=risk_free,\n        period=period,\n        annualization=annualization,\n        out=out,\n    )\n\n\ndef roll_alpha_beta(returns, factor_returns, window=10, **kwargs):\n    \"\"\"\n    Computes alpha and beta over a rolling window.\n\n    Parameters\n    ----------\n    lhs : array-like\n        The first array to pass to the rolling alpha-beta.\n    rhs : array-like\n        The second array to pass to the rolling alpha-beta.\n    window : int\n        Size of the rolling window in terms of the periodicity of the data.\n    out : array-like, optional\n        Array to use as output buffer.\n        If not passed, a new array will be created.\n    **kwargs\n        Forwarded to :func:`~empyrical.alpha_beta`.\n    \"\"\"\n    returns, factor_returns = _aligned_series(returns, factor_returns)\n\n    return roll_alpha_beta_aligned(\n        returns,\n        factor_returns,\n        window=window,\n        **kwargs\n    )\n\n\ndef alpha_beta_aligned(returns,\n                       factor_returns,\n                       risk_free=0.0,\n                       period=DAILY,\n                       annualization=None,\n                       out=None):\n    \"\"\"Calculates annualized alpha and beta.\n\n    If they are pd.Series, expects returns and factor_returns have already\n    been aligned on their labels.  If np.ndarray, these arguments should have\n    the same shape.\n\n    Parameters\n    ----------\n    returns : pd.Series or np.ndarray\n        Daily returns of the strategy, noncumulative.\n        - See full explanation in :func:`~empyrical.stats.cum_returns`.\n    factor_returns : pd.Series or np.ndarray\n         Daily noncumulative returns of the factor to which beta is\n         computed. Usually a benchmark such as the market.\n         - This is in the same style as returns.\n    risk_free : int, float, optional\n        Constant risk-free return throughout the period. For example, the\n        interest rate on a three month us treasury bill.\n    period : str, optional\n        Defines the periodicity of the 'returns' data for purposes of\n        annualizing. Value ignored if `annualization` parameter is specified.\n        Defaults are::\n\n            'monthly':12\n            'weekly': 52\n            'daily': 252\n\n    annualization : int, optional\n        Used to suppress default values available in `period` to convert\n        returns into annual returns. Value should be the annual frequency of\n        `returns`.\n    out : array-like, optional\n        Array to use as output buffer.\n        If not passed, a new array will be created.\n\n    Returns\n    -------\n    alpha : float\n    beta : float\n    \"\"\"\n    if out is None:\n        out = np.empty(returns.shape[1:] + (2,), dtype='float64')\n\n    b = beta_aligned(returns, factor_returns, risk_free, out=out[..., 1])\n    alpha_aligned(\n        returns,\n        factor_returns,\n        risk_free,\n        period,\n        annualization,\n        out=out[..., 0],\n        _beta=b,\n    )\n\n    return out\n\n\nroll_alpha_beta_aligned = _create_binary_vectorized_roll_function(\n    alpha_beta_aligned,\n)\n\n\ndef alpha(returns,\n          factor_returns,\n          risk_free=0.0,\n          period=DAILY,\n          annualization=None,\n          out=None,\n          _beta=None):\n    \"\"\"Calculates annualized alpha.\n\n    Parameters\n    ----------\n    returns : pd.Series\n        Daily returns of the strategy, noncumulative.\n        - See full explanation in :func:`~empyrical.stats.cum_returns`.\n    factor_returns : pd.Series\n        Daily noncumulative returns of the factor to which beta is\n        computed. Usually a benchmark such as the market.\n        - This is in the same style as returns.\n    risk_free : int, float, optional\n        Constant risk-free return throughout the period. For example, the\n        interest rate on a three month us treasury bill.\n    period : str, optional\n        Defines the periodicity of the 'returns' data for purposes of\n        annualizing. Value ignored if `annualization` parameter is specified.\n        Defaults are::\n\n            'monthly':12\n            'weekly': 52\n            'daily': 252\n\n    annualization : int, optional\n        Used to suppress default values available in `period` to convert\n        returns into annual returns. Value should be the annual frequency of\n        `returns`.\n        - See full explanation in :func:`~empyrical.stats.annual_return`.\n    _beta : float, optional\n        The beta for the given inputs, if already known. Will be calculated\n        internally if not provided.\n    out : array-like, optional\n        Array to use as output buffer.\n        If not passed, a new array will be created.\n\n    Returns\n    -------\n    float\n        Alpha.\n    \"\"\"\n    if not (isinstance(returns, np.ndarray) and\n            isinstance(factor_returns, np.ndarray)):\n        returns, factor_returns = _aligned_series(returns, factor_returns)\n\n    return alpha_aligned(\n        returns,\n        factor_returns,\n        risk_free=risk_free,\n        period=period,\n        annualization=annualization,\n        out=out,\n        _beta=_beta\n    )\n\n\nroll_alpha = _create_binary_vectorized_roll_function(alpha)\n\n\ndef alpha_aligned(returns,\n                  factor_returns,\n                  risk_free=0.0,\n                  period=DAILY,\n                  annualization=None,\n                  out=None,\n                  _beta=None):\n    \"\"\"Calculates annualized alpha.\n\n    If they are pd.Series, expects returns and factor_returns have already\n    been aligned on their labels.  If np.ndarray, these arguments should have\n    the same shape.\n\n    Parameters\n    ----------\n    returns : pd.Series or np.ndarray\n        Daily returns of the strategy, noncumulative.\n        - See full explanation in :func:`~empyrical.stats.cum_returns`.\n    factor_returns : pd.Series or np.ndarray\n        Daily noncumulative returns of the factor to which beta is\n        computed. Usually a benchmark such as the market.\n        - This is in the same style as returns.\n    risk_free : int, float, optional\n        Constant risk-free return throughout the period. For example, the\n        interest rate on a three month us treasury bill.\n    period : str, optional\n        Defines the periodicity of the 'returns' data for purposes of\n        annualizing. Value ignored if `annualization` parameter is specified.\n        Defaults are::\n\n            'monthly':12\n            'weekly': 52\n            'daily': 252\n\n    annualization : int, optional\n        Used to suppress default values available in `period` to convert\n        returns into annual returns. Value should be the annual frequency of\n        `returns`.\n        - See full explanation in :func:`~empyrical.stats.annual_return`.\n    _beta : float, optional\n        The beta for the given inputs, if already known. Will be calculated\n        internally if not provided.\n    out : array-like, optional\n        Array to use as output buffer.\n        If not passed, a new array will be created.\n\n    Returns\n    -------\n    alpha : float\n    \"\"\"\n    allocated_output = out is None\n    if allocated_output:\n        out = np.empty(returns.shape[1:], dtype='float64')\n\n    if len(returns) < 2:\n        out[()] = np.nan\n        if returns.ndim == 1:\n            out = out.item()\n        return out\n\n    ann_factor = annualization_factor(period, annualization)\n\n    if _beta is None:\n        _beta = beta_aligned(returns, factor_returns, risk_free)\n\n    adj_returns = _adjust_returns(returns, risk_free)\n    adj_factor_returns = _adjust_returns(factor_returns, risk_free)\n    alpha_series = adj_returns - (_beta * adj_factor_returns)\n\n    out = np.subtract(\n        np.power(\n            np.add(\n                nanmean(alpha_series, axis=0, out=out),\n                1,\n                out=out\n            ),\n            ann_factor,\n            out=out\n        ),\n        1,\n        out=out\n    )\n\n    if allocated_output and isinstance(returns, pd.DataFrame):\n        out = pd.Series(out)\n\n    if returns.ndim == 1:\n        out = out.item()\n\n    return out\n\n\nroll_alpha_aligned = _create_binary_vectorized_roll_function(alpha_aligned)\n\n\ndef beta(returns, factor_returns, risk_free=0.0, out=None):\n    \"\"\"Calculates beta.\n\n    Parameters\n    ----------\n    returns : pd.Series\n        Daily returns of the strategy, noncumulative.\n        - See full explanation in :func:`~empyrical.stats.cum_returns`.\n    factor_returns : pd.Series\n         Daily noncumulative returns of the factor to which beta is\n         computed. Usually a benchmark such as the market.\n         - This is in the same style as returns.\n    risk_free : int, float, optional\n        Constant risk-free return throughout the period. For example, the\n        interest rate on a three month us treasury bill.\n    out : array-like, optional\n        Array to use as output buffer.\n        If not passed, a new array will be created.\n\n    Returns\n    -------\n    beta : float\n    \"\"\"\n    if not (isinstance(returns, np.ndarray) and\n            isinstance(factor_returns, np.ndarray)):\n        returns, factor_returns = _aligned_series(returns, factor_returns)\n\n    return beta_aligned(\n        returns,\n        factor_returns,\n        risk_free=risk_free,\n        out=out,\n    )\n\n\nroll_beta = _create_binary_vectorized_roll_function(beta)\n\n\ndef beta_aligned(returns, factor_returns, risk_free=0.0, out=None):\n    \"\"\"Calculates beta.\n\n    If they are pd.Series, expects returns and factor_returns have already\n    been aligned on their labels.  If np.ndarray, these arguments should have\n    the same shape.\n\n    Parameters\n    ----------\n    returns : pd.Series or np.ndarray\n        Daily returns of the strategy, noncumulative.\n        - See full explanation in :func:`~empyrical.stats.cum_returns`.\n    factor_returns : pd.Series or np.ndarray\n         Daily noncumulative returns of the factor to which beta is\n         computed. Usually a benchmark such as the market.\n         - This is in the same style as returns.\n    risk_free : int, float, optional\n        Constant risk-free return throughout the period. For example, the\n        interest rate on a three month us treasury bill.\n    out : array-like, optional\n        Array to use as output buffer.\n        If not passed, a new array will be created.\n\n    Returns\n    -------\n    beta : float\n        Beta.\n    \"\"\"\n    # Cache these as locals since we're going to call them multiple times.\n    nan = np.nan\n    isnan = np.isnan\n\n    returns_1d = returns.ndim == 1\n    if returns_1d:\n        returns = np.asanyarray(returns)[:, np.newaxis]\n\n    if factor_returns.ndim == 1:\n        factor_returns = np.asanyarray(factor_returns)[:, np.newaxis]\n\n    N, M = returns.shape\n\n    if out is None:\n        out = np.full(M, nan)\n    elif out.ndim == 0:\n        out = out[np.newaxis]\n\n    if len(returns) < 1 or len(factor_returns) < 2:\n        out[()] = nan\n        if returns_1d:\n            out = out.item()\n        return out\n\n    # Copy N times as a column vector and fill with nans to have the same\n    # missing value pattern as the dependent variable.\n    #\n    # PERF_TODO: We could probably avoid the space blowup by doing this in\n    # Cython.\n\n    # shape: (N, M)\n    independent = np.where(\n        isnan(returns),\n        nan,\n        factor_returns,\n    )\n\n    # Calculate beta as Cov(X, Y) / Cov(X, X).\n    # https://en.wikipedia.org/wiki/Simple_linear_regression#Fitting_the_regression_line  # noqa\n    #\n    # NOTE: The usual formula for covariance is::\n    #\n    #    mean((X - mean(X)) * (Y - mean(Y)))\n    #\n    # However, we don't actually need to take the mean of both sides of the\n    # product, because of the folllowing equivalence::\n    #\n    # Let X_res = (X - mean(X)).\n    # We have:\n    #\n    #     mean(X_res * (Y - mean(Y))) = mean(X_res * (Y - mean(Y)))\n    #                             (1) = mean((X_res * Y) - (X_res * mean(Y)))\n    #                             (2) = mean(X_res * Y) - mean(X_res * mean(Y))\n    #                             (3) = mean(X_res * Y) - mean(X_res) * mean(Y)\n    #                             (4) = mean(X_res * Y) - 0 * mean(Y)\n    #                             (5) = mean(X_res * Y)\n    #\n    #\n    # The tricky step in the above derivation is step (4). We know that\n    # mean(X_res) is zero because, for any X:\n    #\n    #     mean(X - mean(X)) = mean(X) - mean(X) = 0.\n    #\n    # The upshot of this is that we only have to center one of `independent`\n    # and `dependent` when calculating covariances. Since we need the centered\n    # `independent` to calculate its variance in the next step, we choose to\n    # center `independent`.\n\n    ind_residual = independent - nanmean(independent, axis=0)\n\n    covariances = nanmean(ind_residual * returns, axis=0)\n\n    # We end up with different variances in each column here because each\n    # column may have a different subset of the data dropped due to missing\n    # data in the corresponding dependent column.\n    # shape: (M,)\n    np.square(ind_residual, out=ind_residual)\n    independent_variances = nanmean(ind_residual, axis=0)\n    independent_variances[independent_variances < 1.0e-30] = np.nan\n\n    np.divide(covariances, independent_variances, out=out)\n\n    if returns_1d:\n        out = out.item()\n\n    return out\n\n\nroll_beta_aligned = _create_binary_vectorized_roll_function(beta_aligned)\n\n\ndef stability_of_timeseries(returns):\n    \"\"\"Determines R-squared of a linear fit to the cumulative\n    log returns. Computes an ordinary least squares linear fit,\n    and returns R-squared.\n\n    Parameters\n    ----------\n    returns : pd.Series or np.ndarray\n        Daily returns of the strategy, noncumulative.\n        - See full explanation in :func:`~empyrical.stats.cum_returns`.\n\n    Returns\n    -------\n    float\n        R-squared.\n\n    \"\"\"\n    if len(returns) < 2:\n        return np.nan\n\n    returns = np.asanyarray(returns)\n    returns = returns[~np.isnan(returns)]\n\n    cum_log_returns = np.log1p(returns).cumsum()\n    rhat = stats.linregress(np.arange(len(cum_log_returns)),\n                            cum_log_returns)[2]\n\n    return rhat ** 2\n\n\ndef tail_ratio(returns):\n    \"\"\"Determines the ratio between the right (95%) and left tail (5%).\n\n    For example, a ratio of 0.25 means that losses are four times\n    as bad as profits.\n\n    Parameters\n    ----------\n    returns : pd.Series or np.ndarray\n        Daily returns of the strategy, noncumulative.\n         - See full explanation in :func:`~empyrical.stats.cum_returns`.\n\n    Returns\n    -------\n    tail_ratio : float\n    \"\"\"\n\n    if len(returns) < 1:\n        return np.nan\n\n    returns = np.asanyarray(returns)\n    # Be tolerant of nan's\n    returns = returns[~np.isnan(returns)]\n    if len(returns) < 1:\n        return np.nan\n\n    return np.abs(np.percentile(returns, 95)) / \\\n        np.abs(np.percentile(returns, 5))\n\n\ndef capture(returns, factor_returns, period=DAILY):\n    \"\"\"Compute capture ratio.\n\n    Parameters\n    ----------\n    returns : pd.Series or np.ndarray\n        Returns of the strategy, noncumulative.\n        - See full explanation in :func:`~empyrical.stats.cum_returns`.\n    factor_returns : pd.Series or np.ndarray\n        Noncumulative returns of the factor to which beta is\n        computed. Usually a benchmark such as the market.\n        - This is in the same style as returns.\n    period : str, optional\n        Defines the periodicity of the 'returns' data for purposes of\n        annualizing. Value ignored if `annualization` parameter is specified.\n        Defaults are::\n\n            'monthly':12\n            'weekly': 52\n            'daily': 252\n\n    Returns\n    -------\n    capture_ratio : float\n\n    Note\n    ----\n    See http://www.investopedia.com/terms/u/up-market-capture-ratio.asp for\n    details.\n    \"\"\"\n    return (annual_return(returns, period=period) /\n            annual_return(factor_returns, period=period))\n\n\ndef beta_fragility_heuristic(returns, factor_returns):\n    \"\"\"Estimate fragility to drops in beta.\n\n    Parameters\n    ----------\n    returns : pd.Series or np.ndarray\n        Daily returns of the strategy, noncumulative.\n        - See full explanation in :func:`~empyrical.stats.cum_returns`.\n    factor_returns : pd.Series or np.ndarray\n         Daily noncumulative returns of the factor to which beta is\n         computed. Usually a benchmark such as the market.\n         - This is in the same style as returns.\n\n    Returns\n    -------\n    float, np.nan\n        The beta fragility of the strategy.\n\n    Note\n    ----\n    A negative return value indicates potential losses\n    could follow volatility in beta.\n    The magnitude of the negative value indicates the size of\n    the potential loss.\n    seealso::\n    `A New Heuristic Measure of Fragility and\nTail Risks: Application to Stress Testing`\n        https://www.imf.org/external/pubs/ft/wp/2012/wp12216.pdf\n        An IMF Working Paper describing the heuristic\n    \"\"\"\n    if len(returns) < 3 or len(factor_returns) < 3:\n        return np.nan\n\n    return beta_fragility_heuristic_aligned(\n        *_aligned_series(returns, factor_returns))\n\n\ndef beta_fragility_heuristic_aligned(returns, factor_returns):\n    \"\"\"Estimate fragility to drops in beta\n\n    Parameters\n    ----------\n    returns : pd.Series or np.ndarray\n        Daily returns of the strategy, noncumulative.\n        - See full explanation in :func:`~empyrical.stats.cum_returns`.\n    factor_returns : pd.Series or np.ndarray\n         Daily noncumulative returns of the factor to which beta is\n         computed. Usually a benchmark such as the market.\n         - This is in the same style as returns.\n\n    Returns\n    -------\n    float, np.nan\n        The beta fragility of the strategy.\n\n    Note\n    ----\n    If they are pd.Series, expects returns and factor_returns have already\n    been aligned on their labels.  If np.ndarray, these arguments should have\n    the same shape.\n    seealso::\n    `A New Heuristic Measure of Fragility and\nTail Risks: Application to Stress Testing`\n        https://www.imf.org/external/pubs/ft/wp/2012/wp12216.pdf\n        An IMF Working Paper describing the heuristic\n    \"\"\"\n    if len(returns) < 3 or len(factor_returns) < 3:\n        return np.nan\n\n    # combine returns and factor returns into pairs\n    returns_series = pd.Series(returns)\n    factor_returns_series = pd.Series(factor_returns)\n    pairs = pd.concat([returns_series, factor_returns_series], axis=1)\n    pairs.columns = ['returns', 'factor_returns']\n\n    # exclude any rows where returns are nan\n    pairs = pairs.dropna()\n    # sort by beta\n    pairs = pairs.sort_values(by='factor_returns')\n\n    # find the three vectors, using median of 3\n    start_index = 0\n    mid_index = int(np.around(len(pairs) / 2, 0))\n    end_index = len(pairs) - 1\n\n    (start_returns, start_factor_returns) = pairs.iloc[start_index]\n    (mid_returns, mid_factor_returns) = pairs.iloc[mid_index]\n    (end_returns, end_factor_returns) = pairs.iloc[end_index]\n\n    factor_returns_range = (end_factor_returns - start_factor_returns)\n    start_returns_weight = 0.5\n    end_returns_weight = 0.5\n\n    # find weights for the start and end returns\n    # using a convex combination\n    if not factor_returns_range == 0:\n        start_returns_weight = \\\n            (mid_factor_returns - start_factor_returns) / \\\n            factor_returns_range\n        end_returns_weight = \\\n            (end_factor_returns - mid_factor_returns) / \\\n            factor_returns_range\n\n    # calculate fragility heuristic\n    heuristic = (start_returns_weight*start_returns) + \\\n        (end_returns_weight*end_returns) - mid_returns\n\n    return heuristic\n\n\ndef gpd_risk_estimates(returns, var_p=0.01):\n    \"\"\"Estimate VaR and ES using the Generalized Pareto Distribution (GPD)\n\n    Parameters\n    ----------\n    returns : pd.Series or np.ndarray\n        Daily returns of the strategy, noncumulative.\n        - See full explanation in :func:`~empyrical.stats.cum_returns`.\n    var_p : float\n        The percentile to use for estimating the VaR and ES\n\n    Returns\n    -------\n    [threshold, scale_param, shape_param, var_estimate, es_estimate]\n        : list[float]\n        threshold - the threshold use to cut off exception tail losses\n        scale_param - a parameter (often denoted by sigma, capturing the\n            scale, related to variance)\n        shape_param - a parameter (often denoted by xi, capturing the shape or\n            type of the distribution)\n        var_estimate - an estimate for the VaR for the given percentile\n        es_estimate - an estimate for the ES for the given percentile\n\n    Note\n    ----\n    seealso::\n    `An Application of Extreme Value Theory for\nMeasuring Risk <https://link.springer.com/article/10.1007/s10614-006-9025-7>`\n        A paper describing how to use the Generalized Pareto\n        Distribution to estimate VaR and ES.\n    \"\"\"\n    if len(returns) < 3:\n        result = np.zeros(5)\n        if isinstance(returns, pd.Series):\n            result = pd.Series(result)\n        return result\n    return gpd_risk_estimates_aligned(*_aligned_series(returns, var_p))\n\n\ndef gpd_risk_estimates_aligned(returns, var_p=0.01):\n    \"\"\"Estimate VaR and ES using the Generalized Pareto Distribution (GPD)\n\n    Parameters\n    ----------\n    returns : pd.Series or np.ndarray\n        Daily returns of the strategy, noncumulative.\n        - See full explanation in :func:`~empyrical.stats.cum_returns`.\n    var_p : float\n        The percentile to use for estimating the VaR and ES\n\n    Returns\n    -------\n    [threshold, scale_param, shape_param, var_estimate, es_estimate]\n        : list[float]\n        threshold - the threshold use to cut off exception tail losses\n        scale_param - a parameter (often denoted by sigma, capturing the\n            scale, related to variance)\n        shape_param - a parameter (often denoted by xi, capturing the shape or\n            type of the distribution)\n        var_estimate - an estimate for the VaR for the given percentile\n        es_estimate - an estimate for the ES for the given percentile\n\n    Note\n    ----\n    seealso::\n    `An Application of Extreme Value Theory for\nMeasuring Risk <https://link.springer.com/article/10.1007/s10614-006-9025-7>`\n        A paper describing how to use the Generalized Pareto\n        Distribution to estimate VaR and ES.\n    \"\"\"\n    result = np.zeros(5)\n    if not len(returns) < 3:\n\n        DEFAULT_THRESHOLD = 0.2\n        MINIMUM_THRESHOLD = 0.000000001\n\n        try:\n            returns_array = pd.Series(returns).to_numpy()\n        except AttributeError:\n            # while zipline requires support for pandas < 0.25\n            returns_array = pd.Series(returns).as_matrix()\n\n        flipped_returns = -1 * returns_array\n        losses = flipped_returns[flipped_returns > 0]\n        threshold = DEFAULT_THRESHOLD\n        finished = False\n        scale_param = 0\n        shape_param = 0\n        while not finished and threshold > MINIMUM_THRESHOLD:\n            losses_beyond_threshold = \\\n                losses[losses >= threshold]\n            param_result = \\\n                gpd_loglikelihood_minimizer_aligned(losses_beyond_threshold)\n            if (param_result[0] is not False and\n                    param_result[1] is not False):\n                scale_param = param_result[0]\n                shape_param = param_result[1]\n                var_estimate = gpd_var_calculator(threshold, scale_param,\n                                                  shape_param, var_p,\n                                                  len(losses),\n                                                  len(losses_beyond_threshold))\n                # non-negative shape parameter is required for fat tails\n                # non-negative VaR estimate is required for loss of some kind\n                if (shape_param > 0 and var_estimate > 0):\n                    finished = True\n            if (not finished):\n                threshold = threshold / 2\n        if (finished):\n            es_estimate = gpd_es_calculator(var_estimate, threshold,\n                                            scale_param, shape_param)\n            result = np.array([threshold, scale_param, shape_param,\n                               var_estimate, es_estimate])\n    if isinstance(returns, pd.Series):\n        result = pd.Series(result)\n    return result\n\n\ndef gpd_es_calculator(var_estimate, threshold, scale_param,\n                      shape_param):\n    result = 0\n    if ((1 - shape_param) != 0):\n        # this formula is from Gilli and Kellezi pg. 8\n        var_ratio = (var_estimate/(1 - shape_param))\n        param_ratio = ((scale_param - (shape_param * threshold)) /\n                       (1 - shape_param))\n        result = var_ratio + param_ratio\n    return result\n\n\ndef gpd_var_calculator(threshold, scale_param, shape_param,\n                       probability, total_n, exceedance_n):\n    result = 0\n    if (exceedance_n > 0 and shape_param > 0):\n        # this formula is from Gilli and Kellezi pg. 12\n        param_ratio = scale_param / shape_param\n        prob_ratio = (total_n/exceedance_n) * probability\n        result = threshold + (param_ratio *\n                              (pow(prob_ratio, -shape_param) - 1))\n    return result\n\n\ndef gpd_loglikelihood_minimizer_aligned(price_data):\n    result = [False, False]\n    DEFAULT_SCALE_PARAM = 1\n    DEFAULT_SHAPE_PARAM = 1\n    if (len(price_data) > 0):\n        gpd_loglikelihood_lambda = \\\n            gpd_loglikelihood_factory(price_data)\n        optimization_results = \\\n            optimize.minimize(gpd_loglikelihood_lambda,\n                              [DEFAULT_SCALE_PARAM,\n                               DEFAULT_SHAPE_PARAM],\n                              method='Nelder-Mead')\n        if optimization_results.success:\n            resulting_params = optimization_results.x\n            if len(resulting_params) == 2:\n                result[0] = resulting_params[0]\n                result[1] = resulting_params[1]\n    return result\n\n\ndef gpd_loglikelihood_factory(price_data):\n    return lambda params: gpd_loglikelihood(params, price_data)\n\n\ndef gpd_loglikelihood(params, price_data):\n    if (params[1] != 0):\n        return -gpd_loglikelihood_scale_and_shape(params[0],\n                                                  params[1],\n                                                  price_data)\n    else:\n        return -gpd_loglikelihood_scale_only(params[0], price_data)\n\n\ndef gpd_loglikelihood_scale_and_shape_factory(price_data):\n    # minimize a function of two variables requires a list of params\n    # we are expecting the lambda below to be called as follows:\n    # parameters = [scale, shape]\n    # the final outer negative is added because scipy only minimizes\n    return lambda params: \\\n        -gpd_loglikelihood_scale_and_shape(params[0],\n                                           params[1],\n                                           price_data)\n\n\ndef gpd_loglikelihood_scale_and_shape(scale, shape, price_data):\n    n = len(price_data)\n    result = -1 * float_info.max\n    if (scale != 0):\n        param_factor = shape / scale\n        if (shape != 0 and param_factor >= 0 and scale >= 0):\n            result = ((-n * np.log(scale)) -\n                      (((1 / shape) + 1) *\n                       (np.log((shape / scale * price_data) + 1)).sum()))\n    return result\n\n\ndef gpd_loglikelihood_scale_only_factory(price_data):\n    # the negative is added because scipy only minimizes\n    return lambda scale: \\\n        -gpd_loglikelihood_scale_only(scale, price_data)\n\n\ndef gpd_loglikelihood_scale_only(scale, price_data):\n    n = len(price_data)\n    data_sum = price_data.sum()\n    result = -1 * float_info.max\n    if (scale >= 0):\n        result = ((-n*np.log(scale)) - (data_sum/scale))\n    return result\n\n\ndef up_capture(returns, factor_returns, **kwargs):\n    \"\"\"\n    Compute the capture ratio for periods when the benchmark return is positive\n\n    Parameters\n    ----------\n    returns : pd.Series or np.ndarray\n        Returns of the strategy, noncumulative.\n        - See full explanation in :func:`~empyrical.stats.cum_returns`.\n    factor_returns : pd.Series or np.ndarray\n        Noncumulative returns of the factor to which beta is\n        computed. Usually a benchmark such as the market.\n        - This is in the same style as returns.\n    period : str, optional\n        Defines the periodicity of the 'returns' data for purposes of\n        annualizing. Value ignored if `annualization` parameter is specified.\n        Defaults are::\n\n            'monthly':12\n            'weekly': 52\n            'daily': 252\n\n    Returns\n    -------\n    up_capture : float\n\n    Note\n    ----\n    See http://www.investopedia.com/terms/u/up-market-capture-ratio.asp for\n    more information.\n    \"\"\"\n    return up(returns, factor_returns, function=capture, **kwargs)\n\n\ndef down_capture(returns, factor_returns, **kwargs):\n    \"\"\"\n    Compute the capture ratio for periods when the benchmark return is negative\n\n    Parameters\n    ----------\n    returns : pd.Series or np.ndarray\n        Returns of the strategy, noncumulative.\n        - See full explanation in :func:`~empyrical.stats.cum_returns`.\n    factor_returns : pd.Series or np.ndarray\n        Noncumulative returns of the factor to which beta is\n        computed. Usually a benchmark such as the market.\n        - This is in the same style as returns.\n    period : str, optional\n        Defines the periodicity of the 'returns' data for purposes of\n        annualizing. Value ignored if `annualization` parameter is specified.\n        Defaults are::\n\n            'monthly':12\n            'weekly': 52\n            'daily': 252\n\n    Returns\n    -------\n    down_capture : float\n\n    Note\n    ----\n    See http://www.investopedia.com/terms/d/down-market-capture-ratio.asp for\n    more information.\n    \"\"\"\n    return down(returns, factor_returns, function=capture, **kwargs)\n\n\ndef up_down_capture(returns, factor_returns, **kwargs):\n    \"\"\"\n    Computes the ratio of up_capture to down_capture.\n\n    Parameters\n    ----------\n    returns : pd.Series or np.ndarray\n        Returns of the strategy, noncumulative.\n        - See full explanation in :func:`~empyrical.stats.cum_returns`.\n    factor_returns : pd.Series or np.ndarray\n        Noncumulative returns of the factor to which beta is\n        computed. Usually a benchmark such as the market.\n        - This is in the same style as returns.\n    period : str, optional\n        Defines the periodicity of the 'returns' data for purposes of\n        annualizing. Value ignored if `annualization` parameter is specified.\n        Defaults are::\n\n            'monthly':12\n            'weekly': 52\n            'daily': 252\n\n    Returns\n    -------\n    up_down_capture : float\n        the updown capture ratio\n    \"\"\"\n    return (up_capture(returns, factor_returns, **kwargs) /\n            down_capture(returns, factor_returns, **kwargs))\n\n\ndef up_alpha_beta(returns, factor_returns, **kwargs):\n    \"\"\"\n    Computes alpha and beta for periods when the benchmark return is positive.\n\n    Parameters\n    ----------\n    see documentation for `alpha_beta`.\n\n    Returns\n    -------\n    float\n        Alpha.\n    float\n        Beta.\n    \"\"\"\n    return up(returns, factor_returns, function=alpha_beta_aligned, **kwargs)\n\n\ndef down_alpha_beta(returns, factor_returns, **kwargs):\n    \"\"\"\n    Computes alpha and beta for periods when the benchmark return is negative.\n\n    Parameters\n    ----------\n    see documentation for `alpha_beta`.\n\n    Returns\n    -------\n    alpha : float\n    beta : float\n    \"\"\"\n    return down(returns, factor_returns, function=alpha_beta_aligned, **kwargs)\n\n\ndef roll_up_capture(returns, factor_returns, window=10, **kwargs):\n    \"\"\"\n    Computes the up capture measure over a rolling window.\n    see documentation for :func:`~empyrical.stats.up_capture`.\n    (pass all args, kwargs required)\n\n    Parameters\n    ----------\n    returns : pd.Series or np.ndarray\n        Daily returns of the strategy, noncumulative.\n        - See full explanation in :func:`~empyrical.stats.cum_returns`.\n\n    factor_returns : pd.Series or np.ndarray\n        Noncumulative returns of the factor to which beta is\n        computed. Usually a benchmark such as the market.\n        - This is in the same style as returns.\n\n    window : int, required\n        Size of the rolling window in terms of the periodicity of the data.\n        - eg window = 60, periodicity=DAILY, represents a rolling 60 day window\n    \"\"\"\n    return roll(returns, factor_returns, window=window, function=up_capture,\n                **kwargs)\n\n\ndef roll_down_capture(returns, factor_returns, window=10, **kwargs):\n    \"\"\"\n    Computes the down capture measure over a rolling window.\n    see documentation for :func:`~empyrical.stats.down_capture`.\n    (pass all args, kwargs required)\n\n    Parameters\n    ----------\n    returns : pd.Series or np.ndarray\n        Daily returns of the strategy, noncumulative.\n        - See full explanation in :func:`~empyrical.stats.cum_returns`.\n\n    factor_returns : pd.Series or np.ndarray\n        Noncumulative returns of the factor to which beta is\n        computed. Usually a benchmark such as the market.\n        - This is in the same style as returns.\n\n    window : int, required\n        Size of the rolling window in terms of the periodicity of the data.\n        - eg window = 60, periodicity=DAILY, represents a rolling 60 day window\n    \"\"\"\n    return roll(returns, factor_returns, window=window, function=down_capture,\n                **kwargs)\n\n\ndef roll_up_down_capture(returns, factor_returns, window=10, **kwargs):\n    \"\"\"\n    Computes the up/down capture measure over a rolling window.\n    see documentation for :func:`~empyrical.stats.up_down_capture`.\n    (pass all args, kwargs required)\n\n    Parameters\n    ----------\n    returns : pd.Series or np.ndarray\n        Daily returns of the strategy, noncumulative.\n        - See full explanation in :func:`~empyrical.stats.cum_returns`.\n\n    factor_returns : pd.Series or np.ndarray\n        Noncumulative returns of the factor to which beta is\n        computed. Usually a benchmark such as the market.\n        - This is in the same style as returns.\n\n    window : int, required\n        Size of the rolling window in terms of the periodicity of the data.\n        - eg window = 60, periodicity=DAILY, represents a rolling 60 day window\n    \"\"\"\n    return roll(returns, factor_returns, window=window,\n                function=up_down_capture, **kwargs)\n\n\ndef value_at_risk(returns, cutoff=0.05):\n    \"\"\"\n    Value at risk (VaR) of a returns stream.\n\n    Parameters\n    ----------\n    returns : pandas.Series or 1-D numpy.array\n        Non-cumulative daily returns.\n    cutoff : float, optional\n        Decimal representing the percentage cutoff for the bottom percentile of\n        returns. Defaults to 0.05.\n\n    Returns\n    -------\n    VaR : float\n        The VaR value.\n    \"\"\"\n    return np.percentile(returns, 100 * cutoff)\n\n\ndef conditional_value_at_risk(returns, cutoff=0.05):\n    \"\"\"\n    Conditional value at risk (CVaR) of a returns stream.\n\n    CVaR measures the expected single-day returns of an asset on that asset's\n    worst performing days, where \"worst-performing\" is defined as falling below\n    ``cutoff`` as a percentile of all daily returns.\n\n    Parameters\n    ----------\n    returns : pandas.Series or 1-D numpy.array\n        Non-cumulative daily returns.\n    cutoff : float, optional\n        Decimal representing the percentage cutoff for the bottom percentile of\n        returns. Defaults to 0.05.\n\n    Returns\n    -------\n    CVaR : float\n        The CVaR value.\n    \"\"\"\n    # PERF: Instead of using the 'value_at_risk' function to find the cutoff\n    # value, which requires a call to numpy.percentile, determine the cutoff\n    # index manually and partition out the lowest returns values. The value at\n    # the cutoff index should be included in the partition.\n    cutoff_index = int((len(returns) - 1) * cutoff)\n    return np.mean(np.partition(returns, cutoff_index)[:cutoff_index + 1])\n\n\nSIMPLE_STAT_FUNCS = [\n    cum_returns_final,\n    annual_return,\n    annual_volatility,\n    sharpe_ratio,\n    calmar_ratio,\n    stability_of_timeseries,\n    max_drawdown,\n    omega_ratio,\n    sortino_ratio,\n    stats.skew,\n    stats.kurtosis,\n    tail_ratio,\n    cagr,\n    value_at_risk,\n    conditional_value_at_risk,\n]\n\nFACTOR_STAT_FUNCS = [\n    excess_sharpe,\n    alpha,\n    beta,\n    beta_fragility_heuristic,\n    gpd_risk_estimates,\n    capture,\n    up_capture,\n    down_capture\n]\n"
  },
  {
    "path": "EigenLedger/modules/empyrical/utils.py",
    "content": "#\n# Copyright 2018 Quantopian, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\nfrom datetime import datetime\nfrom functools import wraps\nfrom os import makedirs, environ\nfrom os.path import expanduser, join, getmtime, isdir\nimport errno\nimport warnings\n\nimport numpy as np\nfrom numpy.lib.stride_tricks import as_strided\nimport pandas as pd\nfrom pandas.tseries.offsets import BDay\ntry:\n    from pandas_datareader import data as web\nexcept ImportError:\n    msg = (\"Unable to import pandas_datareader. Suppressing import error and \"\n           \"continuing. All data reading functionality will raise errors; but \"\n           \"has been deprecated and will be removed in a later version.\")\n    warnings.warn(msg)\nfrom .deprecate import deprecated\n\nDATAREADER_DEPRECATION_WARNING = \\\n        (\"Yahoo and Google Finance have suffered large API breaks with no \"\n         \"stable replacement. As a result, any data reading functionality \"\n         \"in empyrical has been deprecated and will be removed in a future \"\n         \"version. See README.md for more details: \"\n         \"\\n\\n\"\n         \"\\thttps://github.com/quantopian/pyfolio/blob/master/README.md\")\ntry:\n    # fast versions\n    import bottleneck as bn\n\n    def _wrap_function(f):\n        @wraps(f)\n        def wrapped(*args, **kwargs):\n            out = kwargs.pop('out', None)\n            data = f(*args, **kwargs)\n            if out is None:\n                out = data\n            else:\n                out[()] = data\n\n            return out\n\n        return wrapped\n\n    nanmean = _wrap_function(bn.nanmean)\n    nanstd = _wrap_function(bn.nanstd)\n    nansum = _wrap_function(bn.nansum)\n    nanmax = _wrap_function(bn.nanmax)\n    nanmin = _wrap_function(bn.nanmin)\n    nanargmax = _wrap_function(bn.nanargmax)\n    nanargmin = _wrap_function(bn.nanargmin)\nexcept ImportError:\n    # slower numpy\n    nanmean = np.nanmean\n    nanstd = np.nanstd\n    nansum = np.nansum\n    nanmax = np.nanmax\n    nanmin = np.nanmin\n    nanargmax = np.nanargmax\n    nanargmin = np.nanargmin\n\n\ndef roll(*args, **kwargs):\n    \"\"\"\n    Calculates a given statistic across a rolling time period.\n\n    Parameters\n    ----------\n    returns : pd.Series or np.ndarray\n        Daily returns of the strategy, noncumulative.\n        - See full explanation in :func:`~empyrical.stats.cum_returns`.\n    factor_returns (optional): float / series\n        Benchmark return to compare returns against.\n    function:\n        the function to run for each rolling window.\n    window (keyword): int\n        the number of periods included in each calculation.\n    (other keywords): other keywords that are required to be passed to the\n        function in the 'function' argument may also be passed in.\n\n    Returns\n    -------\n    np.ndarray, pd.Series\n        depends on input type\n        ndarray(s) ==> ndarray\n        Series(s) ==> pd.Series\n\n        A Series or ndarray of the results of the stat across the rolling\n        window.\n\n    \"\"\"\n    func = kwargs.pop('function')\n    window = kwargs.pop('window')\n    if len(args) > 2:\n        raise ValueError(\"Cannot pass more than 2 return sets\")\n\n    if len(args) == 2:\n        if not isinstance(args[0], type(args[1])):\n            raise ValueError(\"The two returns arguments are not the same.\")\n\n    if isinstance(args[0], np.ndarray):\n        return _roll_ndarray(func, window, *args, **kwargs)\n    return _roll_pandas(func, window, *args, **kwargs)\n\n\ndef up(returns, factor_returns, **kwargs):\n    \"\"\"\n    Calculates a given statistic filtering only positive factor return periods.\n\n    Parameters\n    ----------\n    returns : pd.Series or np.ndarray\n        Daily returns of the strategy, noncumulative.\n        - See full explanation in :func:`~empyrical.stats.cum_returns`.\n    factor_returns (optional): float / series\n        Benchmark return to compare returns against.\n    function:\n        the function to run for each rolling window.\n    (other keywords): other keywords that are required to be passed to the\n        function in the 'function' argument may also be passed in.\n\n    Returns\n    -------\n    Same as the return of the function\n    \"\"\"\n    func = kwargs.pop('function')\n    returns = returns[factor_returns > 0]\n    factor_returns = factor_returns[factor_returns > 0]\n    return func(returns, factor_returns, **kwargs)\n\n\ndef down(returns, factor_returns, **kwargs):\n    \"\"\"\n    Calculates a given statistic filtering only negative factor return periods.\n\n    Parameters\n    ----------\n    returns : pd.Series or np.ndarray\n        Daily returns of the strategy, noncumulative.\n        - See full explanation in :func:`~empyrical.stats.cum_returns`.\n    factor_returns (optional): float / series\n        Benchmark return to compare returns against.\n    function:\n        the function to run for each rolling window.\n    (other keywords): other keywords that are required to be passed to the\n        function in the 'function' argument may also be passed in.\n\n    Returns\n    -------\n    Same as the return of the 'function'\n    \"\"\"\n    func = kwargs.pop('function')\n    returns = returns[factor_returns < 0]\n    factor_returns = factor_returns[factor_returns < 0]\n    return func(returns, factor_returns, **kwargs)\n\n\ndef _roll_ndarray(func, window, *args, **kwargs):\n    data = []\n    for i in range(window, len(args[0]) + 1):\n        rets = [s[i-window:i] for s in args]\n        data.append(func(*rets, **kwargs))\n    return np.array(data)\n\n\ndef _roll_pandas(func, window, *args, **kwargs):\n    data = {}\n    index_values = []\n    for i in range(window, len(args[0]) + 1):\n        rets = [s.iloc[i-window:i] for s in args]\n        index_value = args[0].index[i - 1]\n        index_values.append(index_value)\n        data[index_value] = func(*rets, **kwargs)\n    return pd.Series(data, index=type(args[0].index)(index_values))\n\n\n@deprecated(msg=DATAREADER_DEPRECATION_WARNING)\ndef cache_dir(environ=environ):\n    try:\n        return environ['EMPYRICAL_CACHE_DIR']\n    except KeyError:\n        return join(\n\n            environ.get(\n                'XDG_CACHE_HOME',\n                expanduser('~/.cache/'),\n            ),\n            'empyrical',\n        )\n\n\n@deprecated(msg=DATAREADER_DEPRECATION_WARNING)\ndef data_path(name):\n    return join(cache_dir(), name)\n\n\n@deprecated(msg=DATAREADER_DEPRECATION_WARNING)\ndef ensure_directory(path):\n    \"\"\"\n    Ensure that a directory named \"path\" exists.\n    \"\"\"\n\n    try:\n        makedirs(path)\n    except OSError as exc:\n        if exc.errno != errno.EEXIST or not isdir(path):\n            raise\n\n\ndef get_utc_timestamp(dt):\n    \"\"\"\n    Returns the Timestamp/DatetimeIndex\n    with either localized or converted to UTC.\n\n    Parameters\n    ----------\n    dt : Timestamp/DatetimeIndex\n        the date(s) to be converted\n\n    Returns\n    -------\n    same type as input\n        date(s) converted to UTC\n    \"\"\"\n\n    dt = pd.to_datetime(dt)\n    try:\n        dt = dt.tz_localize('UTC')\n    except TypeError:\n        dt = dt.tz_convert('UTC')\n    return dt\n\n\n_1_bday = BDay()\n\n\ndef _1_bday_ago():\n    return pd.Timestamp.now().normalize() - _1_bday\n\n\n@deprecated(msg=DATAREADER_DEPRECATION_WARNING)\ndef get_fama_french():\n    \"\"\"\n    Retrieve Fama-French factors via pandas-datareader\n    Returns\n    -------\n    pandas.DataFrame\n        Percent change of Fama-French factors\n    \"\"\"\n\n    start = '1/1/1970'\n    research_factors = web.DataReader('F-F_Research_Data_Factors_daily',\n                                      'famafrench', start=start)[0]\n    momentum_factor = web.DataReader('F-F_Momentum_Factor_daily',\n                                     'famafrench', start=start)[0]\n    five_factors = research_factors.join(momentum_factor).dropna()\n    five_factors /= 100.\n    five_factors.index = five_factors.index.tz_localize('utc')\n\n    five_factors.columns = five_factors.columns.str.strip()\n\n    return five_factors\n\n\n@deprecated(msg=DATAREADER_DEPRECATION_WARNING)\ndef get_returns_cached(filepath, update_func, latest_dt, **kwargs):\n    \"\"\"\n    Get returns from a cached file if the cache is recent enough,\n    otherwise, try to retrieve via a provided update function and\n    update the cache file.\n    Parameters\n    ----------\n    filepath : str\n        Path to cached csv file\n    update_func : function\n        Function to call in case cache is not up-to-date.\n    latest_dt : pd.Timestamp (tz=UTC)\n        Latest datetime required in csv file.\n    **kwargs : Keyword arguments\n        Optional keyword arguments will be passed to update_func()\n    Returns\n    -------\n    pandas.DataFrame\n        DataFrame containing returns\n    \"\"\"\n\n    update_cache = False\n\n    try:\n        mtime = getmtime(filepath)\n    except OSError as e:\n        if e.errno != errno.ENOENT:\n            raise\n        update_cache = True\n    else:\n\n        file_dt = pd.Timestamp(mtime, unit='s')\n\n        if latest_dt.tzinfo:\n            file_dt = file_dt.tz_localize('utc')\n\n        if file_dt < latest_dt:\n            update_cache = True\n        else:\n            returns = pd.read_csv(filepath, index_col=0, parse_dates=True)\n            returns.index = returns.index.tz_localize(\"UTC\")\n\n    if update_cache:\n        returns = update_func(**kwargs)\n        try:\n            ensure_directory(cache_dir())\n        except OSError as e:\n            warnings.warn(\n                'could not update cache: {}. {}: {}'.format(\n                    filepath, type(e).__name__, e,\n                ),\n                UserWarning,\n            )\n\n        try:\n            returns.to_csv(filepath)\n        except OSError as e:\n            warnings.warn(\n                'could not update cache {}. {}: {}'.format(\n                    filepath, type(e).__name__, e,\n                ),\n                UserWarning,\n            )\n\n    return returns\n\n\n@deprecated(msg=DATAREADER_DEPRECATION_WARNING)\ndef load_portfolio_risk_factors(filepath_prefix=None, start=None, end=None):\n    \"\"\"\n    Load risk factors Mkt-Rf, SMB, HML, Rf, and UMD.\n    Data is stored in HDF5 file. If the data is more than 2\n    days old, redownload from Dartmouth.\n    Returns\n    -------\n    five_factors : pd.DataFrame\n        Risk factors timeseries.\n    \"\"\"\n\n    if start is None:\n        start = '1/1/1970'\n    if end is None:\n        end = _1_bday_ago()\n\n    start = get_utc_timestamp(start)\n    end = get_utc_timestamp(end)\n\n    if filepath_prefix is None:\n        filepath = data_path('factors.csv')\n    else:\n        filepath = filepath_prefix\n\n    five_factors = get_returns_cached(filepath, get_fama_french, end)\n\n    return five_factors.loc[start:end]\n\n\n@deprecated(msg=DATAREADER_DEPRECATION_WARNING)\ndef get_treasury_yield(start=None, end=None, period='3MO'):\n    \"\"\"\n    Load treasury yields from FRED.\n\n    Parameters\n    ----------\n    start : date, optional\n        Earliest date to fetch data for.\n        Defaults to earliest date available.\n    end : date, optional\n        Latest date to fetch data for.\n        Defaults to latest date available.\n    period : {'1MO', '3MO', '6MO', 1', '5', '10'}, optional\n        Which maturity to use.\n    Returns\n    -------\n    pd.Series\n        Annual treasury yield for every day.\n    \"\"\"\n\n    if start is None:\n        start = '1/1/1970'\n    if end is None:\n        end = _1_bday_ago()\n\n    treasury = web.DataReader(\"DGS3{}\".format(period), \"fred\",\n                              start, end)\n\n    treasury = treasury.ffill()\n\n    return treasury\n\n\n@deprecated(msg=DATAREADER_DEPRECATION_WARNING)\ndef get_symbol_returns_from_yahoo(symbol, start=None, end=None):\n    \"\"\"\n    Wrapper for pandas.io.data.get_data_yahoo().\n    Retrieves prices for symbol from yahoo and computes returns\n    based on adjusted closing prices.\n\n    Parameters\n    ----------\n    symbol : str\n        Symbol name to load, e.g. 'SPY'\n    start : pandas.Timestamp compatible, optional\n        Start date of time period to retrieve\n    end : pandas.Timestamp compatible, optional\n        End date of time period to retrieve\n\n    Returns\n    -------\n    pandas.DataFrame\n        Returns of symbol in requested period.\n    \"\"\"\n\n    try:\n        px = web.get_data_yahoo(symbol, start=start, end=end)\n        px['date'] = pd.to_datetime(px['date'])\n        px.set_index('date', drop=False, inplace=True)\n        rets = px[['adjclose']].pct_change().dropna()\n    except Exception as e:\n        warnings.warn(\n            'Yahoo Finance read failed: {}, falling back to Google'.format(e),\n            UserWarning)\n        px = web.get_data_google(symbol, start=start, end=end)\n        rets = px[['Close']].pct_change().dropna()\n\n    rets.index = rets.index.tz_localize(\"UTC\")\n    rets.columns = [symbol]\n    return rets\n\n\n@deprecated(msg=DATAREADER_DEPRECATION_WARNING)\ndef default_returns_func(symbol, start=None, end=None):\n    \"\"\"\n    Gets returns for a symbol.\n    Queries Yahoo Finance. Attempts to cache SPY.\n\n    Parameters\n    ----------\n    symbol : str\n        Ticker symbol, e.g. APPL.\n    start : date, optional\n        Earliest date to fetch data for.\n        Defaults to earliest date available.\n    end : date, optional\n        Latest date to fetch data for.\n        Defaults to latest date available.\n\n    Returns\n    -------\n    pd.Series\n        Daily returns for the symbol.\n         - See full explanation in tears.create_full_tear_sheet (returns).\n    \"\"\"\n\n    if start is None:\n        start = '1/1/1970'\n    if end is None:\n        end = _1_bday_ago()\n\n    start = get_utc_timestamp(start)\n    end = get_utc_timestamp(end)\n\n    if symbol == 'SPY':\n        filepath = data_path('spy.csv')\n        rets = get_returns_cached(filepath,\n                                  get_symbol_returns_from_yahoo,\n                                  end,\n                                  symbol='SPY',\n                                  start='1/1/1970',\n                                  end=datetime.now())\n        rets = rets[start:end]\n    else:\n        rets = get_symbol_returns_from_yahoo(symbol, start=start, end=end)\n\n    return rets[symbol]\n\n\ndef rolling_window(array, length, mutable=False):\n    \"\"\"\n    Restride an array of shape\n\n        (X_0, ... X_N)\n\n    into an array of shape\n\n        (length, X_0 - length + 1, ... X_N)\n\n    where each slice at index i along the first axis is equivalent to\n\n        result[i] = array[length * i:length * (i + 1)]\n\n    Parameters\n    ----------\n    array : np.ndarray\n        The base array.\n    length : int\n        Length of the synthetic first axis to generate.\n    mutable : bool, optional\n        Return a mutable array? The returned array shares the same memory as\n        the input array. This means that writes into the returned array affect\n        ``array``. The returned array also uses strides to map the same values\n        to multiple indices. Writes to a single index may appear to change many\n        values in the returned array.\n\n    Returns\n    -------\n    out : np.ndarray\n\n    Example\n    -------\n    >>> from numpy import arange\n    >>> a = arange(25).reshape(5, 5)\n    >>> a\n    array([[ 0,  1,  2,  3,  4],\n           [ 5,  6,  7,  8,  9],\n           [10, 11, 12, 13, 14],\n           [15, 16, 17, 18, 19],\n           [20, 21, 22, 23, 24]])\n\n    >>> rolling_window(a, 2)\n    array([[[ 0,  1,  2,  3,  4],\n            [ 5,  6,  7,  8,  9]],\n    <BLANKLINE>\n           [[ 5,  6,  7,  8,  9],\n            [10, 11, 12, 13, 14]],\n    <BLANKLINE>\n           [[10, 11, 12, 13, 14],\n            [15, 16, 17, 18, 19]],\n    <BLANKLINE>\n           [[15, 16, 17, 18, 19],\n            [20, 21, 22, 23, 24]]])\n    \"\"\"\n    if not length:\n        raise ValueError(\"Can't have 0-length window\")\n\n    orig_shape = array.shape\n    if not orig_shape:\n        raise IndexError(\"Can't restride a scalar.\")\n    elif orig_shape[0] < length:\n        raise IndexError(\n            \"Can't restride array of shape {shape} with\"\n            \" a window length of {len}\".format(\n                shape=orig_shape,\n                len=length,\n            )\n        )\n\n    num_windows = (orig_shape[0] - length + 1)\n    new_shape = (num_windows, length) + orig_shape[1:]\n\n    new_strides = (array.strides[0],) + array.strides\n\n    out = as_strided(array, new_shape, new_strides)\n    out.setflags(write=mutable)\n    return out\n"
  },
  {
    "path": "EigenLedger/run.py",
    "content": "from main import portfolio_analysis, Engine\nimport pandas as pd\n\n# Define custom data\nportfolio_data = pd.DataFrame({\n    \"AAPL\": [145.0, 147.0, 149.0],\n    \"MSFT\": [240.0, 242.0, 245.0],\n    \"GOOGL\": [2700.0, 2725.0, 2750.0],\n}, index=pd.to_datetime([\"2023-01-01\", \"2023-01-02\", \"2023-01-03\"]))\n\nbenchmark_data = pd.DataFrame({\n    \"TGT\": [420.0, 425.0, 430.0],\n}, index=pd.to_datetime([\"2023-01-01\", \"2023-01-02\", \"2023-01-03\"]))\n\n\nportfolio = Engine(\n    start_date=\"2023-01-01\",\n    portfolio=[\"AAPL\", \"MSFT\", \"GOOGL\"],\n    weights=[0.4, 0.3, 0.3],\n    data=portfolio_data,\n    benchmark=[\"TGT\"],\n    benchmark_data=benchmark_data\n)\n\n# Fetch benchmark data and analyze\nportfolio.fetch_benchmark_data()\nportfolio_analysis(portfolio)\n\n"
  },
  {
    "path": "LICENSE",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"{}\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright 2024 Santosh P.\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "README.md",
    "content": "#### 📢 Announcement \nGood news! You can now use a patched version of the library [empyrical](https://github.com/quantopian/empyrical) through EigenLedger! 🎉\n<br>\n👉 Learn [how to use it here](https://eigenledger.gitbook.io/eigenledger/using-empyrical/using-empyrical) and read more in [this announcement post](https://github.com/santoshlite/EigenLedger/discussions/128).\n<br>\n\n# By Investors, For Investors.\n<br>\n<div align=\"center\">\n<img src=\"https://github.com/user-attachments/assets/470f1d59-09c6-4b95-af7e-f142764d8195\"/>\n<br><br><br><br>\n\n![](https://img.shields.io/badge/Downloads-245k-brightgreen)\n![](https://img.shields.io/badge/license-MIT-orange)\n![](https://img.shields.io/badge/version-2.1.6-blueviolet)\n![](https://img.shields.io/badge/language-python🐍-blue)\n![](https://img.shields.io/badge/Open%20source-💜-white)\t\n[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/drive/1TyNgudyFcsgob7o49PwfDJHLaHvluxaU?usp=sharing)\n  \n </div>\n\n<br>\n\nWant to read this in **Mandarin 🇨🇳**? Click [**here**](README_CN.md)\n\nEigenLedger (prev. \"Empyrial\") is a Python-based **open-source quantitative investment** library dedicated to **financial institutions** and **retail investors**, officially released in 2021. Already used by **thousands of people working in the finance industry**, EigenLedger aims to become an all-in-one platform for **portfolio management**, **analysis**, and **optimization**.\n\nEigenLedger **empowers portfolio management** by bringing the best of **performance and risk analysis** in an **easy-to-understand**, **flexible** and **powerful framework**.\n\nWith EigenLedger, you can easily analyze security or a portfolio in order to **get the best insights from it**. This is mainly a **wrapper** of financial analysis libraries such as **Quantstats** and **PyPortfolioOpt**.\n\n<br>\n\n<br>\n\n\n\n<div align=\"center\">\n  \n| Table of Contents 📖 | \n| --                     \n| 1. [Installation](#installation) | \n| 2. [Documentation](#documentation) | \n| 3. [Quickstart](#quickstart) |\n| 4. [Contribution and Issues](#contribution-and-issues) | \n| 5. [Contributors](#contributors) |\n| 6. [Contact](#contact) |\n| 7. [License](#license) |\n\t\n</div>\n\n\n\n\n## Installation\n\nYou can install EigenLedger using pip:\n\n```\npip install EigenLedger\n```\n\nFor a better experience, **we advise you to use EigenLedger on a notebook** (e.g., Jupyter, Google Colab)\n\n_Note: macOS users will need to install [Xcode Command Line Tools](https://osxdaily.com/2014/02/12/install-command-line-tools-mac-os-x/)._\n\n_Note: Windows users will need to install C++. ([download](https://visualstudio.microsoft.com/thank-you-downloading-visual-studio/?sku=BuildTools&rel=16), [install instructions](https://drive.google.com/file/d/0B4GsMXCRaSSIOWpYQkstajlYZ0tPVkNQSElmTWh1dXFaYkJr/view))_\n\n\n\n## Documentation\n\nHere is our full [documentation](https://eigenledger.gitbook.io/documentation)! Check it out our full documentation for detailed guides, all features, and tips on getting the most out of this library.\n\n\n\n## Quickstart\n\n```py\nfrom EigenLedger import portfolio_analysis, Engine\n\nportfolio = Engine(\n    start_date = \"2018-08-01\", \n    portfolio = [\"BABA\", \"PDD\", \"KO\", \"AMD\",\"^IXIC\"], \n    weights = [0.2, 0.2, 0.2, 0.2, 0.2],  # equal weighting is set by default\n    benchmark = [\"SPY\"]  # SPY is set by default\n)\n\nportfolio_analysis(portfolio)\n```\n\n\n\n<div align=\"center\">\n\n![image](https://user-images.githubusercontent.com/61618641/126879140-ea03ff17-a7c6-481a-bb3e-61c055b31267.png)\n![image](https://user-images.githubusercontent.com/61618641/126879203-4390813c-a4f2-41b9-916b-e03dd8bafffb.png)\n![image](https://user-images.githubusercontent.com/61618641/128025087-04afed7e-96ab-4730-9bd8-98f5491b2b5d.png)\n![image](https://user-images.githubusercontent.com/61618641/126879204-01fe1eca-00b8-438e-b489-0213535dd31b.png)\n![image](https://user-images.githubusercontent.com/61618641/126879210-9fd61e2b-01ab-4bfd-b679-3b1867d9302d.png)\n![image](https://user-images.githubusercontent.com/61618641/126879215-e24c929a-55be-4912-8e2c-043e31ff2a95.png)\n![image](https://user-images.githubusercontent.com/61618641/126879221-455b8ffa-c958-4ac9-ae98-d15b4c5f0826.png)\n![image](https://user-images.githubusercontent.com/61618641/126879222-08906643-16db-441e-a099-7ac3b00bdbd7.png)\n![image](https://user-images.githubusercontent.com/61618641/126879223-f1116dc3-cceb-493c-93b3-2d3810cae789.png)\n![image](https://user-images.githubusercontent.com/61618641/126879225-dc879b71-2070-46ed-a8ad-e90880050be8.png)\n![image](https://user-images.githubusercontent.com/61618641/126879297-cb78743a-6d43-465b-8021-d4b62a659828.png)\n\n</div>\n\n\n## Stargazers over time\n\n<div align=\"center\">\n\t\n![追星族的时间](https://starchart.cc/ssantoshp/empyrial.svg)\n\t\n</div>\n\n## Contribution and Issues\nEigenLedger uses GitHub to host its source code.  *Learn more about the [Github flow](https://docs.github.com/en/get-started/quickstart/github-flow).*  \n\nFor larger changes (e.g., new feature request, large refactoring), please open an issue to discuss first.  \n\n* If you wish to create a new Issue, then [click here to create a new issue](https://github.com/ssantoshp/EigenLedger/issues/new/choose).  \n\nSmaller improvements (e.g., document improvements, bugfixes) can be handled by the Pull Request process of GitHub: [pull requests](https://github.com/ssantoshp/EigenLedger/pulls).  \n\n* To contribute to the code, you will need to do the following:  \n\n * [Fork](https://docs.github.com/en/get-started/quickstart/fork-a-repo#forking-a-repository) [EigenLedger](https://github.com/ssantoshp/EigenLedger) - Click the **Fork** button at the upper right corner of this page. \n * [Clone your own fork](https://docs.github.com/en/get-started/quickstart/fork-a-repo#cloning-your-forked-repository).  E.g., ```git clone https://github.com/ssantoshp/EigenLedger.git```  \n  *If your fork is out of date, then will you need to manually sync your fork: [Synchronization method](https://help.github.com/articles/syncing-a-fork/)*\n * [Create a Pull Request](https://github.com/ssantoshp/EigenLedger/pulls) using **your fork** as the `compare head repository`. \n\nYou contributions will be reviewed, potentially modified, and hopefully merged into EigenLedger.  \n\n## Contributors\n\nThanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):\n\n[![All Contributors](https://img.shields.io/badge/all_contributors-11-orange.svg?style=flat-square)](#contributors-)\n\n<table>\n  <tr>\n    <td align=\"center\"><a href=\"https://github.com/BrendanGlancy\"><img src=\"https://avatars.githubusercontent.com/u/61941978?v=4\" width=\"100px;\" alt=\"\"/><br /><sub><b>Brendan Glancy</b></sub></a><br /><a title=\"Code\">💻</a> <a title=\"Bug report\">🐛</a></td>\n    <td align=\"center\"><a href=\"https://github.com/rslopes\"><img src=\"https://avatars.githubusercontent.com/u/24928343?v=4\" width=\"100px;\" alt=\"\"/><br /><sub><b>Renan Lopes</b></sub></a><br /><a title=\"Code\">💻</a> <a title=\"Bug report\">🐛</a></td>\n    <td align=\"center\"><a href=\"https://github.com/markthebault\"><img src=\"https://avatars.githubusercontent.com/u/3846664?v=4\" width=\"100px;\" alt=\"\"/><br /><sub><b>Mark Thebault</b></sub></a><br /><a title=\"Code\">💻</a></td>\n    <td align=\"center\"><a href=\"https://github.com/diegodalvarez\"><img src=\"https://avatars.githubusercontent.com/u/48641554?v=4\" width=\"100px;\" alt=\"\"/><br /><sub><b>Diego Alvarez</b></sub></a><br /><a title=\"Code\">💻🐛</a></td>\n    <td align=\"center\"><a href=\"https://github.com/rakeshbhat9\"><img src=\"https://avatars.githubusercontent.com/u/11472305?v=4\" width=\"100px;\" alt=\"\"/><br /><sub><b>Rakesh Bhat</b></sub></a><br /><a title=\"Code\">💻</a></td>\n    <td align=\"center\"><a href=\"https://github.com/Haizzz\"><img src=\"https://avatars.githubusercontent.com/u/5275680?v=4\" width=\"100px;\" alt=\"\"/><br /><sub><b>Anh Le</b></sub></a><br /><a title=\"Bug report\">🐛</a></td>\n    <td align=\"center\"><a href=\"https://github.com/TonyZhangkz\"><img src=\"https://avatars.githubusercontent.com/u/65281213?v=4\" width=\"100px;\" alt=\"\"/><br /><sub><b>Tony Zhang</b></sub></a><br /><a title=\"Code\">💻</a></td>\n    <td align=\"center\"><a href=\"https://github.com/eltociear\"><img src=\"https://avatars.githubusercontent.com/u/22633385?v=4\" width=\"100px;\" alt=\"\"/><br /><sub><b>Ikko Ashimine</b></sub></a><br /><a title=\"Code\">✒️</a></td>\n    <td align=\"center\"><a href=\"https://www.youtube.com/watch?v=-4qx3tbtTgs\"><img src=\"https://avatars.githubusercontent.com/u/50767660?v=4\" width=\"100px;\" alt=\"\"/><br /><sub><b>QuantNomad</b></sub></a><br /><a title=\"Code\">📹</a></td>\n    <td align=\"center\"><a href=\"https://github.com/buckleyc\"><img src=\"https://avatars.githubusercontent.com/u/4175900?v=4\" width=\"100px;\" alt=\"\"/><br /><sub><b>Buckley</b></sub></a><br /><a title=\"Code\">✒️💻</a></td>\n    <td align=\"center\"><a href=\"https://github.com/agn35\"><img src=\"https://lh3.googleusercontent.com/a-/AOh14GhXGFHHpVQTL2r23oEXFssH0f7RyoGDihrS_HmT=s48\" width=\"100px;\" alt=\"\"/><br /><sub><b>Adam Nelsson</b></sub></a><br /><a title=\"Code\">💻</a></td>\n    <td align=\"center\"><a href=\"https://github.com/rgleavenworth\"><img src=\"https://avatars.githubusercontent.com/u/87843950?v=4\" width=\"100px;\" alt=\"\"/><br /><sub><b>Ranjan Grover</b></sub></a><br /><a title=\"Code\">🐛💻</a></td>\n  </tr>\n</table>\n\nThis project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. **Contributions of any kind are welcome!**\n\n## Credit\n\nThis library has also been made possible because of the work of these incredible people:\n- [**Ran Aroussi**](https://github.com/ranaroussi) for the [**Quantstats library**](https://github.com/ranaroussi/quantstats) \n- [**Robert Martin**](https://github.com/robertmartin8) for the [**PyPortfolioOpt**](https://github.com/robertmartin8/PyPortfolioOpt) \n\n## Contact\n\nYou are welcome to contact us by email at **santoshpassoubady@gmail.com** or in EigenLedger's [discussion space](https://github.com/ssantoshp/EigenLedger/discussions)\n\n## License\n\nApache License 2.0"
  },
  {
    "path": "README_CN.md",
    "content": "#### 📢 公告\n\n好消息！你现在可以通过 EigenLedger 使用维护的 [empyrical](https://github.com/quantopian/empyrical) 库版本了！🎉\n<br>\n👉 在[这里](https://eigenledger.gitbook.io/eigenledger/using-empyrical/using-empyrical)了解如何使用它，并阅读[此公告帖子](https://github.com/santoshlite/EigenLedger/discussions/128)了解更多信息。\n<br>\n\n# 投资者为投资者打造\n<br>\n<div align=\"center\">\n<img src=\"https://github.com/user-attachments/assets/470f1d59-09c6-4b95-af7e-f142764d8195\"/>\n<br><br><br><br>\n\n![](https://img.shields.io/badge/Downloads-245k-brightgreen)\n![](https://img.shields.io/badge/license-MIT-orange)\n![](https://img.shields.io/badge/version-2.1.6-blueviolet)\n![](https://img.shields.io/badge/language-python🐍-blue)\n![](https://img.shields.io/badge/Open%20source-💜-white)\t\n[![在 Colab 中打开](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/drive/1TyNgudyFcsgob7o49PwfDJHLaHvluxaU?usp=sharing)\n  \n</div>\n\n<br>\n\n想要阅读**英文版 🇺🇸**？请点击[**这里**](README.md)\n\nEigenLedger（原名 \"Empyrial\"）是一个基于 Python 的**开源量化投资**库，专为**金融机构**和**散户投资者**打造，正式发布于 2021 年。EigenLedger 已被**数千名金融行业人士**使用，旨在成为集**投资组合管理**、**分析**和**优化**于一体的平台。\n\nEigenLedger 通过在一个**易于理解**、**灵活**和**强大**的框架中提供最佳的**绩效和风险分析**，**赋能投资组合管理**。\n\n使用 EigenLedger，您可以轻松分析证券或投资组合，以**获得最佳洞察**。它主要是**Quantstats** 和 **PyPortfolioOpt** 等金融分析库的**封装器**。\n\n<br>\n\n<br>\n\n\n\n<div align=\"center\">\n  \n| 目录 📖 | \n| --                     \n| 1. [安装](#安装) | \n| 2. [文档](#文档) | \n| 3. [快速开始](#快速开始) |\n| 4. [贡献和问题](#贡献和问题) | \n| 5. [贡献者](#贡献者) |\n| 6. [联系方式](#联系方式) |\n| 7. [许可证](#许可证) |\n  \n</div>\n\n\n\n\n## 安装\n\n您可以使用 pip 安装 EigenLedger：\n\n```\npip install EigenLedger\n```\n\n为了获得更好的体验，**我们建议您在笔记本环境中使用 EigenLedger**（例如，Jupyter，Google Colab）\n\n_注意：macOS 用户需要安装 [Xcode 命令行工具](https://osxdaily.com/2014/02/12/install-command-line-tools-mac-os-x/)。_\n\n_注意：Windows 用户需要安装 C++。([下载](https://visualstudio.microsoft.com/thank-you-downloading-visual-studio/?sku=BuildTools&rel=16)，[安装说明](https://drive.google.com/file/d/0B4GsMXCRaSSIOWpYQkstajlYZ0tPVkNQSElmTWh1dXFaYkJr/view))_\n\n\n\n## 文档\n\n这是我们的完整[文档](https://eigenledger.gitbook.io/documentation)！查看我们的完整文档，获取详细指南、所有功能，以及充分利用此库的技巧。\n\n\n\n## 快速开始\n\n```py\nfrom EigenLedger import portfolio_analysis, Engine\n\nportfolio = Engine(\n    start_date = \"2018-08-01\", \n    portfolio = [\"BABA\", \"PDD\", \"KO\", \"AMD\",\"^IXIC\"], \n    weights = [0.2, 0.2, 0.2, 0.2, 0.2],  # 默认设置为等权重\n    benchmark = [\"SPY\"]  # 默认设置为 SPY\n)\n\nportfolio_analysis(portfolio)\n```\n\n\n\n<div align=\"center\">\n\n![image](https://user-images.githubusercontent.com/61618641/126879140-ea03ff17-a7c6-481a-bb3e-61c055b31267.png)\n![image](https://user-images.githubusercontent.com/61618641/126879203-4390813c-a4f2-41b9-916b-e03dd8bafffb.png)\n![image](https://user-images.githubusercontent.com/61618641/128025087-04afed7e-96ab-4730-9bd8-98f5491b2b5d.png)\n![image](https://user-images.githubusercontent.com/61618641/126879204-01fe1eca-00b8-438e-b489-0213535dd31b.png)\n![image](https://user-images.githubusercontent.com/61618641/126879210-9fd61e2b-01ab-4bfd-b679-3b1867d9302d.png)\n![image](https://user-images.githubusercontent.com/61618641/126879215-e24c929a-55be-4912-8e2c-043e31ff2a95.png)\n![image](https://user-images.githubusercontent.com/61618641/126879221-455b8ffa-c958-4ac9-ae98-d15b4c5f0826.png)\n![image](https://user-images.githubusercontent.com/61618641/126879222-08906643-16db-441e-a099-7ac3b00bdbd7.png)\n![image](https://user-images.githubusercontent.com/61618641/126879223-f1116dc3-cceb-493c-93b3-2d3810cae789.png)\n![image](https://user-images.githubusercontent.com/61618641/126879225-dc879b71-2070-46ed-a8ad-e90880050be8.png)\n![image](https://user-images.githubusercontent.com/61618641/126879297-cb78743a-6d43-465b-8021-d4b62a659828.png)\n\n</div>\n\n\n## 星标数随时间变化\n\n<div align=\"center\">\n\t\n![星标数随时间变化](https://starchart.cc/ssantoshp/empyrial.svg)\n\t\n</div>\n\n## 贡献和问题\nEigenLedger 使用 GitHub 来托管其源代码。*了解更多关于 [GitHub 流程](https://docs.github.com/en/get-started/quickstart/github-flow)的信息。*  \n\n对于较大的更改（例如，新功能请求、大型重构），请先打开一个 issue 进行讨论。  \n\n* 如果您想创建一个新的 Issue，请[点击这里创建新 Issue](https://github.com/ssantoshp/EigenLedger/issues/new/choose)。  \n\n较小的改进（例如，文档改进、错误修复）可以通过 GitHub 的 Pull Request 流程处理：[拉取请求](https://github.com/ssantoshp/EigenLedger/pulls)。  \n\n* 要贡献代码，您需要执行以下操作：  \n\n * [Fork](https://docs.github.com/en/get-started/quickstart/fork-a-repo#forking-a-repository) [EigenLedger](https://github.com/ssantoshp/EigenLedger) - 点击本页面右上角的 **Fork** 按钮。 \n * [克隆您自己的 fork](https://docs.github.com/en/get-started/quickstart/fork-a-repo#cloning-your-forked-repository)。例如，```git clone https://github.com/ssantoshp/EigenLedger.git```  \n  *如果您的 fork 过期了，您需要手动同步您的 fork：[同步方法](https://help.github.com/articles/syncing-a-fork/)*  \n * 使用您的 **fork** 作为 `compare head repository`，[创建一个 Pull Request](https://github.com/ssantoshp/EigenLedger/pulls)。  \n\n您的贡献将被审核，可能会被修改，并希望合并到 EigenLedger 中。  \n\n## 贡献者\n\n感谢这些了不起的人（[emoji 说明](https://allcontributors.org/docs/en/emoji-key)）：\n\n[![All Contributors](https://img.shields.io/badge/all_contributors-11-orange.svg?style=flat-square)](#contributors-)\n\n<table>\n  <tr>\n    <td align=\"center\"><a href=\"https://github.com/BrendanGlancy\"><img src=\"https://avatars.githubusercontent.com/u/61941978?v=4\" width=\"100px;\" alt=\"\"/><br /><sub><b>Brendan Glancy</b></sub></a><br /><a title=\"Code\">💻</a> <a title=\"Bug report\">🐛</a></td>\n    <td align=\"center\"><a href=\"https://github.com/rslopes\"><img src=\"https://avatars.githubusercontent.com/u/24928343?v=4\" width=\"100px;\" alt=\"\"/><br /><sub><b>Renan Lopes</b></sub></a><br /><a title=\"Code\">💻</a> <a title=\"Bug report\">🐛</a></td>\n    <td align=\"center\"><a href=\"https://github.com/markthebault\"><img src=\"https://avatars.githubusercontent.com/u/3846664?v=4\" width=\"100px;\" alt=\"\"/><br /><sub><b>Mark Thebault</b></sub></a><br /><a title=\"Code\">💻</a></td>\n    <td align=\"center\"><a href=\"https://github.com/diegodalvarez\"><img src=\"https://avatars.githubusercontent.com/u/48641554?v=4\" width=\"100px;\" alt=\"\"/><br /><sub><b>Diego Alvarez</b></sub></a><br /><a title=\"Code\">💻🐛</a></td>\n    <td align=\"center\"><a href=\"https://github.com/rakeshbhat9\"><img src=\"https://avatars.githubusercontent.com/u/11472305?v=4\" width=\"100px;\" alt=\"\"/><br /><sub><b>Rakesh Bhat</b></sub></a><br /><a title=\"Code\">💻</a></td>\n    <td align=\"center\"><a href=\"https://github.com/Haizzz\"><img src=\"https://avatars.githubusercontent.com/u/5275680?v=4\" width=\"100px;\" alt=\"\"/><br /><sub><b>Anh Le</b></sub></a><br /><a title=\"Bug report\">🐛</a></td>\n    <td align=\"center\"><a href=\"https://github.com/TonyZhangkz\"><img src=\"https://avatars.githubusercontent.com/u/65281213?v=4\" width=\"100px;\" alt=\"\"/><br /><sub><b>Tony Zhang</b></sub></a><br /><a title=\"Code\">💻</a></td>\n    <td align=\"center\"><a href=\"https://github.com/eltociear\"><img src=\"https://avatars.githubusercontent.com/u/22633385?v=4\" width=\"100px;\" alt=\"\"/><br /><sub><b>Ikko Ashimine</b></sub></a><br /><a title=\"Code\">✒️</a></td>\n    <td align=\"center\"><a href=\"https://www.youtube.com/watch?v=-4qx3tbtTgs\"><img src=\"https://avatars.githubusercontent.com/u/50767660?v=4\" width=\"100px;\" alt=\"\"/><br /><sub><b>QuantNomad</b></sub></a><br /><a title=\"Code\">📹</a></td>\n    <td align=\"center\"><a href=\"https://github.com/buckleyc\"><img src=\"https://avatars.githubusercontent.com/u/4175900?v=4\" width=\"100px;\" alt=\"\"/><br /><sub><b>Buckley</b></sub></a><br /><a title=\"Code\">✒️💻</a></td>\n    <td align=\"center\"><a href=\"https://github.com/agn35\"><img src=\"https://lh3.googleusercontent.com/a-/AOh14GhXGFHHpVQTL2r23oEXFssH0f7RyoGDihrS_HmT=s48\" width=\"100px;\" alt=\"\"/><br /><sub><b>Adam Nelsson</b></sub></a><br /><a title=\"Code\">💻</a></td>\n    <td align=\"center\"><a href=\"https://github.com/rgleavenworth\"><img src=\"https://avatars.githubusercontent.com/u/87843950?v=4\" width=\"100px;\" alt=\"\"/><br /><sub><b>Ranjan Grover</b></sub></a><br /><a title=\"Code\">🐛💻</a></td>\n  </tr>\n</table>\n\n本项目遵循 [all-contributors](https://github.com/all-contributors/all-contributors) 规范。**欢迎任何形式的贡献！**\n\n## 致谢\n\n由于这些令人难以置信的人的工作，这个库才成为可能：\n- [**Ran Aroussi**](https://github.com/ranaroussi) 的 [**Quantstats 库**](https://github.com/ranaroussi/quantstats) \n- [**Robert Martin**](https://github.com/robertmartin8) 的 [**PyPortfolioOpt**](https://github.com/robertmartin8/PyPortfolioOpt) \n\n## 联系方式\n\n欢迎通过电子邮件 **santoshpassoubady@gmail.com** 或在 EigenLedger 的[讨论空间](https://github.com/ssantoshp/EigenLedger/discussions)与我们联系\n\n## 许可证\n\nApache 许可证 2.0\n"
  },
  {
    "path": "pyproject.toml",
    "content": "[tool.poetry]\nname = \"EigenLedger\"\nauthors = [\"Santosh <santoshpassoubadyp@gmail.com>\"]\nversion = \"2.1.6\"\ndescription = \"An Open Source Portfolio Management Framework for Everyone 投资组合管理\"\nreadme = \"README.md\"\nlicense = \"LICENSE\"\nhomepage = \"https://github.com/ssantoshp/EigenLedger\"\n\n[tool.poetry.dependencies]\npython = \">=3.0\"\nnumpy = \"^1.21.0\"\nmatplotlib = \"^3.4.0\"\nquantstats = \"^0.0.62\"\nyfinance = \"^0.1.0\"\nipython = \"^7.16.0\"\nfpdf = \"^1.7.2\"\npyportfolioopt = \"^1.4.0\"\n\n[[tool.poetry.packages]]\ninclude = \"EigenLedger\"\n\n[build-system]\nrequires = [\"poetry-core>=1.0.0\"]\nbuild-backend = \"poetry.core.masonry.api\"\n\n"
  }
]