├── .gitignore ├── LICENSE ├── README.md ├── images ├── example_df.png └── garbage_example_df.png ├── pypme ├── __init__.py ├── mod_tessa_pme.py └── pme.py ├── pyproject.toml └── tests ├── __init__.py ├── test_pypme.py └── test_tessa_pypme.py /.gitignore: -------------------------------------------------------------------------------- 1 | poetry.lock 2 | *.pyc 3 | .hypothesis/ 4 | dist/ 5 | settings.json 6 | .venv/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 ymyke 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pypme – Python package for PME (Public Market Equivalent) calculation 2 | 3 | Based on the [Modified PME 4 | method](https://en.wikipedia.org/wiki/Public_Market_Equivalent#Modified_PME). 5 | 6 | ## Example 7 | 8 | ```python 9 | from pypme import verbose_xpme 10 | from datetime import date 11 | 12 | pmeirr, assetirr, df = verbose_xpme( 13 | dates=[date(2015, 1, 1), date(2015, 6, 12), date(2016, 2, 15)], 14 | cashflows=[-10000, 7500], 15 | prices=[100, 120, 100], 16 | pme_prices=[100, 150, 100], 17 | ) 18 | ``` 19 | 20 | Will return `0.5525698793027238` and `0.19495150355969598` for the IRRs and produce this 21 | dataframe: 22 | 23 | ![Example dataframe](https://raw.githubusercontent.com/ymyke/pypme/main/images/example_df.png) 24 | 25 | Notes: 26 | - The `cashflows` are interpreted from a transaction account that is used to buy from an 27 | asset at price `prices`. 28 | - The corresponding prices for the PME are `pme_prices`. 29 | - The `cashflows` is extended with one element representing the remaining value, that's 30 | why all the other lists (`dates`, `prices`, `pme_prices`) need to be exactly 1 element 31 | longer than `cashflows`. 32 | 33 | ## Variants 34 | 35 | - `xpme`: Calculate PME for unevenly spaced / scheduled cashflows and return the PME IRR 36 | only. In this case, the IRR is always annual. 37 | - `verbose_xpme`: Calculate PME for unevenly spaced / scheduled cashflows and return 38 | vebose information. 39 | - `pme`: Calculate PME for evenly spaced cashflows and return the PME IRR only. In this 40 | case, the IRR is for the underlying period. 41 | - `verbose_pme`: Calculate PME for evenly spaced cashflows and return vebose 42 | information. 43 | - `tessa_xpme` and `tessa_verbose_xpme`: Use live price information via the tessa 44 | library. See below. 45 | 46 | ## tessa examples – using tessa to retrieve PME prices online 47 | 48 | Use `tessa_xpme` and `tessa_verbose_xpme` to get live prices via the [tessa 49 | library](https://github.com/ymyke/tessa) and use those prices as the PME. Like so: 50 | 51 | ```python 52 | from datetime import datetime, timezone 53 | from pypme import tessa_xpme 54 | 55 | common_args = { 56 | "dates": [ 57 | datetime(2012, 1, 1, tzinfo=timezone.utc), 58 | datetime(2013, 1, 1, tzinfo=timezone.utc) 59 | ], 60 | "cashflows": [-100], 61 | "prices": [1, 1], 62 | } 63 | print(tessa_xpme(pme_ticker="LIT", **common_args)) # source will default to "yahoo" 64 | print(tessa_xpme(pme_ticker="bitcoin", pme_source="coingecko", **common_args)) 65 | print(tessa_xpme(pme_ticker="SREN.SW", pme_source="yahoo", **common_args)) 66 | ``` 67 | 68 | Note that the dates need to be timezone-aware for these functions. 69 | 70 | 71 | ## Garbage in, garbage out 72 | 73 | Note that the package will only perform essential sanity checks and otherwise just works 74 | with what it gets, also with nonsensical data. E.g.: 75 | 76 | ```python 77 | from pypme import verbose_pme 78 | 79 | pmeirr, assetirr, df = verbose_pme( 80 | cashflows=[-10, 500], prices=[1, 1, 1], pme_prices=[1, 1, 1] 81 | ) 82 | ``` 83 | 84 | Results in this df and IRRs of 0: 85 | 86 | ![Garbage example df](https://raw.githubusercontent.com/ymyke/pypme/main/images/garbage_example_df.png) 87 | 88 | 89 | ## Other noteworthy libraries 90 | 91 | - [tessa](https://github.com/ymyke/tessa): Find financial assets and get their price history without worrying about different APIs or rate limiting. 92 | - [strela](https://github.com/ymyke/strela): A python package for financial alerts. 93 | 94 | 95 | ## References 96 | 97 | - [Google Sheet w/ reference calculation](https://docs.google.com/spreadsheets/d/1LMSBU19oWx8jw1nGoChfimY5asUA4q6Vzh7jRZ_7_HE/edit#gid=0) 98 | - [Modified PME on Wikipedia](https://en.wikipedia.org/wiki/Public_Market_Equivalent#Modified_PME) 99 | -------------------------------------------------------------------------------- /images/example_df.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ymyke/pypme/8053c0d0190e5d74f9450be8f9e5a4851f6d9736/images/example_df.png -------------------------------------------------------------------------------- /images/garbage_example_df.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ymyke/pypme/8053c0d0190e5d74f9450be8f9e5a4851f6d9736/images/garbage_example_df.png -------------------------------------------------------------------------------- /pypme/__init__.py: -------------------------------------------------------------------------------- 1 | from .pme import verbose_pme, pme, verbose_xpme, xpme 2 | from .mod_tessa_pme import ( 3 | tessa_verbose_xpme, 4 | tessa_xpme, 5 | pick_prices_from_dataframe, 6 | ) 7 | -------------------------------------------------------------------------------- /pypme/mod_tessa_pme.py: -------------------------------------------------------------------------------- 1 | """Calculate PME and get the prices via tessa library (https://github.com/ymyke/tessa). 2 | 3 | Args: 4 | 5 | - `pme_ticker`: The ticker symbol/name. 6 | - `pme_source`: The source to look up the ticker from, e.g., "yahoo" or "coingecko". 7 | 8 | For both arguments, refer to the tessa library for details. 9 | 10 | Refer to the `pme` module to understand other arguments and what the functions return. 11 | """ 12 | 13 | from typing import List, Tuple 14 | from datetime import date 15 | import pandas as pd 16 | import tessa 17 | from .pme import verbose_xpme 18 | 19 | 20 | def pick_prices_from_dataframe( 21 | dates: List[date], pricedf: pd.DataFrame, which_column: str 22 | ) -> List[float]: 23 | """Return the prices from `pricedf` that are nearest to dates `dates`. Use 24 | `which_column` to pick the dataframe column. 25 | """ 26 | return list( 27 | pricedf.iloc[pricedf.index.get_indexer([x], method="nearest")[0]][which_column] 28 | for x in dates 29 | ) 30 | 31 | 32 | def tessa_verbose_xpme( 33 | dates: List[date], 34 | cashflows: List[float], 35 | prices: List[float], 36 | pme_ticker: str, 37 | pme_source: tessa.SourceType = "yahoo", 38 | ) -> Tuple[float, float, pd.DataFrame]: 39 | """Calculate PME return vebose information, retrieving PME price information via 40 | tessa library in real time. 41 | """ 42 | pmedf = tessa.price_history(pme_ticker, pme_source).df 43 | return verbose_xpme( 44 | dates, cashflows, prices, pick_prices_from_dataframe(dates, pmedf, "close") 45 | ) 46 | 47 | 48 | def tessa_xpme( 49 | dates: List[date], 50 | cashflows: List[float], 51 | prices: List[float], 52 | pme_ticker: str, 53 | pme_source: tessa.SourceType = "yahoo", 54 | ) -> float: 55 | """Calculate PME and return the PME IRR only, retrieving PME price information via 56 | tessa library in real time. 57 | """ 58 | return tessa_verbose_xpme(dates, cashflows, prices, pme_ticker, pme_source)[0] 59 | -------------------------------------------------------------------------------- /pypme/pme.py: -------------------------------------------------------------------------------- 1 | """Calculate PME (Public Market Equivalent) for both evenly and unevenly spaced 2 | cashflows. 3 | 4 | Calculation according to 5 | https://en.wikipedia.org/wiki/Public_Market_Equivalent#Modified_PME 6 | 7 | Args: 8 | - `dates`: The points in time. (Only for `xpme` variants.) 9 | - `cashflows`: The cashflows from a transaction account perspective. 10 | - `prices`: Asset's prices at each interval / point in time. 11 | - `pme_prices`: PME's prices at each interval / point in time. 12 | 13 | Note: 14 | - Both `prices` and `pme_prices` need an additional item at the end for the last 15 | interval / point in time, for which the PME is calculated. 16 | - `cashflows` has one fewer entry than the other lists because the last cashflow is 17 | implicitly assumed to be the current NAV at that time. 18 | 19 | Verbose versions return a tuple with: 20 | - PME IRR 21 | - Asset IRR 22 | - Dataframe containing all the cashflows, prices, and values used to derive the PME 23 | """ 24 | 25 | from typing import List, Tuple 26 | from datetime import date 27 | import pandas as pd 28 | import numpy_financial as npf 29 | from xirr.math import listsXirr 30 | 31 | 32 | def verbose_pme( 33 | cashflows: List[float], 34 | prices: List[float], 35 | pme_prices: List[float], 36 | ) -> Tuple[float, float, pd.DataFrame]: 37 | """Calculate PME for evenly spaced cashflows and return vebose information.""" 38 | if len(cashflows) == 0: 39 | raise ValueError("Must have at least one cashflow") 40 | if not cashflows[0] < 0: 41 | raise ValueError( 42 | "The first cashflow must be negative, i.e., a buy of some of the asset" 43 | ) 44 | if not all(x > 0 for x in prices + pme_prices): 45 | raise ValueError("All prices must be > 0") 46 | if len(prices) != len(pme_prices) or len(cashflows) != len(prices) - 1: 47 | raise ValueError("Inconsistent input data") 48 | 49 | current_asset_pre = 0 # The current NAV of the asset 50 | current_pme_pre = 0 # The current NAV of the PME 51 | df_rows = [] # To build the dataframe 52 | for cf, asset_price, asset_price_next, pme_price, pme_price_next in zip( 53 | cashflows, prices[:-1], prices[1:], pme_prices[:-1], pme_prices[1:] 54 | ): 55 | if cf < 0: 56 | # Simply buy from the asset and the PME the cashflow amount: 57 | asset_cf = pme_cf = -cf 58 | else: 59 | # Calculate the cashflow's ratio of the asset's NAV at this point in time 60 | # and sell that ratio of the PME: 61 | asset_cf = -cf 62 | ratio = cf / current_asset_pre 63 | pme_cf = -current_pme_pre * ratio 64 | 65 | df_rows.append( 66 | [ 67 | cf, 68 | asset_price, 69 | current_asset_pre, 70 | asset_cf, 71 | current_asset_pre + asset_cf, 72 | pme_price, 73 | current_pme_pre, 74 | pme_cf, 75 | current_pme_pre + pme_cf, 76 | ] 77 | ) 78 | 79 | # Calculate next: 80 | current_asset_pre = ( 81 | (current_asset_pre + asset_cf) * asset_price_next / asset_price 82 | ) 83 | current_pme_pre = (current_pme_pre + pme_cf) * pme_price_next / pme_price 84 | 85 | df_rows.append( 86 | [ 87 | current_asset_pre, 88 | asset_price_next, 89 | current_asset_pre, 90 | -current_asset_pre, 91 | 0, 92 | pme_price_next, 93 | current_pme_pre, 94 | -current_pme_pre, 95 | 0, 96 | ] 97 | ) 98 | df = pd.DataFrame( 99 | df_rows, 100 | columns=pd.MultiIndex.from_arrays( 101 | [ 102 | ["Account"] + ["Asset"] * 4 + ["PME"] * 4, 103 | ["CF"] + ["Price", "NAVpre", "CF", "NAVpost"] * 2, 104 | ] 105 | ), 106 | ) 107 | return (npf.irr(df["PME", "CF"]), npf.irr(df["Asset", "CF"]), df) 108 | 109 | 110 | def pme( 111 | cashflows: List[float], 112 | prices: List[float], 113 | pme_prices: List[float], 114 | ) -> float: 115 | """Calculate PME for evenly spaced cashflows and return the PME IRR only.""" 116 | return verbose_pme(cashflows, prices, pme_prices)[0] 117 | 118 | 119 | def verbose_xpme( 120 | dates: List[date], 121 | cashflows: List[float], 122 | prices: List[float], 123 | pme_prices: List[float], 124 | ) -> Tuple[float, float, pd.DataFrame]: 125 | """Calculate PME for unevenly spaced / scheduled cashflows and return vebose 126 | information. 127 | 128 | Requires the points in time as `dates` as an input parameter in addition to the ones 129 | required by `pme()`. 130 | """ 131 | if dates != sorted(dates): 132 | raise ValueError("Dates must be in order") 133 | if len(dates) != len(prices): 134 | raise ValueError("Inconsistent input data") 135 | df = verbose_pme(cashflows, prices, pme_prices)[2] 136 | df["Dates"] = dates 137 | df.set_index("Dates", inplace=True) 138 | return listsXirr(dates, df["PME", "CF"]), listsXirr(dates, df["Asset", "CF"]), df 139 | 140 | 141 | def xpme( 142 | dates: List[date], 143 | cashflows: List[float], 144 | prices: List[float], 145 | pme_prices: List[float], 146 | ) -> float: 147 | """Calculate PME for unevenly spaced / scheduled cashflows and return the PME IRR 148 | only. 149 | """ 150 | return verbose_xpme(dates, cashflows, prices, pme_prices)[0] 151 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "pypme" 3 | version = "0.6.2" 4 | description = "Python package for PME (Public Market Equivalent) calculation" 5 | authors = ["ymyke"] 6 | license = "MIT" 7 | repository = "https://github.com/ymyke/pypme" 8 | homepage = "https://github.com/ymyke/pypme" 9 | readme = "README.md" 10 | keywords = ["python", "finance", "investing", "financial-analysis", "pme", "investment-analysis"] 11 | 12 | [tool.poetry.dependencies] 13 | python = ">=3.9" 14 | xirr = "^0" 15 | numpy-financial = "^1" 16 | pandas = "^2.2" 17 | tessa = "^0.9" 18 | 19 | [tool.poetry.dev-dependencies] 20 | pytest = "^8" 21 | pytest-mock = "^3.7" 22 | black = "^24" 23 | hypothesis = "^6" 24 | 25 | [build-system] 26 | requires = ["poetry-core>=1.0.0"] 27 | build-backend = "poetry.core.masonry.api" 28 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ymyke/pypme/8053c0d0190e5d74f9450be8f9e5a4851f6d9736/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_pypme.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from datetime import date 3 | from hypothesis import given, strategies as st, settings, Verbosity 4 | import pandas as pd 5 | from xirr.math import xnpv 6 | from pypme import verbose_pme, pme, verbose_xpme, xpme 7 | 8 | 9 | @pytest.mark.parametrize( 10 | "cashflows, prices, pme_prices, target_pme_irr, target_asset_irr", 11 | [ 12 | ( 13 | [-100, -50, 60, 100], 14 | [1.00000000, 1.15000000, 1.28939394, 1.18624242, 1.58165657], 15 | [100, 105, 115, 100, 120], 16 | 2.02, 17 | 7.77, 18 | ), 19 | ([-10, 5], [1, 2, 1], [1, 1, 0.5], -25, 15.14), 20 | ([-10, 1], [1, 1, 1], [1, 1, 1], 0, 0), 21 | ], 22 | ) 23 | def test_verbose_pme(cashflows, prices, pme_prices, target_pme_irr, target_asset_irr): 24 | """`verbose_pme` works with all kinds of input parameters.""" 25 | pme_irr, asset_irr, df = verbose_pme(cashflows, prices, pme_prices) 26 | assert round(pme_irr * 100.0, 2) == round(target_pme_irr, 2) 27 | assert round(asset_irr * 100.0, 2) == round(target_asset_irr, 2) 28 | assert isinstance(df, pd.DataFrame) 29 | 30 | 31 | def test_pme(): 32 | """`pme` properly passes args and returns to and back from `verbose_pme`.""" 33 | assert pme([-10, 5], [1, 2, 1], [1, 1, 0.5]) == -0.25 34 | 35 | 36 | @pytest.mark.parametrize( 37 | "dates, cashflows, prices, pme_prices, target_pme_irr, target_asset_irr", 38 | [ 39 | ( 40 | [date(2019, 12, 31), date(2020, 3, 12)], 41 | [-80005.8], 42 | [80005.8, 65209.6], 43 | [1, 1], 44 | 0, 45 | -64.54, 46 | ), 47 | ( 48 | [date(2015, 1, 1), date(2015, 6, 12), date(2016, 2, 15)], 49 | [-10000, 7500], 50 | [100, 120, 100], 51 | [100, 150, 100], 52 | 55.26, 53 | 19.50, 54 | ), 55 | ([date(1, 1, 1), date(1, 1, 2)], [-1], [1, 1], [1, 1], 0, 0), 56 | ([date(1, 1, 1), date(2, 1, 1)], [-1], [1, 1.1], [1, 0.9], -10, 10), 57 | ([date(1, 1, 1), date(3, 1, 1)], [-1], [1, 1.1], [1, 0.9], -5.13, 4.88), 58 | ([date(1, 1, 1), date(11, 1, 1)], [-1], [1, 1.1], [1, 0.9], -1.05, 0.96), 59 | ], 60 | ) 61 | def test_verbose_xpme( 62 | dates, cashflows, prices, pme_prices, target_pme_irr, target_asset_irr 63 | ): 64 | """`verbose_xpme` works with all kinds of input parameters.""" 65 | pme_irr, asset_irr, df = verbose_xpme(dates, cashflows, prices, pme_prices) 66 | assert round(pme_irr * 100.0, 2) == round(target_pme_irr, 2) 67 | assert round(asset_irr * 100.0, 2) == round(target_asset_irr, 2) 68 | assert isinstance(df, pd.DataFrame) 69 | 70 | 71 | def test_xpme(): 72 | """`xpme` properly passes args and returns to and back from `verbose_xpme`.""" 73 | assert xpme([date(1, 1, 1), date(1, 1, 2)], [-1], [1, 1], [1, 1]) == 0 74 | 75 | 76 | @pytest.mark.parametrize( 77 | "list1, list2, list3, exc_pattern", 78 | [ 79 | ([], [], [], "Must have at least one cashflow"), 80 | ([1], [], [], "The first cashflow must be negative"), 81 | ([-1], [0], [], "All prices must be > 0"), 82 | ([-1], [], [], "Inconsistent input data"), 83 | ], 84 | ) 85 | def test_for_valueerrors(list1, list2, list3, exc_pattern): 86 | with pytest.raises(ValueError) as exc: 87 | pme(list1, list2, list3) 88 | assert exc_pattern in str(exc) 89 | 90 | 91 | def test_for_non_sorted_dates(): 92 | with pytest.raises(ValueError) as exc: 93 | xpme([date(2000, 1, 1), date(1900, 1, 1)], [], [], []) 94 | assert "Dates must be in order" in str(exc) 95 | 96 | 97 | @st.composite 98 | def same_len_lists(draw): 99 | n = draw(st.integers(min_value=2, max_value=100)) 100 | floatlist = st.lists(st.floats(), min_size=n, max_size=n) 101 | datelist = st.lists(st.dates(), min_size=n, max_size=n) 102 | return (sorted(draw(datelist)), draw(floatlist), draw(floatlist), draw(floatlist)) 103 | 104 | 105 | @given(same_len_lists()) 106 | # @settings(verbosity=Verbosity.verbose) 107 | def test_xpme_hypothesis_driven(lists): 108 | try: 109 | pme_irr, asset_irr, df = verbose_xpme( 110 | lists[0], lists[1][:-1], lists[2], lists[3] 111 | ) 112 | except ValueError as exc: 113 | assert "The first cashflow" in str(exc) or "All prices" in str(exc) 114 | except OverflowError as exc: 115 | assert "Result too large" in str(exc) 116 | else: 117 | assert xnpv(df["PME", "CF"], pme_irr) == 0 118 | assert xnpv(df["Asset", "CF"], asset_irr) == 0 119 | -------------------------------------------------------------------------------- /tests/test_tessa_pypme.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from datetime import datetime, timezone, date 3 | import pandas as pd 4 | from tessa.price import PriceHistory 5 | from pypme.mod_tessa_pme import tessa_xpme, tessa_verbose_xpme 6 | 7 | 8 | @pytest.mark.parametrize( 9 | "dates, cashflows, prices, pme_timestamps, pme_prices, target_pme_irr, target_asset_irr", 10 | [ 11 | ( 12 | [date(2012, 1, 1), date(2013, 1, 1)], 13 | [-100], 14 | [1, 1], 15 | ["2012-01-01"], 16 | [20], 17 | 0, 18 | # B/c the function will search for the nearest date which is always the same 19 | # one b/c there is only one and therefore produce a PME IRR of 0 20 | 0, 21 | ), 22 | ( 23 | [date(2012, 1, 1), date(2013, 1, 1)], 24 | [-100], 25 | [1, 1], 26 | ["2012-01-01", "2012-01-02"], 27 | [20, 40], 28 | 99.62, 29 | # In this case, the "nearest" option in `tessa_verbose_pme`'s call to 30 | # `get_indexer` finds the entry at 2012-01-02. Even though it's far away 31 | # from 2013-01-01, it's still the closest. 32 | 0, 33 | ), 34 | ], 35 | ) 36 | def test_tessa_xpme( 37 | mocker, 38 | dates, 39 | cashflows, 40 | prices, 41 | pme_timestamps, 42 | pme_prices, 43 | target_pme_irr, 44 | target_asset_irr, 45 | ): 46 | """Test both the verbose and non-verbose variant in one go to keep things simple. 47 | 48 | Note that this test does _not_ hit the network since the relevant function gets 49 | mocked. 50 | """ 51 | mocker.patch( 52 | "tessa.price_history", 53 | return_value=PriceHistory( 54 | pd.DataFrame( 55 | { 56 | "close": { 57 | pd.Timestamp(x): y for x, y in zip(pme_timestamps, pme_prices) 58 | } 59 | } 60 | ), 61 | "USD", 62 | ), 63 | ) 64 | pme_irr, asset_irr, df = tessa_verbose_xpme( 65 | dates=dates, 66 | cashflows=cashflows, 67 | prices=prices, 68 | pme_ticker="dummy", 69 | ) 70 | assert round(pme_irr * 100.0, 2) == round(target_pme_irr, 2) 71 | assert round(asset_irr * 100.0, 2) == round(target_asset_irr, 2) 72 | assert isinstance(df, pd.DataFrame) 73 | 74 | pme_irr = tessa_xpme( 75 | dates=dates, 76 | cashflows=cashflows, 77 | prices=prices, 78 | pme_ticker="dummy", 79 | ) 80 | assert round(pme_irr * 100.0, 2) == round(target_pme_irr, 2) 81 | 82 | 83 | def test_tessa_xpme_networked(): 84 | """Test `tessa_xpme` with an actual network/API call.""" 85 | pme_irr, asset_irr, df = tessa_verbose_xpme( 86 | dates=[ 87 | datetime(2012, 1, 1, tzinfo=timezone.utc), 88 | datetime(2013, 1, 1, tzinfo=timezone.utc), 89 | ], 90 | cashflows=[-100], 91 | prices=[1, 1.1], 92 | pme_ticker="MSFT", 93 | ) 94 | assert round(pme_irr * 100.0, 2) == 5.78 95 | assert round(asset_irr * 100.0, 2) == 9.97 96 | assert isinstance(df, pd.DataFrame) 97 | assert df.shape == (2, 9) 98 | --------------------------------------------------------------------------------