Repository: ssantoshp/trafalgar Branch: main Commit: 0f965f95c147 Files: 20 Total size: 179.2 KB Directory structure: gitextract_elbbg46b/ ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ └── feature_request.md │ └── workflows/ │ └── python-publish.yml ├── EigenLedger/ │ ├── __init__.py │ ├── main.py │ ├── modules/ │ │ └── empyrical/ │ │ ├── .gitattributes │ │ ├── .gitignore │ │ ├── .travis.yml │ │ ├── __init__.py │ │ ├── _version.py │ │ ├── deprecate.py │ │ ├── perf_attrib.py │ │ ├── periods.py │ │ ├── stats.py │ │ └── utils.py │ └── run.py ├── LICENSE ├── README.md ├── README_CN.md └── pyproject.toml ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Create a report to help us improve title: '' labels: '' assignees: '' --- **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' 4. See error **Expected behavior** A clear and concise description of what you expected to happen. **Screenshots** If applicable, add screenshots to help explain your problem. **Desktop (please complete the following information):** - OS: [e.g. iOS] - Browser [e.g. chrome, safari] - Version [e.g. 22] **Additional context** Add any other context about the problem here. ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature request about: Suggest an idea for this project title: '' labels: '' assignees: '' --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when X happens [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context or screenshots about the feature request here. ================================================ FILE: .github/workflows/python-publish.yml ================================================ name: Publish to PyPI.org on: release: types: [published] jobs: pypi: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v3 with: fetch-depth: 0 - run: python3 -m pip install --upgrade build && python3 -m build - name: Publish package uses: pypa/gh-action-pypi-publish@release/v1 with: password: ${{ secrets.PYPI_API_TOKEN }} ================================================ FILE: EigenLedger/__init__.py ================================================ from .main import * from .modules.empyrical import * ================================================ FILE: EigenLedger/main.py ================================================ import numpy as np import pandas as pd import datetime as dt import quantstats as qs from IPython.display import display import matplotlib.pyplot as plt import copy import yfinance as yf from fpdf import FPDF import warnings import logging from modules.empyrical import ( cagr, cum_returns, stability_of_timeseries, max_drawdown, sortino_ratio, alpha_beta, tail_ratio, ) from pypfopt import ( EfficientFrontier, risk_models, expected_returns, HRPOpt, objective_functions, # black_litterman, # BlackLittermanModel, ) warnings.filterwarnings("ignore") logging.getLogger('matplotlib.font_manager').disabled = True logging.getLogger('matplotlib.legend').disabled = True TODAY = dt.date.today() BENCHMARK = ["SPY"] DAYS_IN_YEAR = 365 rebalance_periods = { "daily": DAYS_IN_YEAR / 365, "weekly": DAYS_IN_YEAR / 52, "monthly": DAYS_IN_YEAR / 12, "month": DAYS_IN_YEAR / 12, "m": DAYS_IN_YEAR / 12, "quarterly": DAYS_IN_YEAR / 4, "quarter": DAYS_IN_YEAR / 4, "q": DAYS_IN_YEAR / 4, "6m": DAYS_IN_YEAR / 2, "2q": DAYS_IN_YEAR / 2, "1y": DAYS_IN_YEAR, "year": DAYS_IN_YEAR, "y": DAYS_IN_YEAR, "2y": DAYS_IN_YEAR * 2, } #defining colors for the allocation pie CS = [ "#ff9999", "#66b3ff", "#99ff99", "#ffcc99", "#f6c9ff", "#a6fff6", "#fffeb8", "#ffe1d4", "#cccdff", "#fad6ff", ] class Engine: def __init__( self, start_date, portfolio, weights=None, rebalance=None, benchmark=None, end_date=TODAY, optimizer=None, max_vol=0.15, diversification=1, expected_returns=None, risk_model=None, # confidences=None, # view=None, min_weights=None, max_weights=None, risk_manager=None, data=pd.DataFrame(), benchmark_data=pd.DataFrame(), ): if benchmark is None: benchmark = BENCHMARK self.start_date = start_date self.end_date = end_date self.portfolio = portfolio self.weights = weights self.benchmark = benchmark self.optimizer = optimizer self.rebalance = rebalance self.max_vol = max_vol self.diversification = diversification self.expected_returns = expected_returns if expected_returns is not None: 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." self.risk_model = risk_model if risk_model is not None: 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." self.max_weights = max_weights self.min_weights = min_weights self.risk_manager = risk_manager self.data = data self.benchmark_data = benchmark_data # To hold benchmark data optimizers = { "EF": efficient_frontier, "MEANVAR": mean_var, "HRP": hrp, "MINVAR": min_var, } if self.optimizer is None and self.weights is None: self.weights = [1.0 / len(self.portfolio)] * len(self.portfolio) elif self.optimizer in optimizers.keys(): if self.optimizer == "MEANVAR": self.weights = optimizers.get(self.optimizer)(self, vol_max=max_vol, perf=False) else: self.weights = optimizers.get(self.optimizer)(self, perf=False) if self.rebalance is not None: self.rebalance = make_rebalance( self.start_date, self.end_date, self.optimizer, self.portfolio, self.rebalance, self.weights, self.max_vol, self.diversification, self.min_weights, self.max_weights, self.expected_returns, self.risk_model ) def fetch_benchmark_data(self): """Fetch benchmark data using Yahoo Finance or validate custom benchmark data.""" if isinstance(self.benchmark, str): self.benchmark = [self.benchmark] # Convert to list for consistency # If `self.data` has benchmark columns, use them if not self.data.empty and all(b in self.data.columns for b in self.benchmark): self.benchmark_data = self.data[self.benchmark] else: # Fetch data from Yahoo Finance try: self.benchmark_data = yf.download(self.benchmark, start=self.start_date)["Adj Close"] except Exception as e: print(f"Error fetching benchmark data: {e}") self.benchmark_data = pd.DataFrame() class PortfolioAnalysisResult: pass def get_returns(stocks, wts, start_date, end_date=TODAY): logging.info("Entering get_returns function with stocks: %s", stocks) # Ensure `stocks` is a list if isinstance(stocks, str): stocks = [stocks] # Validate weights length if len(wts) != len(stocks): logging.error("Length mismatch: stocks=%s, weights=%s", len(stocks), len(wts)) raise ValueError("Weights and stocks lists must have the same length.") # Initialize lists for tracking available and missing stocks available_stocks = [] missing_stocks = [] # Check each stock ticker and attempt to download its data for stock in stocks: try: asset = yf.download(stock, start=start_date, end=end_date, progress=False)["Adj Close"] if not asset.empty: available_stocks.append(stock) else: missing_stocks.append(stock) except Exception as e: logging.error("Error downloading data for %s: %s", stock, e) missing_stocks.append(stock) # Raise an error if any stocks are missing if missing_stocks: logging.error("Missing stock(s): %s", missing_stocks) raise ValueError(f"Some stock(s) are missing in the downloaded data: {missing_stocks}") # Proceed with downloading and processing data for available stocks logging.info("Available stocks for processing: %s", available_stocks) if len(available_stocks) > 1: assets = yf.download(available_stocks, start=start_date, end=end_date, progress=False)["Adj Close"] assets = assets.filter(available_stocks) # Calculate initial allocation initial_alloc = wts / assets.iloc[0] logging.debug("Initial allocation calculated: %s", initial_alloc) if initial_alloc.isna().any(): logging.error("Some stock is not available at initial state for: %s", available_stocks) raise ValueError("Some stock is not available at initial state!") # Calculate portfolio value and returns portfolio_value = (assets * initial_alloc).sum(axis=1) logging.debug("Portfolio value: %s", portfolio_value) returns = portfolio_value.pct_change().dropna() logging.info("Returning returns with multiple stocks.") return returns elif len(available_stocks) == 1: df = yf.download(available_stocks[0], start=start_date, end=end_date, progress=False)["Adj Close"] df = pd.DataFrame(df) returns = df.pct_change().dropna() logging.info("Returning returns for single stock.") return returns else: logging.error("No valid stocks found for download.") raise ValueError("No valid stocks were found in the provided list.") def get_returns_from_data(data, wts, stocks): assets = data.filter(stocks) initial_alloc = wts/assets.iloc[0] if initial_alloc.isna().any(): raise ValueError("Some stock is not available at initial state!") portfolio_value = (assets * initial_alloc).sum(axis=1) returns = portfolio_value.pct_change()[1:] return returns def get_returns_from_benchmark_data(data, wts, stocks): if wts is None: wts = [1] assets = data.filter(stocks) initial_alloc = wts/assets.iloc[0] if initial_alloc.isna().any(): raise ValueError("Some stock is not available at initial state!") benchmark_value = (assets * initial_alloc).sum(axis=1) returns = benchmark_value.pct_change()[1:] return returns def calculate_information_ratio(returns, benchmark_returns, days=252) -> float: return_difference = returns - benchmark_returns volatility = return_difference.std() * np.sqrt(days) information_ratio_result = return_difference.mean() / volatility return information_ratio_result def graph_allocation(my_portfolio): fig1, ax1 = plt.subplots() ax1.pie( my_portfolio.weights, labels=my_portfolio.portfolio, autopct="%1.1f%%", shadow=False, ) ax1.axis("equal") # Equal aspect ratio ensures that pie is drawn as a circle. plt.title("Portfolio's allocation") plt.show() def portfolio_analysis(my_portfolio, rf=0.0, sigma_value=1, confidence_value=0.95, report=False, filename="report.pdf"): # Fetch benchmark data my_portfolio.fetch_benchmark_data() result = PortfolioAnalysisResult() # Handling rebalance data and getting returns if isinstance(my_portfolio.rebalance, pd.DataFrame): # we want to get the dataframe with the dates and weights rebalance_schedule = my_portfolio.rebalance columns = [] for date in rebalance_schedule.columns: date = date[0:10] columns.append(date) rebalance_schedule.columns = columns # then want to make a list of the dates and start with our first date dates = [my_portfolio.start_date] # then our rebalancing dates into that list dates = dates + rebalance_schedule.columns.to_list() datess = [] for date in dates: date = date[0:10] datess.append(date) dates = datess # this will hold returns returns = pd.Series() # then we want to be able to call the dates like tuples for i in range(len(dates) - 1): # get our weights weights = rebalance_schedule[str(dates[i + 1])] # then we want to get the returns add_returns = get_returns( my_portfolio.portfolio, weights, start_date=dates[i], end_date=dates[i + 1], ) # then append those returns returns = returns._append(add_returns) else: if not my_portfolio.data.empty: returns = get_returns_from_data(my_portfolio.data, my_portfolio.weights, my_portfolio.portfolio) else: returns = get_returns( my_portfolio.portfolio, my_portfolio.weights, start_date=my_portfolio.start_date, end_date=my_portfolio.end_date, ) creturns = (returns + 1).cumprod() # risk manager try: if list(my_portfolio.risk_manager.keys())[0] == "Stop Loss": values = [] for r in creturns: if r <= 1 + my_portfolio.risk_manager["Stop Loss"]: values.append(r) else: pass try: date = creturns[creturns == values[0]].index[0] date = str(date.to_pydatetime()) my_portfolio.end_date = date[0:10] returns = returns[: my_portfolio.end_date] except Exception as e: pass if list(my_portfolio.risk_manager.keys())[0] == "Take Profit": values = [] for r in creturns: if r >= 1 + my_portfolio.risk_manager["Take Profit"]: values.append(r) else: pass try: date = creturns[creturns == values[0]].index[0] date = str(date.to_pydatetime()) my_portfolio.end_date = date[0:10] returns = returns[: my_portfolio.end_date] except Exception as e: pass if list(my_portfolio.risk_manager.keys())[0] == "Max Drawdown": drawdown = qs.stats.to_drawdown_series(returns) values = [] for r in drawdown: if r <= my_portfolio.risk_manager["Max Drawdown"]: values.append(r) else: pass try: date = drawdown[drawdown == values[0]].index[0] date = str(date.to_pydatetime()) my_portfolio.end_date = date[0:10] returns = returns[: my_portfolio.end_date] except Exception as e: pass except Exception as e: pass print("Start date: " + str(my_portfolio.start_date)) print("End date: " + str(my_portfolio.end_date)) if not my_portfolio.benchmark_data.empty: print("Portfolio Data (my_portfolio.data):") print(my_portfolio.data.head()) print("Portfolio Columns:", my_portfolio.portfolio) print("Weights:", my_portfolio.weights) print("\nBenchmark Data (my_portfolio.benchmark_data):") print(my_portfolio.benchmark_data.head()) # Raw benchmark data (prices) # Check if benchmark_data is a Series or DataFrame and print accordingly if isinstance(my_portfolio.benchmark_data, pd.Series): print("benchmark Columns:", my_portfolio.benchmark_data.tolist()) elif isinstance(my_portfolio.benchmark_data, pd.DataFrame): print("Benchmark Columns:", my_portfolio.benchmark_data.columns.tolist()) wts = [1] # Get benchmark returns from portfolio data benchmark = get_returns_from_benchmark_data(my_portfolio.data, my_portfolio.weights, my_portfolio.portfolio) print("Benchmark Returns from Portfolio Data:") print(benchmark.head()) # Inspect the first few rows of returns else: print("Benchmark Tickers (my_portfolio.benchmark):", my_portfolio.benchmark) print("Start Date:", my_portfolio.start_date) print("End Date:", my_portfolio.end_date) benchmark = get_returns( my_portfolio.benchmark, wts=[1], start_date=my_portfolio.start_date, end_date=my_portfolio.end_date, ).dropna() print("Benchmark Returns from Yahoo Finance (or fallback):") print(benchmark.head()) # # Fetch benchmark returns # benchmark = get_returns( # my_portfolio.benchmark, # wts=[1], # start_date=my_portfolio.start_date, # end_date=my_portfolio.end_date, # ) # benchmark = benchmark.dropna() CAGR = cagr(returns, period='daily', annualization=None) # CAGR = round(CAGR, 2) # CAGR = CAGR.tolist() CAGR = str(round(CAGR * 100, 2)) + "%" CUM = cum_returns(returns, starting_value=0, out=None) * 100 CUM = CUM.iloc[-1] CUM = CUM.tolist() CUM = str(round(CUM, 2)) + "%" VOL = qs.stats.volatility(returns, annualize=True) VOL = VOL.tolist() VOL = str(round(VOL * 100, 2)) + " %" SR = qs.stats.sharpe(returns, rf=rf) SR = np.round(SR, decimals=2) SR = str(SR) result.SR = SR CR = qs.stats.calmar(returns) CR = CR.tolist() CR = str(round(CR, 2)) result.CR = CR STABILITY = stability_of_timeseries(returns) STABILITY = round(STABILITY, 2) STABILITY = str(STABILITY) MD = max_drawdown(returns, out=None) MD = str(round(MD * 100, 2)) + " %" """OR = omega_ratio(returns, risk_free=0.0, required_return=0.0) OR = round(OR,2) OR = str(OR) print(OR)""" SOR = sortino_ratio(returns, required_return=0, period='daily') SOR = round(SOR, 2) SOR = str(SOR) SK = qs.stats.skew(returns) SK = round(SK, 2) if isinstance(SK, float): SK = [SK] elif isinstance(SK, (list, np.ndarray)): SK = SK.tolist() SK = str(SK) KU = qs.stats.kurtosis(returns) KU = round(KU, 2) if isinstance(KU, float): KU = [KU] elif isinstance(KU, (list, np.ndarray)): KU = KU.tolist() KU = str(KU) TA = tail_ratio(returns) TA = round(TA, 2) TA = str(TA) CSR = qs.stats.common_sense_ratio(returns) CSR = round(CSR, 2) CSR = CSR.tolist() CSR = str(CSR) VAR = qs.stats.value_at_risk( returns, sigma=sigma_value, confidence=confidence_value ) VAR = np.round(VAR, decimals=2) VAR = str(VAR * 100) + " %" returns = returns.tz_localize(None) # Making tz-naive benchmark = benchmark.tz_localize(None) # Making tz-naive alpha, beta = alpha_beta(returns, benchmark, risk_free=rf) AL = round(alpha, 2) BTA = round(beta, 2) def condition(x): return x > 0 win = sum(condition(x) for x in returns) total = len(returns) win_ratio = win / total win_ratio = win_ratio * 100 win_ratio = round(win_ratio, 2) # IR = calculate_information_ratio(returns, benchmark.iloc[:, 0]) if isinstance(benchmark, pd.Series): IR = calculate_information_ratio(returns, benchmark) else: IR = calculate_information_ratio(returns, benchmark.iloc[:, 0]) IR = round(IR, 2) data = { "": [ "Annual return", "Cumulative return", "Annual volatility", "Winning day ratio", "Sharpe ratio", "Calmar ratio", "Information ratio", "Stability", "Max Drawdown", "Sortino ratio", "Skew", "Kurtosis", "Tail Ratio", "Common sense ratio", "Daily value at risk", "Alpha", "Beta", ], "Backtest": [ CAGR, CUM, VOL, f"{win_ratio}%", SR, CR, IR, STABILITY, MD, SOR, SK, KU, TA, CSR, VAR, AL, BTA, ], } # Create DataFrame df = pd.DataFrame(data) df.set_index("", inplace=True) df.style.set_properties( **{"background-color": "white", "color": "black", "border-color": "black"} ) display(df) result.df = data y = [] for x in returns: y.append(x) arr = np.array(y) # arr # returns.index my_color = np.where(arr >= 0, "blue", "grey") ret = plt.figure(figsize=(30, 8)) plt.vlines(x=returns.index, ymin=0, ymax=arr, color=my_color, alpha=0.4) plt.title("Returns") result.returns = returns result.creturns = creturns result.benchmark = benchmark result.CAGR = CAGR result.CUM = CUM result.VOL = VOL result.SR = SR result.win_ratio = win_ratio result.CR = CR result.IR = IR result.STABILITY = STABILITY result.MD = MD result.SOR = SOR result.SK = SK result.KU = KU result.TA = TA result.CSR = CSR result.VAR = VAR result.AL = AL result.BTA = BTA try: result.orderbook = make_rebalance.output except Exception as e: OrderBook = pd.DataFrame( { "Assets": my_portfolio.portfolio, "Allocation": my_portfolio.weights, } ) result.orderbook = OrderBook.T wts = copy.deepcopy(my_portfolio.weights) indices = [i for i, x in enumerate(wts) if x == 0.0] while 0.0 in wts: wts.remove(0.0) for i in sorted(indices, reverse=True): del my_portfolio.portfolio[i] if not returns.empty: if not report: qs.plots.returns(returns, benchmark, cumulative=True) qs.plots.yearly_returns(returns, benchmark), qs.plots.monthly_heatmap(returns, benchmark) qs.plots.drawdown(returns) qs.plots.drawdowns_periods(returns) # qs.plots.rolling_volatility(returns) # qs.plots.rolling_sharpe(returns) qs.plots.rolling_beta(returns, benchmark) graph_opt(my_portfolio.portfolio, wts, pie_size=7, font_size=14) else: qs.plots.returns(returns, benchmark, cumulative=True, savefig="retbench.png") qs.plots.yearly_returns(returns, benchmark, savefig="y_returns.png"), qs.plots.monthly_heatmap(returns, benchmark, savefig="heatmap.png") qs.plots.drawdown(returns, savefig="drawdown.png") qs.plots.drawdowns_periods(returns, savefig="d_periods.png") # qs.plots.rolling_volatility(returns, savefig="rvol.png") qs.plots.rolling_sharpe(returns, savefig="rsharpe.png") qs.plots.rolling_beta(returns, benchmark, savefig="rbeta.png") graph_opt(my_portfolio.portfolio, wts, pie_size=7, font_size=14, save=True) pdf = FPDF() pdf.add_page() pdf.set_font("arial", "B", 14) pdf.image( "https://user-images.githubusercontent.com/61618641/120909011-98f8a180-c670-11eb-8844-2d423ba3fa9c.png", x=None, y=None, w=45, h=5, type="", link="https://github.com/ssantoshp/E", ) pdf.cell(20, 15, f"Report", ln=1) pdf.set_font("arial", size=11) pdf.image("allocation.png", x=135, y=0, w=70, h=70, type="", link="") pdf.cell(20, 7, f"Start date: " + str(my_portfolio.start_date), ln=1) pdf.cell(20, 7, f"End date: " + str(my_portfolio.end_date), ln=1) ret.savefig("ret.png") pdf.cell(20, 7, f"", ln=1) pdf.cell(20, 7, f"Annual return: " + str(CAGR), ln=1) pdf.cell(20, 7, f"Cumulative return: " + str(CUM), ln=1) pdf.cell(20, 7, f"Annual volatility: " + str(VOL), ln=1) pdf.cell(20, 7, f"Winning day ratio: " + str(win_ratio), ln=1) pdf.cell(20, 7, f"Sharpe ratio: " + str(SR), ln=1) pdf.cell(20, 7, f"Calmar ratio: " + str(CR), ln=1) pdf.cell(20, 7, f"Information ratio: " + str(IR), ln=1) pdf.cell(20, 7, f"Stability: " + str(STABILITY), ln=1) pdf.cell(20, 7, f"Max drawdown: " + str(MD), ln=1) pdf.cell(20, 7, f"Sortino ratio: " + str(SOR), ln=1) pdf.cell(20, 7, f"Skew: " + str(SK), ln=1) pdf.cell(20, 7, f"Kurtosis: " + str(KU), ln=1) pdf.cell(20, 7, f"Tail ratio: " + str(TA), ln=1) pdf.cell(20, 7, f"Common sense ratio: " + str(CSR), ln=1) pdf.cell(20, 7, f"Daily value at risk: " + str(VAR), ln=1) pdf.cell(20, 7, f"Alpha: " + str(AL), ln=1) pdf.cell(20, 7, f"Beta: " + str(BTA), ln=1) pdf.image("ret.png", x=-20, y=None, w=250, h=80, type="", link="") pdf.cell(20, 7, f"", ln=1) pdf.image("y_returns.png", x=-20, y=None, w=200, h=100, type="", link="") pdf.cell(20, 7, f"", ln=1) pdf.image("retbench.png", x=None, y=None, w=200, h=100, type="", link="") pdf.cell(20, 7, f"", ln=1) pdf.image("heatmap.png", x=None, y=None, w=200, h=80, type="", link="") pdf.cell(20, 7, f"", ln=1) pdf.image("drawdown.png", x=None, y=None, w=200, h=80, type="", link="") pdf.cell(20, 7, f"", ln=1) pdf.image("d_periods.png", x=None, y=None, w=200, h=80, type="", link="") pdf.cell(20, 7, f"", ln=1) pdf.image("rvol.png", x=None, y=None, w=190, h=80, type="", link="") pdf.cell(20, 7, f"", ln=1) pdf.image("rsharpe.png", x=None, y=None, w=190, h=80, type="", link="") pdf.cell(20, 7, f"", ln=1) pdf.image("rbeta.png", x=None, y=None, w=190, h=80, type="", link="") pdf.output(dest="F", name=filename) print("The PDF was generated successfully!") return result def flatten(subject) -> list: muster = [] for item in subject: if isinstance(item, (list, tuple, set)): muster.extend(flatten(item)) else: muster.append(item) return muster def graph_opt(my_portfolio, my_weights, pie_size, font_size, save=False): fig1, ax1 = plt.subplots() fig1.set_size_inches(pie_size, pie_size) ax1.pie(my_weights, labels=my_portfolio, autopct="%1.1f%%", shadow=False, colors=CS) ax1.axis("equal") # Equal aspect ratio ensures that pie is drawn as a circle. plt.rcParams["font.size"] = font_size if save: plt.savefig("allocation.png") plt.show() def equal_weighting(my_portfolio) -> list: return [1.0 / len(my_portfolio.portfolio)] * len(my_portfolio.portfolio) def efficient_frontier(my_portfolio, perf=True) -> list: # changed to take in desired timeline, the problem is that it would use all historical data ohlc = yf.download( my_portfolio.portfolio, start=my_portfolio.start_date, end=my_portfolio.end_date, progress=False, ) prices = ohlc["Adj Close"].dropna(how="all") df = prices.filter(my_portfolio.portfolio) # 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 df = df.fillna(1) if my_portfolio.expected_returns == None: my_portfolio.expected_returns = 'mean_historical_return' if my_portfolio.risk_model == None: my_portfolio.risk_model = 'sample_cov' mu = expected_returns.return_model(df, method=my_portfolio.expected_returns) S = risk_models.risk_matrix(df, method=my_portfolio.risk_model) # optimize for max sharpe ratio ef = EfficientFrontier(mu, S) ef.add_objective(objective_functions.L2_reg, gamma=my_portfolio.diversification) if my_portfolio.min_weights is not None: ef.add_constraint(lambda x: x >= my_portfolio.min_weights) if my_portfolio.max_weights is not None: ef.add_constraint(lambda x: x <= my_portfolio.max_weights) weights = ef.max_sharpe() cleaned_weights = ef.clean_weights() wts = cleaned_weights.items() result = [] for val in wts: a, b = map(list, zip(*[val])) result.append(b) if perf is True: pred = ef.portfolio_performance(verbose=True) return flatten(result) def hrp(my_portfolio, perf=True) -> list: # changed to take in desired timeline, the problem is that it would use all historical data ohlc = yf.download( my_portfolio.portfolio, start=my_portfolio.start_date, end=my_portfolio.end_date, progress=False, ) prices = ohlc["Adj Close"].dropna(how="all") prices = prices.filter(my_portfolio.portfolio) # 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 prices = prices.fillna(1) rets = expected_returns.returns_from_prices(prices) hrp = HRPOpt(rets) hrp.optimize() weights = hrp.clean_weights() wts = weights.items() result = [] for val in wts: a, b = map(list, zip(*[val])) result.append(b) if perf is True: hrp.portfolio_performance(verbose=True) return flatten(result) def mean_var(my_portfolio, vol_max=0.15, perf=True) -> list: # changed to take in desired timeline, the problem is that it would use all historical data ohlc = yf.download( my_portfolio.portfolio, start=my_portfolio.start_date, end=my_portfolio.end_date, progress=False, ) prices = ohlc["Adj Close"].dropna(how="all") prices = prices.filter(my_portfolio.portfolio) # 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 prices = prices.fillna(1) if my_portfolio.expected_returns == None: my_portfolio.expected_returns = 'capm_return' if my_portfolio.risk_model == None: my_portfolio.risk_model = 'ledoit_wolf' mu = expected_returns.return_model(prices, method=my_portfolio.expected_returns) S = risk_models.risk_matrix(prices, method=my_portfolio.risk_model) ef = EfficientFrontier(mu, S) ef.add_objective(objective_functions.L2_reg, gamma=my_portfolio.diversification) if my_portfolio.min_weights is not None: ef.add_constraint(lambda x: x >= my_portfolio.min_weights) if my_portfolio.max_weights is not None: ef.add_constraint(lambda x: x <= my_portfolio.max_weights) ef.efficient_risk(vol_max) weights = ef.clean_weights() wts = weights.items() result = [] for val in wts: a, b = map(list, zip(*[val])) result.append(b) if perf is True: ef.portfolio_performance(verbose=True) return flatten(result) def min_var(my_portfolio, perf=True) -> list: ohlc = yf.download( my_portfolio.portfolio, start=my_portfolio.start_date, end=my_portfolio.end_date, progress=False, ) prices = ohlc["Adj Close"].dropna(how="all") prices = prices.filter(my_portfolio.portfolio) if my_portfolio.expected_returns == None: my_portfolio.expected_returns = 'capm_return' if my_portfolio.risk_model == None: my_portfolio.risk_model = 'ledoit_wolf' mu = expected_returns.return_model(prices, method=my_portfolio.expected_returns) S = risk_models.risk_matrix(prices, method=my_portfolio.risk_model) ef = EfficientFrontier(mu, S) ef.add_objective(objective_functions.L2_reg, gamma=my_portfolio.diversification) if my_portfolio.min_weights is not None: ef.add_constraint(lambda x: x >= my_portfolio.min_weights) if my_portfolio.max_weights is not None: ef.add_constraint(lambda x: x <= my_portfolio.max_weights) ef.min_volatility() weights = ef.clean_weights() wts = weights.items() result = [] for val in wts: a, b = map(list, zip(*[val])) result.append(b) if perf is True: ef.portfolio_performance(verbose=True) return flatten(result) def optimize_portfolio(my_portfolio, vol_max=25, pie_size=5, font_size=14): if my_portfolio.optimizer == None: raise Exception("You didn't define any optimizer in your portfolio!") returns1 = get_returns( my_portfolio.portfolio, equal_weighting(my_portfolio), start_date=my_portfolio.start_date, end_date=my_portfolio.end_date, ) creturns1 = (returns1 + 1).cumprod() port = copy.deepcopy(my_portfolio.portfolio) wts = [1.0 / len(my_portfolio.portfolio)] * len(my_portfolio.portfolio) optimizers = { "EF": efficient_frontier, "MEANVAR": mean_var, "HRP": hrp, "MINVAR": min_var, } if my_portfolio.optimizer in optimizers.keys(): if my_portfolio.optimizer == "MEANVAR": wts = optimizers.get(my_portfolio.optimizer)(my_portfolio, my_portfolio.max_vol) else: wts = optimizers.get(my_portfolio.optimizer)(my_portfolio) else: opt = my_portfolio.optimizer my_portfolio.weights = opt() print("\n") indices = [i for i, x in enumerate(wts) if x == 0.0] while 0.0 in wts: wts.remove(0.0) for i in sorted(indices, reverse=True): del port[i] graph_opt(port, wts, pie_size, font_size) print("\n") returns2 = get_returns( port, wts, start_date=my_portfolio.start_date, end_date=my_portfolio.end_date ) creturns2 = (returns2 + 1).cumprod() plt.rcParams["font.size"] = 13 plt.figure(figsize=(30, 10)) plt.xlabel("Portfolio vs Benchmark") ax1 = creturns1.plot(color="blue", label="Without optimization") ax2 = creturns2.plot(color="red", label="With optimization") h1, l1 = ax1.get_legend_handles_labels() h2, l2 = ax2.get_legend_handles_labels() plt.legend(l1 + l2, loc=2) plt.show() def check_schedule(rebalance) -> bool: valid_schedule = False if rebalance.lower() in rebalance_periods.keys(): valid_schedule = True return valid_schedule def valid_range(start_date, end_date, rebalance) -> tuple: # make the start date to a datetime start_date = dt.datetime.strptime(start_date, "%Y-%m-%d") # custom dates don't need further chekings if type(rebalance) is list: return start_date, rebalance[-1] # make the end date to a datetime end_date = dt.datetime.strptime(str(end_date), "%Y-%m-%d") # gets the number of days days = (end_date - start_date).days # checking that date range covers rebalance period if rebalance in rebalance_periods.keys() and days <= (int(rebalance_periods[rebalance])): raise KeyError("Date Range does not encompass rebalancing interval") # we will needs these dates later on so we'll return them back return start_date, end_date def get_date_range(start_date, end_date, rebalance) -> list: # this will keep track of the rebalancing dates and we want to start on the first date rebalance_dates = [start_date] input_date = start_date if rebalance in rebalance_periods.keys(): # run for an arbitrarily large number we'll resolve this by breaking when we break the equality for i in range(1000): days = rebalance_periods.get(rebalance, 0.0) if days is None: raise ValueError("Rebalance period cannot be None") # Increment the date based on the selected period input_date = input_date + timedelta(days=days) if input_date <= end_date: # Append the new date if it is earlier or equal to the final date rebalance_dates.append(input_date) else: # Break when the next rebalance date is later than our end date break # then we want to return those dates return rebalance_dates def make_rebalance( start_date, end_date, optimize, portfolio_input, rebalance, allocation, vol_max, div, min, max, expected_returns, risk_model, ) -> pd.DataFrame: sdate = str(start_date)[:10] if rebalance[0] != sdate: # makes sure the start date matches the first element of the list of custom rebalance dates if type(rebalance) is list: raise KeyError("the rebalance dates and start date doesn't match") # makes sure that the value passed through for rebalancing is a valid one valid_schedule = check_schedule(rebalance) if valid_schedule is False: raise KeyError("Not an accepted rebalancing schedule") # this checks to make sure that the date range given works for the rebalancing start_date, end_date = valid_range(start_date, end_date, rebalance) # this function will get us the specific dates if rebalance[0] != sdate: dates = get_date_range(start_date, end_date, rebalance) else: dates = rebalance # we are going to make columns with the end date and the weights columns = ["end_date"] + portfolio_input # then make a dataframe with the index being the tickers output_df = pd.DataFrame(index=portfolio_input) for i in range(len(dates) - 1): try: portfolio = Engine( start_date=dates[0], end_date=dates[i + 1], portfolio=portfolio_input, weights=allocation, optimizer="{}".format(optimize), max_vol=vol_max, diversification=div, min_weights=min, max_weights=max, expected_returns=expected_returns, risk_model=risk_model, ) except TypeError: portfolio = Engine( start_date=dates[0], end_date=dates[i + 1], portfolio=portfolio_input, weights=allocation, optimizer=optimize, max_vol=vol_max, diversification=div, min_weights=min, max_weights=max, expected_returns=expected_returns, risk_model=risk_model, ) output_df["{}".format(dates[i + 1])] = portfolio.weights # we have to run it one more time to get what the optimization is for up to today's date try: portfolio = Engine( start_date=dates[0], portfolio=portfolio_input, weights=allocation, optimizer="{}".format(optimize), max_vol=vol_max, diversification=div, min_weights=min, max_weights=max, expected_returns=expected_returns, risk_model=risk_model, ) except TypeError: portfolio = Engine( start_date=dates[0], portfolio=portfolio_input, weights=allocation, optimizer=optimize, max_vol=vol_max, diversification=div, min_weights=min, max_weights=max, expected_returns=expected_returns, risk_model=risk_model, ) output_df["{}".format(TODAY)] = portfolio.weights make_rebalance.output = output_df print("Rebalance schedule: ") print(output_df) return output_df ================================================ FILE: EigenLedger/modules/empyrical/.gitattributes ================================================ empyrical/_version.py export-subst ================================================ FILE: EigenLedger/modules/empyrical/.gitignore ================================================ # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python env/ build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ *.egg-info/ .installed.cfg *.egg # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *,cover .hypothesis/ # Translations *.mo *.pot # Django stuff: *.log # Sphinx documentation docs/_build/ # PyBuilder target/ #Ipython Notebook .ipynb_checkpoints # JetBrains .idea/ ================================================ FILE: EigenLedger/modules/empyrical/.travis.yml ================================================ language: python sudo: false matrix: include: - python: 2.7 env: PANDAS_VERSION=0.24.2 NUMPY_VERSION=1.12.1 SCIPY_VERSION=1.2.1 LIBGFORTRAN_VERSION=3.0 - python: 2.7 env: PANDAS_VERSION=0.20.1 NUMPY_VERSION=1.12.1 SCIPY_VERSION=0.19.0 LIBGFORTRAN_VERSION=3.0 - python: 3.6 env: PANDAS_VERSION=1.0.4 NUMPY_VERSION=1.18.4 SCIPY_VERSION=1.4.1 LIBGFORTRAN_VERSION=3.0 - python: 3.6 env: PANDAS_VERSION=0.20.1 NUMPY_VERSION=1.12.1 SCIPY_VERSION=0.19.0 LIBGFORTRAN_VERSION=3.0 - python: 3.6 env: PANDAS_VERSION=1.0.4 NUMPY_VERSION=1.18.4 SCIPY_VERSION=1.4.1 LIBGFORTRAN_VERSION=3.0 - python: 3.7 env: PANDAS_VERSION=1.0.4 NUMPY_VERSION=1.18.4 SCIPY_VERSION=1.4.1 LIBGFORTRAN_VERSION=3.0 before_install: # We do this conditionally because it saves us some downloading if the # version is the same. - if [[ "$TRAVIS_PYTHON_VERSION" == "2.7" ]]; then wget https://repo.continuum.io/miniconda/Miniconda-latest-Linux-x86_64.sh -O miniconda.sh; else wget https://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh -O miniconda.sh; fi - bash miniconda.sh -b -p $HOME/miniconda - export PATH="$HOME/miniconda/bin:$PATH" - conda config --set always_yes yes --set changeps1 no - conda update -q conda install: - 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 - source activate testenv - pip install -e .[dev] before_script: - "flake8 ." script: - nosetests - source deactivate notifications: email: false branches: only: - master ================================================ FILE: EigenLedger/modules/empyrical/__init__.py ================================================ # # Copyright 2016 Quantopian, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # flake8: noqa from ._version import get_versions __version__ = get_versions()['version'] del get_versions from .stats import ( aggregate_returns, alpha, alpha_aligned, alpha_beta, alpha_beta_aligned, annual_return, annual_volatility, beta, beta_aligned, cagr, beta_fragility_heuristic, beta_fragility_heuristic_aligned, gpd_risk_estimates, gpd_risk_estimates_aligned, calmar_ratio, capture, conditional_value_at_risk, cum_returns, cum_returns_final, down_alpha_beta, down_capture, downside_risk, excess_sharpe, max_drawdown, omega_ratio, roll_alpha, roll_alpha_aligned, roll_alpha_beta, roll_alpha_beta, roll_alpha_beta_aligned, roll_annual_volatility, roll_beta, roll_beta_aligned, roll_down_capture, roll_max_drawdown, roll_sharpe_ratio, roll_sortino_ratio, roll_up_capture, roll_up_down_capture, sharpe_ratio, simple_returns, sortino_ratio, stability_of_timeseries, tail_ratio, up_alpha_beta, up_capture, up_down_capture, value_at_risk, ) from .periods import ( DAILY, WEEKLY, MONTHLY, QUARTERLY, YEARLY ) from .perf_attrib import ( perf_attrib, compute_exposures, ) ================================================ FILE: EigenLedger/modules/empyrical/_version.py ================================================ # This file helps to compute a version number in source trees obtained from # git-archive tarball (such as those provided by githubs download-from-tag # feature). Distribution tarballs (built by setup.py sdist) and build # directories (produced by setup.py build) will contain a much shorter file # that just contains the computed version number. # This file is released into the public domain. Generated by # versioneer-0.16 (https://github.com/warner/python-versioneer) """Git implementation of _version.py.""" import errno import os import re import subprocess import sys def get_keywords(): """Get the keywords needed to look up the version information.""" # these strings will be replaced by git during git-archive. # setup.py/versioneer.py will grep for the variable names, so they must # each be defined on a line of their own. _version.py will just call # get_keywords(). git_refnames = "$Format:%d$" git_full = "$Format:%H$" keywords = {"refnames": git_refnames, "full": git_full} return keywords class VersioneerConfig: """Container for Versioneer configuration parameters.""" def get_config(): """Create, populate and return the VersioneerConfig() object.""" # these strings are filled in when 'setup.py versioneer' creates # _version.py cfg = VersioneerConfig() cfg.VCS = "git" cfg.style = "pep440" cfg.tag_prefix = "" cfg.parentdir_prefix = "empyrical-" cfg.versionfile_source = "empyrical/_version.py" cfg.verbose = False return cfg class NotThisMethod(Exception): """Exception raised if a method is not valid for the current scenario.""" LONG_VERSION_PY = {} HANDLERS = {} def register_vcs_handler(vcs, method): # decorator """Decorator to mark a method as the handler for a particular VCS.""" def decorate(f): """Store f in HANDLERS[vcs][method].""" if vcs not in HANDLERS: HANDLERS[vcs] = {} HANDLERS[vcs][method] = f return f return decorate def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False): """Call the given command(s).""" assert isinstance(commands, list) p = None for c in commands: try: dispcmd = str([c] + args) # remember shell=False, so use git.cmd on windows, not just git p = subprocess.Popen([c] + args, cwd=cwd, stdout=subprocess.PIPE, stderr=(subprocess.PIPE if hide_stderr else None)) break except EnvironmentError: e = sys.exc_info()[1] if e.errno == errno.ENOENT: continue if verbose: print("unable to run %s" % dispcmd) print(e) return None else: if verbose: print("unable to find command, tried %s" % (commands,)) return None stdout = p.communicate()[0].strip() if sys.version_info[0] >= 3: stdout = stdout.decode() if p.returncode != 0: if verbose: print("unable to run %s (error)" % dispcmd) return None return stdout def versions_from_parentdir(parentdir_prefix, root, verbose): """Try to determine the version from the parent directory name. Source tarballs conventionally unpack into a directory that includes both the project name and a version string. """ dirname = os.path.basename(root) if not dirname.startswith(parentdir_prefix): if verbose: print("guessing rootdir is '%s', but '%s' doesn't start with " "prefix '%s'" % (root, dirname, parentdir_prefix)) raise NotThisMethod("rootdir doesn't start with parentdir_prefix") return {"version": dirname[len(parentdir_prefix):], "full-revisionid": None, "dirty": False, "error": None} @register_vcs_handler("git", "get_keywords") def git_get_keywords(versionfile_abs): """Extract version information from the given file.""" # the code embedded in _version.py can just fetch the value of these # keywords. When used from setup.py, we don't want to import _version.py, # so we do it with a regexp instead. This function is not used from # _version.py. keywords = {} try: f = open(versionfile_abs, "r") for line in f.readlines(): if line.strip().startswith("git_refnames ="): mo = re.search(r'=\s*"(.*)"', line) if mo: keywords["refnames"] = mo.group(1) if line.strip().startswith("git_full ="): mo = re.search(r'=\s*"(.*)"', line) if mo: keywords["full"] = mo.group(1) f.close() except EnvironmentError: pass return keywords @register_vcs_handler("git", "keywords") def git_versions_from_keywords(keywords, tag_prefix, verbose): """Get version information from git keywords.""" if not keywords: raise NotThisMethod("no keywords at all, weird") refnames = keywords["refnames"].strip() if refnames.startswith("$Format"): if verbose: print("keywords are unexpanded, not using") raise NotThisMethod("unexpanded keywords, not a git-archive tarball") refs = set([r.strip() for r in refnames.strip("()").split(",")]) # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of # just "foo-1.0". If we see a "tag: " prefix, prefer those. TAG = "tag: " tags = set([r[len(TAG):] for r in refs if r.startswith(TAG)]) if not tags: # Either we're using git < 1.8.3, or there really are no tags. We use # a heuristic: assume all version tags have a digit. The old git %d # expansion behaves like git log --decorate=short and strips out the # refs/heads/ and refs/tags/ prefixes that would let us distinguish # between branches and tags. By ignoring refnames without digits, we # filter out many common branch names like "release" and # "stabilization", as well as "HEAD" and "master". tags = set([r for r in refs if re.search(r'\d', r)]) if verbose: print("discarding '%s', no digits" % ",".join(refs-tags)) if verbose: print("likely tags: %s" % ",".join(sorted(tags))) for ref in sorted(tags): # sorting will prefer e.g. "2.0" over "2.0rc1" if ref.startswith(tag_prefix): r = ref[len(tag_prefix):] if verbose: print("picking %s" % r) return {"version": r, "full-revisionid": keywords["full"].strip(), "dirty": False, "error": None } # no suitable tags, so version is "0+unknown", but full hex is still there if verbose: print("no suitable tags, using unknown + full revision id") return {"version": "0+unknown", "full-revisionid": keywords["full"].strip(), "dirty": False, "error": "no suitable tags"} @register_vcs_handler("git", "pieces_from_vcs") def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): """Get version from 'git describe' in the root of the source tree. This only gets called if the git-archive 'subst' keywords were *not* expanded, and _version.py hasn't already been rewritten with a short version string, meaning we're inside a checked out source tree. """ if not os.path.exists(os.path.join(root, ".git")): if verbose: print("no .git in %s" % root) raise NotThisMethod("no .git directory") GITS = ["git"] if sys.platform == "win32": GITS = ["git.cmd", "git.exe"] # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty] # if there isn't one, this yields HEX[-dirty] (no NUM) describe_out = run_command(GITS, ["describe", "--tags", "--dirty", "--always", "--long", "--match", "%s*" % tag_prefix], cwd=root) # --long was added in git-1.5.5 if describe_out is None: raise NotThisMethod("'git describe' failed") describe_out = describe_out.strip() full_out = run_command(GITS, ["rev-parse", "HEAD"], cwd=root) if full_out is None: raise NotThisMethod("'git rev-parse' failed") full_out = full_out.strip() pieces = {} pieces["long"] = full_out pieces["short"] = full_out[:7] # maybe improved later pieces["error"] = None # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty] # TAG might have hyphens. git_describe = describe_out # look for -dirty suffix dirty = git_describe.endswith("-dirty") pieces["dirty"] = dirty if dirty: git_describe = git_describe[:git_describe.rindex("-dirty")] # now we have TAG-NUM-gHEX or HEX if "-" in git_describe: # TAG-NUM-gHEX mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe) if not mo: # unparseable. Maybe git-describe is misbehaving? pieces["error"] = ("unable to parse git-describe output: '%s'" % describe_out) return pieces # tag full_tag = mo.group(1) if not full_tag.startswith(tag_prefix): if verbose: fmt = "tag '%s' doesn't start with prefix '%s'" print(fmt % (full_tag, tag_prefix)) pieces["error"] = ("tag '%s' doesn't start with prefix '%s'" % (full_tag, tag_prefix)) return pieces pieces["closest-tag"] = full_tag[len(tag_prefix):] # distance: number of commits since tag pieces["distance"] = int(mo.group(2)) # commit: short hex revision ID pieces["short"] = mo.group(3) else: # HEX: no tags pieces["closest-tag"] = None count_out = run_command(GITS, ["rev-list", "HEAD", "--count"], cwd=root) pieces["distance"] = int(count_out) # total number of commits return pieces def plus_or_dot(pieces): """Return a + if we don't already have one, else return a .""" if "+" in pieces.get("closest-tag", ""): return "." return "+" def render_pep440(pieces): """Build up version string, with post-release "local version identifier". Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty Exceptions: 1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty] """ if pieces["closest-tag"]: rendered = pieces["closest-tag"] if pieces["distance"] or pieces["dirty"]: rendered += plus_or_dot(pieces) rendered += "%d.g%s" % (pieces["distance"], pieces["short"]) if pieces["dirty"]: rendered += ".dirty" else: # exception #1 rendered = "0+untagged.%d.g%s" % (pieces["distance"], pieces["short"]) if pieces["dirty"]: rendered += ".dirty" return rendered def render_pep440_pre(pieces): """TAG[.post.devDISTANCE] -- No -dirty. Exceptions: 1: no tags. 0.post.devDISTANCE """ if pieces["closest-tag"]: rendered = pieces["closest-tag"] if pieces["distance"]: rendered += ".post.dev%d" % pieces["distance"] else: # exception #1 rendered = "0.post.dev%d" % pieces["distance"] return rendered def render_pep440_post(pieces): """TAG[.postDISTANCE[.dev0]+gHEX] . The ".dev0" means dirty. Note that .dev0 sorts backwards (a dirty tree will appear "older" than the corresponding clean one), but you shouldn't be releasing software with -dirty anyways. Exceptions: 1: no tags. 0.postDISTANCE[.dev0] """ if pieces["closest-tag"]: rendered = pieces["closest-tag"] if pieces["distance"] or pieces["dirty"]: rendered += ".post%d" % pieces["distance"] if pieces["dirty"]: rendered += ".dev0" rendered += plus_or_dot(pieces) rendered += "g%s" % pieces["short"] else: # exception #1 rendered = "0.post%d" % pieces["distance"] if pieces["dirty"]: rendered += ".dev0" rendered += "+g%s" % pieces["short"] return rendered def render_pep440_old(pieces): """TAG[.postDISTANCE[.dev0]] . The ".dev0" means dirty. Eexceptions: 1: no tags. 0.postDISTANCE[.dev0] """ if pieces["closest-tag"]: rendered = pieces["closest-tag"] if pieces["distance"] or pieces["dirty"]: rendered += ".post%d" % pieces["distance"] if pieces["dirty"]: rendered += ".dev0" else: # exception #1 rendered = "0.post%d" % pieces["distance"] if pieces["dirty"]: rendered += ".dev0" return rendered def render_git_describe(pieces): """TAG[-DISTANCE-gHEX][-dirty]. Like 'git describe --tags --dirty --always'. Exceptions: 1: no tags. HEX[-dirty] (note: no 'g' prefix) """ if pieces["closest-tag"]: rendered = pieces["closest-tag"] if pieces["distance"]: rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) else: # exception #1 rendered = pieces["short"] if pieces["dirty"]: rendered += "-dirty" return rendered def render_git_describe_long(pieces): """TAG-DISTANCE-gHEX[-dirty]. Like 'git describe --tags --dirty --always -long'. The distance/hash is unconditional. Exceptions: 1: no tags. HEX[-dirty] (note: no 'g' prefix) """ if pieces["closest-tag"]: rendered = pieces["closest-tag"] rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) else: # exception #1 rendered = pieces["short"] if pieces["dirty"]: rendered += "-dirty" return rendered def render(pieces, style): """Render the given version pieces into the requested style.""" if pieces["error"]: return {"version": "unknown", "full-revisionid": pieces.get("long"), "dirty": None, "error": pieces["error"]} if not style or style == "default": style = "pep440" # the default if style == "pep440": rendered = render_pep440(pieces) elif style == "pep440-pre": rendered = render_pep440_pre(pieces) elif style == "pep440-post": rendered = render_pep440_post(pieces) elif style == "pep440-old": rendered = render_pep440_old(pieces) elif style == "git-describe": rendered = render_git_describe(pieces) elif style == "git-describe-long": rendered = render_git_describe_long(pieces) else: raise ValueError("unknown style '%s'" % style) return {"version": rendered, "full-revisionid": pieces["long"], "dirty": pieces["dirty"], "error": None} def get_versions(): """Get version information or return default if unable to do so.""" # I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have # __file__, we can work backwards from there to the root. Some # py2exe/bbfreeze/non-CPython implementations don't do __file__, in which # case we can only use expanded keywords. cfg = get_config() verbose = cfg.verbose try: return git_versions_from_keywords(get_keywords(), cfg.tag_prefix, verbose) except NotThisMethod: pass try: root = os.path.realpath(__file__) # versionfile_source is the relative path from the top of the source # tree (where the .git directory might live) to this file. Invert # this to find the root from __file__. for i in cfg.versionfile_source.split('/'): root = os.path.dirname(root) except NameError: return {"version": "0+unknown", "full-revisionid": None, "dirty": None, "error": "unable to find root of source tree"} try: pieces = git_pieces_from_vcs(cfg.tag_prefix, root, verbose) return render(pieces, cfg.style) except NotThisMethod: pass try: if cfg.parentdir_prefix: return versions_from_parentdir(cfg.parentdir_prefix, root, verbose) except NotThisMethod: pass return {"version": "0+unknown", "full-revisionid": None, "dirty": None, "error": "unable to compute version"} ================================================ FILE: EigenLedger/modules/empyrical/deprecate.py ================================================ """Utilities for marking deprecated functions.""" # Copyright 2018 Quantopian, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import warnings from functools import wraps def deprecated(msg=None, stacklevel=2): """ Used to mark a function as deprecated. Parameters ---------- msg : str The message to display in the deprecation warning. stacklevel : int How far up the stack the warning needs to go, before showing the relevant calling lines. Usage ----- @deprecated(msg='function_a is deprecated! Use function_b instead.') def function_a(*args, **kwargs): """ def deprecated_dec(fn): @wraps(fn) def wrapper(*args, **kwargs): warnings.warn( msg or "Function %s is deprecated." % fn.__name__, category=DeprecationWarning, stacklevel=stacklevel ) return fn(*args, **kwargs) return wrapper return deprecated_dec ================================================ FILE: EigenLedger/modules/empyrical/perf_attrib.py ================================================ from collections import OrderedDict import pandas as pd def perf_attrib(returns, positions, factor_returns, factor_loadings): """ Attributes the performance of a returns stream to a set of risk factors. Performance attribution determines how much each risk factor, e.g., momentum, the technology sector, etc., contributed to total returns, as well as the daily exposure to each of the risk factors. The returns that can be attributed to one of the given risk factors are the `common_returns`, and the returns that _cannot_ be attributed to a risk factor are the `specific_returns`. The `common_returns` and `specific_returns` summed together will always equal the total returns. Parameters ---------- returns : pd.Series Returns for each day in the date range. - Example: 2017-01-01 -0.017098 2017-01-02 0.002683 2017-01-03 -0.008669 positions: pd.Series Daily holdings in percentages, indexed by date. - Examples: dt ticker 2017-01-01 AAPL 0.417582 TLT 0.010989 XOM 0.571429 2017-01-02 AAPL 0.202381 TLT 0.535714 XOM 0.261905 factor_returns : pd.DataFrame Returns by factor, with date as index and factors as columns - Example: momentum reversal 2017-01-01 0.002779 -0.005453 2017-01-02 0.001096 0.010290 factor_loadings : pd.DataFrame Factor loadings for all days in the date range, with date and ticker as index, and factors as columns. - Example: momentum reversal dt ticker 2017-01-01 AAPL -1.592914 0.852830 TLT 0.184864 0.895534 XOM 0.993160 1.149353 2017-01-02 AAPL -0.140009 -0.524952 TLT -1.066978 0.185435 XOM -1.798401 0.761549 Returns ------- tuple of (risk_exposures_portfolio, perf_attribution) risk_exposures_portfolio : pd.DataFrame df indexed by datetime, with factors as columns - Example: momentum reversal dt 2017-01-01 -0.238655 0.077123 2017-01-02 0.821872 1.520515 perf_attribution : pd.DataFrame df with factors, common returns, and specific returns as columns, and datetimes as index - Example: momentum reversal common_returns specific_returns dt 2017-01-01 0.249087 0.935925 1.185012 1.185012 2017-01-02 -0.003194 -0.400786 -0.403980 -0.403980 Note ---- See https://en.wikipedia.org/wiki/Performance_attribution for more details. """ # Make risk data match time range of returns start = returns.index[0] end = returns.index[-1] factor_returns = factor_returns.loc[start:end] factor_loadings = factor_loadings.loc[start:end] factor_loadings.index = factor_loadings.index.set_names(['dt', 'ticker']) positions = positions.copy() positions.index = positions.index.set_names(['dt', 'ticker']) risk_exposures_portfolio = compute_exposures(positions, factor_loadings) perf_attrib_by_factor = risk_exposures_portfolio.multiply(factor_returns) common_returns = perf_attrib_by_factor.sum(axis='columns') tilt_exposure = risk_exposures_portfolio.mean() tilt_returns = factor_returns.multiply(tilt_exposure).sum(axis='columns') timing_returns = common_returns - tilt_returns specific_returns = returns - common_returns returns_df = pd.DataFrame(OrderedDict([ ('total_returns', returns), ('common_returns', common_returns), ('specific_returns', specific_returns), ('tilt_returns', tilt_returns), ('timing_returns', timing_returns) ])) return (risk_exposures_portfolio, pd.concat([perf_attrib_by_factor, returns_df], axis='columns')) def compute_exposures(positions, factor_loadings): """ Compute daily risk factor exposures. Parameters ---------- positions: pd.Series A series of holdings as percentages indexed by date and ticker. - Examples: dt ticker 2017-01-01 AAPL 0.417582 TLT 0.010989 XOM 0.571429 2017-01-02 AAPL 0.202381 TLT 0.535714 XOM 0.261905 factor_loadings : pd.DataFrame Factor loadings for all days in the date range, with date and ticker as index, and factors as columns. - Example: momentum reversal dt ticker 2017-01-01 AAPL -1.592914 0.852830 TLT 0.184864 0.895534 XOM 0.993160 1.149353 2017-01-02 AAPL -0.140009 -0.524952 TLT -1.066978 0.185435 XOM -1.798401 0.761549 Returns ------- risk_exposures_portfolio : pd.DataFrame df indexed by datetime, with factors as columns - Example: momentum reversal dt 2017-01-01 -0.238655 0.077123 2017-01-02 0.821872 1.520515 """ risk_exposures = factor_loadings.multiply(positions, axis='rows') return risk_exposures.groupby(level='dt').sum() ================================================ FILE: EigenLedger/modules/empyrical/periods.py ================================================ APPROX_BDAYS_PER_MONTH = 21 APPROX_BDAYS_PER_YEAR = 252 MONTHS_PER_YEAR = 12 WEEKS_PER_YEAR = 52 QTRS_PER_YEAR = 4 DAILY = 'daily' WEEKLY = 'weekly' MONTHLY = 'monthly' QUARTERLY = 'quarterly' YEARLY = 'yearly' ANNUALIZATION_FACTORS = { DAILY: APPROX_BDAYS_PER_YEAR, WEEKLY: WEEKS_PER_YEAR, MONTHLY: MONTHS_PER_YEAR, QUARTERLY: QTRS_PER_YEAR, YEARLY: 1 } ================================================ FILE: EigenLedger/modules/empyrical/stats.py ================================================ # # Copyright 2016 Quantopian, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import division import math import pandas as pd import numpy as np from math import pow from scipy import stats, optimize from six import iteritems from sys import float_info from .utils import nanmean, nanstd, nanmin, up, down, roll, rolling_window from .periods import ANNUALIZATION_FACTORS, APPROX_BDAYS_PER_YEAR from .periods import DAILY, WEEKLY, MONTHLY, QUARTERLY, YEARLY def _create_unary_vectorized_roll_function(function): def unary_vectorized_roll(arr, window, out=None, **kwargs): """ Computes the {human_readable} measure over a rolling window. Parameters ---------- arr : array-like The array to compute the rolling {human_readable} over. window : int Size of the rolling window in terms of the periodicity of the data. out : array-like, optional Array to use as output buffer. If not passed, a new array will be created. **kwargs Forwarded to :func:`~empyrical.{name}`. Returns ------- rolling_{name} : array-like The rolling {human_readable}. """ allocated_output = out is None if len(arr): out = function( rolling_window(_flatten(arr), min(len(arr), window)).T, out=out, **kwargs ) else: out = np.empty(0, dtype='float64') if allocated_output and isinstance(arr, pd.Series): out = pd.Series(out, index=arr.index[-len(out):]) return out unary_vectorized_roll.__doc__ = unary_vectorized_roll.__doc__.format( name=function.__name__, human_readable=function.__name__.replace('_', ' '), ) return unary_vectorized_roll def _create_binary_vectorized_roll_function(function): def binary_vectorized_roll(lhs, rhs, window, out=None, **kwargs): """ Computes the {human_readable} measure over a rolling window. Parameters ---------- lhs : array-like The first array to pass to the rolling {human_readable}. rhs : array-like The second array to pass to the rolling {human_readable}. window : int Size of the rolling window in terms of the periodicity of the data. out : array-like, optional Array to use as output buffer. If not passed, a new array will be created. **kwargs Forwarded to :func:`~empyrical.{name}`. Returns ------- rolling_{name} : array-like The rolling {human_readable}. """ allocated_output = out is None if window >= 1 and len(lhs) and len(rhs): out = function( rolling_window(_flatten(lhs), min(len(lhs), window)).T, rolling_window(_flatten(rhs), min(len(rhs), window)).T, out=out, **kwargs ) elif allocated_output: out = np.empty(0, dtype='float64') else: out[()] = np.nan if allocated_output: if out.ndim == 1 and isinstance(lhs, pd.Series): out = pd.Series(out, index=lhs.index[-len(out):]) elif out.ndim == 2 and isinstance(lhs, pd.Series): out = pd.DataFrame(out, index=lhs.index[-len(out):]) return out binary_vectorized_roll.__doc__ = binary_vectorized_roll.__doc__.format( name=function.__name__, human_readable=function.__name__.replace('_', ' '), ) return binary_vectorized_roll def _flatten(arr): return arr if not isinstance(arr, pd.Series) else arr.values def _adjust_returns(returns, adjustment_factor): """ Returns the returns series adjusted by adjustment_factor. Optimizes for the case of adjustment_factor being 0 by returning returns itself, not a copy! Parameters ---------- returns : pd.Series or np.ndarray adjustment_factor : pd.Series or np.ndarray or float or int Returns ------- adjusted_returns : array-like """ if isinstance(adjustment_factor, (float, int)) and adjustment_factor == 0: return returns return returns - adjustment_factor def annualization_factor(period, annualization): """ Return annualization factor from period entered or if a custom value is passed in. Parameters ---------- period : str, optional Defines the periodicity of the 'returns' data for purposes of annualizing. Value ignored if `annualization` parameter is specified. Defaults are:: 'monthly':12 'weekly': 52 'daily': 252 annualization : int, optional Used to suppress default values available in `period` to convert returns into annual returns. Value should be the annual frequency of `returns`. Returns ------- annualization_factor : float """ if annualization is None: try: factor = ANNUALIZATION_FACTORS[period] except KeyError: raise ValueError( "Period cannot be '{}'. " "Can be '{}'.".format( period, "', '".join(ANNUALIZATION_FACTORS.keys()) ) ) else: factor = annualization return factor def simple_returns(prices): """ Compute simple returns from a timeseries of prices. Parameters ---------- prices : pd.Series, pd.DataFrame or np.ndarray Prices of assets in wide-format, with assets as columns, and indexed by datetimes. Returns ------- returns : array-like Returns of assets in wide-format, with assets as columns, and index coerced to be tz-aware. """ if isinstance(prices, (pd.DataFrame, pd.Series)): out = prices.pct_change().iloc[1:] else: # Assume np.ndarray out = np.diff(prices, axis=0) np.divide(out, prices[:-1], out=out) return out def cum_returns(returns, starting_value=0, out=None): """ Compute cumulative returns from simple returns. Parameters ---------- returns : pd.Series, np.ndarray, or pd.DataFrame Returns of the strategy as a percentage, noncumulative. - Time series with decimal returns. - Example:: 2015-07-16 -0.012143 2015-07-17 0.045350 2015-07-20 0.030957 2015-07-21 0.004902 - Also accepts two dimensional data. In this case, each column is cumulated. starting_value : float, optional The starting returns. out : array-like, optional Array to use as output buffer. If not passed, a new array will be created. Returns ------- cumulative_returns : array-like Series of cumulative returns. """ if len(returns) < 1: return returns.copy() nanmask = np.isnan(returns) if np.any(nanmask): returns = returns.copy() returns[nanmask] = 0 allocated_output = out is None if allocated_output: out = np.empty_like(returns) np.add(returns, 1, out=out) out.cumprod(axis=0, out=out) if starting_value == 0: np.subtract(out, 1, out=out) else: np.multiply(out, starting_value, out=out) if allocated_output: if returns.ndim == 1 and isinstance(returns, pd.Series): out = pd.Series(out, index=returns.index) elif isinstance(returns, pd.DataFrame): out = pd.DataFrame( out, index=returns.index, columns=returns.columns, ) return out def cum_returns_final(returns, starting_value=0): """ Compute total returns from simple returns. Parameters ---------- returns : pd.DataFrame, pd.Series, or np.ndarray Noncumulative simple returns of one or more timeseries. starting_value : float, optional The starting returns. Returns ------- total_returns : pd.Series, np.ndarray, or float If input is 1-dimensional (a Series or 1D numpy array), the result is a scalar. If input is 2-dimensional (a DataFrame or 2D numpy array), the result is a 1D array containing cumulative returns for each column of input. """ if len(returns) == 0: return np.nan if isinstance(returns, pd.DataFrame): result = (returns + 1).prod() else: result = np.nanprod(returns + 1, axis=0) if starting_value == 0: result -= 1 else: result *= starting_value return result def aggregate_returns(returns, convert_to): """ Aggregates returns by week, month, or year. Parameters ---------- returns : pd.Series Daily returns of the strategy, noncumulative. - See full explanation in :func:`~empyrical.stats.cum_returns`. convert_to : str Can be 'weekly', 'monthly', or 'yearly'. Returns ------- aggregated_returns : pd.Series """ def cumulate_returns(x): return cum_returns(x).iloc[-1] if convert_to == WEEKLY: grouping = [lambda x: x.year, lambda x: x.isocalendar()[1]] elif convert_to == MONTHLY: grouping = [lambda x: x.year, lambda x: x.month] elif convert_to == QUARTERLY: grouping = [lambda x: x.year, lambda x: int(math.ceil(x.month/3.))] elif convert_to == YEARLY: grouping = [lambda x: x.year] else: raise ValueError( 'convert_to must be {}, {} or {}'.format(WEEKLY, MONTHLY, YEARLY) ) return returns.groupby(grouping).apply(cumulate_returns) def max_drawdown(returns, out=None): """ Determines the maximum drawdown of a strategy. Parameters ---------- returns : pd.Series or np.ndarray Daily returns of the strategy, noncumulative. - See full explanation in :func:`~empyrical.stats.cum_returns`. out : array-like, optional Array to use as output buffer. If not passed, a new array will be created. Returns ------- max_drawdown : float Note ----- See https://en.wikipedia.org/wiki/Drawdown_(economics) for more details. """ allocated_output = out is None if allocated_output: out = np.empty(returns.shape[1:]) returns_1d = returns.ndim == 1 if len(returns) < 1: out[()] = np.nan if returns_1d: out = out.item() return out returns_array = np.asanyarray(returns) cumulative = np.empty( (returns.shape[0] + 1,) + returns.shape[1:], dtype='float64', ) cumulative[0] = start = 100 cum_returns(returns_array, starting_value=start, out=cumulative[1:]) max_return = np.fmax.accumulate(cumulative, axis=0) nanmin((cumulative - max_return) / max_return, axis=0, out=out) if returns_1d: out = out.item() elif allocated_output and isinstance(returns, pd.DataFrame): out = pd.Series(out) return out roll_max_drawdown = _create_unary_vectorized_roll_function(max_drawdown) def annual_return(returns, period=DAILY, annualization=None): """ Determines the mean annual growth rate of returns. This is equivilent to the compound annual growth rate. Parameters ---------- returns : pd.Series or np.ndarray Periodic returns of the strategy, noncumulative. - See full explanation in :func:`~empyrical.stats.cum_returns`. period : str, optional Defines the periodicity of the 'returns' data for purposes of annualizing. Value ignored if `annualization` parameter is specified. Defaults are:: 'monthly':12 'weekly': 52 'daily': 252 annualization : int, optional Used to suppress default values available in `period` to convert returns into annual returns. Value should be the annual frequency of `returns`. Returns ------- annual_return : float Annual Return as CAGR (Compounded Annual Growth Rate). """ if len(returns) < 1: return np.nan ann_factor = annualization_factor(period, annualization) num_years = len(returns) / ann_factor # Pass array to ensure index -1 looks up successfully. ending_value = cum_returns_final(returns, starting_value=1) return ending_value ** (1 / num_years) - 1 def cagr(returns, period=DAILY, annualization=None): """ Compute compound annual growth rate. Alias function for :func:`~empyrical.stats.annual_return` Parameters ---------- returns : pd.Series or np.ndarray Daily returns of the strategy, noncumulative. - See full explanation in :func:`~empyrical.stats.cum_returns`. period : str, optional Defines the periodicity of the 'returns' data for purposes of annualizing. Value ignored if `annualization` parameter is specified. Defaults are:: 'monthly':12 'weekly': 52 'daily': 252 annualization : int, optional Used to suppress default values available in `period` to convert returns into annual returns. Value should be the annual frequency of `returns`. - See full explanation in :func:`~empyrical.stats.annual_return`. Returns ------- cagr : float The CAGR value. """ return annual_return(returns, period, annualization) roll_cagr = _create_unary_vectorized_roll_function(cagr) def annual_volatility(returns, period=DAILY, alpha=2.0, annualization=None, out=None): """ Determines the annual volatility of a strategy. Parameters ---------- returns : pd.Series or np.ndarray Periodic returns of the strategy, noncumulative. - See full explanation in :func:`~empyrical.stats.cum_returns`. period : str, optional Defines the periodicity of the 'returns' data for purposes of annualizing. Value ignored if `annualization` parameter is specified. Defaults are:: 'monthly':12 'weekly': 52 'daily': 252 alpha : float, optional Scaling relation (Levy stability exponent). annualization : int, optional Used to suppress default values available in `period` to convert returns into annual returns. Value should be the annual frequency of `returns`. out : array-like, optional Array to use as output buffer. If not passed, a new array will be created. Returns ------- annual_volatility : float """ allocated_output = out is None if allocated_output: out = np.empty(returns.shape[1:]) returns_1d = returns.ndim == 1 if len(returns) < 2: out[()] = np.nan if returns_1d: out = out.item() return out ann_factor = annualization_factor(period, annualization) nanstd(returns, ddof=1, axis=0, out=out) out = np.multiply(out, ann_factor ** (1.0 / alpha), out=out) if returns_1d: out = out.item() return out roll_annual_volatility = _create_unary_vectorized_roll_function( annual_volatility, ) def calmar_ratio(returns, period=DAILY, annualization=None): """ Determines the Calmar ratio, or drawdown ratio, of a strategy. Parameters ---------- returns : pd.Series or np.ndarray Daily returns of the strategy, noncumulative. - See full explanation in :func:`~empyrical.stats.cum_returns`. period : str, optional Defines the periodicity of the 'returns' data for purposes of annualizing. Value ignored if `annualization` parameter is specified. Defaults are:: 'monthly':12 'weekly': 52 'daily': 252 annualization : int, optional Used to suppress default values available in `period` to convert returns into annual returns. Value should be the annual frequency of `returns`. Returns ------- calmar_ratio : float Calmar ratio (drawdown ratio) as float. Returns np.nan if there is no calmar ratio. Note ----- See https://en.wikipedia.org/wiki/Calmar_ratio for more details. """ max_dd = max_drawdown(returns=returns) if max_dd < 0: temp = annual_return( returns=returns, period=period, annualization=annualization ) / abs(max_dd) else: return np.nan if np.isinf(temp): return np.nan return temp def omega_ratio(returns, risk_free=0.0, required_return=0.0, annualization=APPROX_BDAYS_PER_YEAR): """Determines the Omega ratio of a strategy. Parameters ---------- returns : pd.Series or np.ndarray Daily returns of the strategy, noncumulative. - See full explanation in :func:`~empyrical.stats.cum_returns`. risk_free : int, float Constant risk-free return throughout the period required_return : float, optional Minimum acceptance return of the investor. Threshold over which to consider positive vs negative returns. It will be converted to a value appropriate for the period of the returns. E.g. An annual minimum acceptable return of 100 will translate to a minimum acceptable return of 0.018. annualization : int, optional Factor used to convert the required_return into a daily value. Enter 1 if no time period conversion is necessary. Returns ------- omega_ratio : float Note ----- See https://en.wikipedia.org/wiki/Omega_ratio for more details. """ if len(returns) < 2: return np.nan if annualization == 1: return_threshold = required_return elif required_return <= -1: return np.nan else: return_threshold = (1 + required_return) ** \ (1. / annualization) - 1 returns_less_thresh = returns - risk_free - return_threshold numer = sum(returns_less_thresh[returns_less_thresh > 0.0]) denom = -1.0 * sum(returns_less_thresh[returns_less_thresh < 0.0]) if denom > 0.0: return numer / denom else: return np.nan def sharpe_ratio(returns, risk_free=0, period=DAILY, annualization=None, out=None): """ Determines the Sharpe ratio of a strategy. Parameters ---------- returns : pd.Series or np.ndarray Daily returns of the strategy, noncumulative. - See full explanation in :func:`~empyrical.stats.cum_returns`. risk_free : int, float Constant daily risk-free return throughout the period. period : str, optional Defines the periodicity of the 'returns' data for purposes of annualizing. Value ignored if `annualization` parameter is specified. Defaults are:: 'monthly':12 'weekly': 52 'daily': 252 annualization : int, optional Used to suppress default values available in `period` to convert returns into annual returns. Value should be the annual frequency of `returns`. out : array-like, optional Array to use as output buffer. If not passed, a new array will be created. Returns ------- sharpe_ratio : float nan if insufficient length of returns or if if adjusted returns are 0. Note ----- See https://en.wikipedia.org/wiki/Sharpe_ratio for more details. """ allocated_output = out is None if allocated_output: out = np.empty(returns.shape[1:]) return_1d = returns.ndim == 1 if len(returns) < 2: out[()] = np.nan if return_1d: out = out.item() return out returns_risk_adj = np.asanyarray(_adjust_returns(returns, risk_free)) ann_factor = annualization_factor(period, annualization) np.multiply( np.divide( nanmean(returns_risk_adj, axis=0), nanstd(returns_risk_adj, ddof=1, axis=0), out=out, ), np.sqrt(ann_factor), out=out, ) if return_1d: out = out.item() return out roll_sharpe_ratio = _create_unary_vectorized_roll_function(sharpe_ratio) def sortino_ratio(returns, required_return=0, period=DAILY, annualization=None, out=None, _downside_risk=None): """ Determines the Sortino ratio of a strategy. Parameters ---------- returns : pd.Series or np.ndarray or pd.DataFrame Daily returns of the strategy, noncumulative. - See full explanation in :func:`~empyrical.stats.cum_returns`. required_return: float / series minimum acceptable return period : str, optional Defines the periodicity of the 'returns' data for purposes of annualizing. Value ignored if `annualization` parameter is specified. Defaults are:: 'monthly':12 'weekly': 52 'daily': 252 annualization : int, optional Used to suppress default values available in `period` to convert returns into annual returns. Value should be the annual frequency of `returns`. _downside_risk : float, optional The downside risk of the given inputs, if known. Will be calculated if not provided. out : array-like, optional Array to use as output buffer. If not passed, a new array will be created. Returns ------- sortino_ratio : float or pd.Series depends on input type series ==> float DataFrame ==> pd.Series Note ----- See ``__ for more details. """ allocated_output = out is None if allocated_output: out = np.empty(returns.shape[1:]) return_1d = returns.ndim == 1 if len(returns) < 2: out[()] = np.nan if return_1d: out = out.item() return out adj_returns = np.asanyarray(_adjust_returns(returns, required_return)) ann_factor = annualization_factor(period, annualization) average_annual_return = nanmean(adj_returns, axis=0) * ann_factor annualized_downside_risk = ( _downside_risk if _downside_risk is not None else downside_risk(returns, required_return, period, annualization) ) np.divide(average_annual_return, annualized_downside_risk, out=out) if return_1d: out = out.item() elif isinstance(returns, pd.DataFrame): out = pd.Series(out) return out roll_sortino_ratio = _create_unary_vectorized_roll_function(sortino_ratio) def downside_risk(returns, required_return=0, period=DAILY, annualization=None, out=None): """ Determines the downside deviation below a threshold Parameters ---------- returns : pd.Series or np.ndarray or pd.DataFrame Daily returns of the strategy, noncumulative. - See full explanation in :func:`~empyrical.stats.cum_returns`. required_return: float / series minimum acceptable return period : str, optional Defines the periodicity of the 'returns' data for purposes of annualizing. Value ignored if `annualization` parameter is specified. Defaults are:: 'monthly':12 'weekly': 52 'daily': 252 annualization : int, optional Used to suppress default values available in `period` to convert returns into annual returns. Value should be the annual frequency of `returns`. out : array-like, optional Array to use as output buffer. If not passed, a new array will be created. Returns ------- downside_deviation : float or pd.Series depends on input type series ==> float DataFrame ==> pd.Series Note ----- See ``__ for more details, specifically why using the standard deviation of the negative returns is not correct. """ allocated_output = out is None if allocated_output: out = np.empty(returns.shape[1:]) returns_1d = returns.ndim == 1 if len(returns) < 1: out[()] = np.nan if returns_1d: out = out.item() return out ann_factor = annualization_factor(period, annualization) downside_diff = np.clip( _adjust_returns( np.asanyarray(returns), np.asanyarray(required_return), ), -np.inf, 0, ) np.square(downside_diff, out=downside_diff) nanmean(downside_diff, axis=0, out=out) np.sqrt(out, out=out) np.multiply(out, np.sqrt(ann_factor), out=out) if returns_1d: out = out.item() elif isinstance(returns, pd.DataFrame): out = pd.Series(out, index=returns.columns) return out roll_downsize_risk = _create_unary_vectorized_roll_function(downside_risk) def excess_sharpe(returns, factor_returns, out=None): """ Determines the Excess Sharpe of a strategy. Parameters ---------- returns : pd.Series or np.ndarray Daily returns of the strategy, noncumulative. - See full explanation in :func:`~empyrical.stats.cum_returns`. factor_returns: float / series Benchmark return to compare returns against. out : array-like, optional Array to use as output buffer. If not passed, a new array will be created. Returns ------- excess_sharpe : float Note ----- The excess Sharpe is a simplified Information Ratio that uses tracking error rather than "active risk" as the denominator. """ allocated_output = out is None if allocated_output: out = np.empty(returns.shape[1:]) returns_1d = returns.ndim == 1 if len(returns) < 2: out[()] = np.nan if returns_1d: out = out.item() return out active_return = _adjust_returns(returns, factor_returns) tracking_error = np.nan_to_num(nanstd(active_return, ddof=1, axis=0)) out = np.divide( nanmean(active_return, axis=0, out=out), tracking_error, out=out, ) if returns_1d: out = out.item() return out roll_excess_sharpe = _create_binary_vectorized_roll_function(excess_sharpe) def _to_pandas(ob): """Convert an array-like to a pandas object. Parameters ---------- ob : array-like The object to convert. Returns ------- pandas_structure : pd.Series or pd.DataFrame The correct structure based on the dimensionality of the data. """ if isinstance(ob, (pd.Series, pd.DataFrame)): return ob if ob.ndim == 1: return pd.Series(ob) elif ob.ndim == 2: return pd.DataFrame(ob) else: raise ValueError( 'cannot convert array of dim > 2 to a pandas structure', ) def _aligned_series(*many_series): """ Return a new list of series containing the data in the input series, but with their indices aligned. NaNs will be filled in for missing values. Parameters ---------- *many_series The series to align. Returns ------- aligned_series : iterable[array-like] A new list of series containing the data in the input series, but with their indices aligned. NaNs will be filled in for missing values. """ head = many_series[0] tail = many_series[1:] n = len(head) if (isinstance(head, np.ndarray) and all(len(s) == n and isinstance(s, np.ndarray) for s in tail)): # optimization: ndarrays of the same length are already aligned return many_series # dataframe has no ``itervalues`` return ( v for _, v in iteritems(pd.concat(map(_to_pandas, many_series), axis=1)) ) def alpha_beta(returns, factor_returns, risk_free=0.0, period=DAILY, annualization=None, out=None): """Calculates annualized alpha and beta. Parameters ---------- returns : pd.Series Daily returns of the strategy, noncumulative. - See full explanation in :func:`~empyrical.stats.cum_returns`. factor_returns : pd.Series Daily noncumulative returns of the factor to which beta is computed. Usually a benchmark such as the market. - This is in the same style as returns. risk_free : int, float, optional Constant risk-free return throughout the period. For example, the interest rate on a three month us treasury bill. period : str, optional Defines the periodicity of the 'returns' data for purposes of annualizing. Value ignored if `annualization` parameter is specified. Defaults are:: 'monthly':12 'weekly': 52 'daily': 252 annualization : int, optional Used to suppress default values available in `period` to convert returns into annual returns. Value should be the annual frequency of `returns`. out : array-like, optional Array to use as output buffer. If not passed, a new array will be created. Returns ------- alpha : float beta : float """ returns, factor_returns = _aligned_series(returns, factor_returns) return alpha_beta_aligned( returns, factor_returns, risk_free=risk_free, period=period, annualization=annualization, out=out, ) def roll_alpha_beta(returns, factor_returns, window=10, **kwargs): """ Computes alpha and beta over a rolling window. Parameters ---------- lhs : array-like The first array to pass to the rolling alpha-beta. rhs : array-like The second array to pass to the rolling alpha-beta. window : int Size of the rolling window in terms of the periodicity of the data. out : array-like, optional Array to use as output buffer. If not passed, a new array will be created. **kwargs Forwarded to :func:`~empyrical.alpha_beta`. """ returns, factor_returns = _aligned_series(returns, factor_returns) return roll_alpha_beta_aligned( returns, factor_returns, window=window, **kwargs ) def alpha_beta_aligned(returns, factor_returns, risk_free=0.0, period=DAILY, annualization=None, out=None): """Calculates annualized alpha and beta. If they are pd.Series, expects returns and factor_returns have already been aligned on their labels. If np.ndarray, these arguments should have the same shape. Parameters ---------- returns : pd.Series or np.ndarray Daily returns of the strategy, noncumulative. - See full explanation in :func:`~empyrical.stats.cum_returns`. factor_returns : pd.Series or np.ndarray Daily noncumulative returns of the factor to which beta is computed. Usually a benchmark such as the market. - This is in the same style as returns. risk_free : int, float, optional Constant risk-free return throughout the period. For example, the interest rate on a three month us treasury bill. period : str, optional Defines the periodicity of the 'returns' data for purposes of annualizing. Value ignored if `annualization` parameter is specified. Defaults are:: 'monthly':12 'weekly': 52 'daily': 252 annualization : int, optional Used to suppress default values available in `period` to convert returns into annual returns. Value should be the annual frequency of `returns`. out : array-like, optional Array to use as output buffer. If not passed, a new array will be created. Returns ------- alpha : float beta : float """ if out is None: out = np.empty(returns.shape[1:] + (2,), dtype='float64') b = beta_aligned(returns, factor_returns, risk_free, out=out[..., 1]) alpha_aligned( returns, factor_returns, risk_free, period, annualization, out=out[..., 0], _beta=b, ) return out roll_alpha_beta_aligned = _create_binary_vectorized_roll_function( alpha_beta_aligned, ) def alpha(returns, factor_returns, risk_free=0.0, period=DAILY, annualization=None, out=None, _beta=None): """Calculates annualized alpha. Parameters ---------- returns : pd.Series Daily returns of the strategy, noncumulative. - See full explanation in :func:`~empyrical.stats.cum_returns`. factor_returns : pd.Series Daily noncumulative returns of the factor to which beta is computed. Usually a benchmark such as the market. - This is in the same style as returns. risk_free : int, float, optional Constant risk-free return throughout the period. For example, the interest rate on a three month us treasury bill. period : str, optional Defines the periodicity of the 'returns' data for purposes of annualizing. Value ignored if `annualization` parameter is specified. Defaults are:: 'monthly':12 'weekly': 52 'daily': 252 annualization : int, optional Used to suppress default values available in `period` to convert returns into annual returns. Value should be the annual frequency of `returns`. - See full explanation in :func:`~empyrical.stats.annual_return`. _beta : float, optional The beta for the given inputs, if already known. Will be calculated internally if not provided. out : array-like, optional Array to use as output buffer. If not passed, a new array will be created. Returns ------- float Alpha. """ if not (isinstance(returns, np.ndarray) and isinstance(factor_returns, np.ndarray)): returns, factor_returns = _aligned_series(returns, factor_returns) return alpha_aligned( returns, factor_returns, risk_free=risk_free, period=period, annualization=annualization, out=out, _beta=_beta ) roll_alpha = _create_binary_vectorized_roll_function(alpha) def alpha_aligned(returns, factor_returns, risk_free=0.0, period=DAILY, annualization=None, out=None, _beta=None): """Calculates annualized alpha. If they are pd.Series, expects returns and factor_returns have already been aligned on their labels. If np.ndarray, these arguments should have the same shape. Parameters ---------- returns : pd.Series or np.ndarray Daily returns of the strategy, noncumulative. - See full explanation in :func:`~empyrical.stats.cum_returns`. factor_returns : pd.Series or np.ndarray Daily noncumulative returns of the factor to which beta is computed. Usually a benchmark such as the market. - This is in the same style as returns. risk_free : int, float, optional Constant risk-free return throughout the period. For example, the interest rate on a three month us treasury bill. period : str, optional Defines the periodicity of the 'returns' data for purposes of annualizing. Value ignored if `annualization` parameter is specified. Defaults are:: 'monthly':12 'weekly': 52 'daily': 252 annualization : int, optional Used to suppress default values available in `period` to convert returns into annual returns. Value should be the annual frequency of `returns`. - See full explanation in :func:`~empyrical.stats.annual_return`. _beta : float, optional The beta for the given inputs, if already known. Will be calculated internally if not provided. out : array-like, optional Array to use as output buffer. If not passed, a new array will be created. Returns ------- alpha : float """ allocated_output = out is None if allocated_output: out = np.empty(returns.shape[1:], dtype='float64') if len(returns) < 2: out[()] = np.nan if returns.ndim == 1: out = out.item() return out ann_factor = annualization_factor(period, annualization) if _beta is None: _beta = beta_aligned(returns, factor_returns, risk_free) adj_returns = _adjust_returns(returns, risk_free) adj_factor_returns = _adjust_returns(factor_returns, risk_free) alpha_series = adj_returns - (_beta * adj_factor_returns) out = np.subtract( np.power( np.add( nanmean(alpha_series, axis=0, out=out), 1, out=out ), ann_factor, out=out ), 1, out=out ) if allocated_output and isinstance(returns, pd.DataFrame): out = pd.Series(out) if returns.ndim == 1: out = out.item() return out roll_alpha_aligned = _create_binary_vectorized_roll_function(alpha_aligned) def beta(returns, factor_returns, risk_free=0.0, out=None): """Calculates beta. Parameters ---------- returns : pd.Series Daily returns of the strategy, noncumulative. - See full explanation in :func:`~empyrical.stats.cum_returns`. factor_returns : pd.Series Daily noncumulative returns of the factor to which beta is computed. Usually a benchmark such as the market. - This is in the same style as returns. risk_free : int, float, optional Constant risk-free return throughout the period. For example, the interest rate on a three month us treasury bill. out : array-like, optional Array to use as output buffer. If not passed, a new array will be created. Returns ------- beta : float """ if not (isinstance(returns, np.ndarray) and isinstance(factor_returns, np.ndarray)): returns, factor_returns = _aligned_series(returns, factor_returns) return beta_aligned( returns, factor_returns, risk_free=risk_free, out=out, ) roll_beta = _create_binary_vectorized_roll_function(beta) def beta_aligned(returns, factor_returns, risk_free=0.0, out=None): """Calculates beta. If they are pd.Series, expects returns and factor_returns have already been aligned on their labels. If np.ndarray, these arguments should have the same shape. Parameters ---------- returns : pd.Series or np.ndarray Daily returns of the strategy, noncumulative. - See full explanation in :func:`~empyrical.stats.cum_returns`. factor_returns : pd.Series or np.ndarray Daily noncumulative returns of the factor to which beta is computed. Usually a benchmark such as the market. - This is in the same style as returns. risk_free : int, float, optional Constant risk-free return throughout the period. For example, the interest rate on a three month us treasury bill. out : array-like, optional Array to use as output buffer. If not passed, a new array will be created. Returns ------- beta : float Beta. """ # Cache these as locals since we're going to call them multiple times. nan = np.nan isnan = np.isnan returns_1d = returns.ndim == 1 if returns_1d: returns = np.asanyarray(returns)[:, np.newaxis] if factor_returns.ndim == 1: factor_returns = np.asanyarray(factor_returns)[:, np.newaxis] N, M = returns.shape if out is None: out = np.full(M, nan) elif out.ndim == 0: out = out[np.newaxis] if len(returns) < 1 or len(factor_returns) < 2: out[()] = nan if returns_1d: out = out.item() return out # Copy N times as a column vector and fill with nans to have the same # missing value pattern as the dependent variable. # # PERF_TODO: We could probably avoid the space blowup by doing this in # Cython. # shape: (N, M) independent = np.where( isnan(returns), nan, factor_returns, ) # Calculate beta as Cov(X, Y) / Cov(X, X). # https://en.wikipedia.org/wiki/Simple_linear_regression#Fitting_the_regression_line # noqa # # NOTE: The usual formula for covariance is:: # # mean((X - mean(X)) * (Y - mean(Y))) # # However, we don't actually need to take the mean of both sides of the # product, because of the folllowing equivalence:: # # Let X_res = (X - mean(X)). # We have: # # mean(X_res * (Y - mean(Y))) = mean(X_res * (Y - mean(Y))) # (1) = mean((X_res * Y) - (X_res * mean(Y))) # (2) = mean(X_res * Y) - mean(X_res * mean(Y)) # (3) = mean(X_res * Y) - mean(X_res) * mean(Y) # (4) = mean(X_res * Y) - 0 * mean(Y) # (5) = mean(X_res * Y) # # # The tricky step in the above derivation is step (4). We know that # mean(X_res) is zero because, for any X: # # mean(X - mean(X)) = mean(X) - mean(X) = 0. # # The upshot of this is that we only have to center one of `independent` # and `dependent` when calculating covariances. Since we need the centered # `independent` to calculate its variance in the next step, we choose to # center `independent`. ind_residual = independent - nanmean(independent, axis=0) covariances = nanmean(ind_residual * returns, axis=0) # We end up with different variances in each column here because each # column may have a different subset of the data dropped due to missing # data in the corresponding dependent column. # shape: (M,) np.square(ind_residual, out=ind_residual) independent_variances = nanmean(ind_residual, axis=0) independent_variances[independent_variances < 1.0e-30] = np.nan np.divide(covariances, independent_variances, out=out) if returns_1d: out = out.item() return out roll_beta_aligned = _create_binary_vectorized_roll_function(beta_aligned) def stability_of_timeseries(returns): """Determines R-squared of a linear fit to the cumulative log returns. Computes an ordinary least squares linear fit, and returns R-squared. Parameters ---------- returns : pd.Series or np.ndarray Daily returns of the strategy, noncumulative. - See full explanation in :func:`~empyrical.stats.cum_returns`. Returns ------- float R-squared. """ if len(returns) < 2: return np.nan returns = np.asanyarray(returns) returns = returns[~np.isnan(returns)] cum_log_returns = np.log1p(returns).cumsum() rhat = stats.linregress(np.arange(len(cum_log_returns)), cum_log_returns)[2] return rhat ** 2 def tail_ratio(returns): """Determines the ratio between the right (95%) and left tail (5%). For example, a ratio of 0.25 means that losses are four times as bad as profits. Parameters ---------- returns : pd.Series or np.ndarray Daily returns of the strategy, noncumulative. - See full explanation in :func:`~empyrical.stats.cum_returns`. Returns ------- tail_ratio : float """ if len(returns) < 1: return np.nan returns = np.asanyarray(returns) # Be tolerant of nan's returns = returns[~np.isnan(returns)] if len(returns) < 1: return np.nan return np.abs(np.percentile(returns, 95)) / \ np.abs(np.percentile(returns, 5)) def capture(returns, factor_returns, period=DAILY): """Compute capture ratio. Parameters ---------- returns : pd.Series or np.ndarray Returns of the strategy, noncumulative. - See full explanation in :func:`~empyrical.stats.cum_returns`. factor_returns : pd.Series or np.ndarray Noncumulative returns of the factor to which beta is computed. Usually a benchmark such as the market. - This is in the same style as returns. period : str, optional Defines the periodicity of the 'returns' data for purposes of annualizing. Value ignored if `annualization` parameter is specified. Defaults are:: 'monthly':12 'weekly': 52 'daily': 252 Returns ------- capture_ratio : float Note ---- See http://www.investopedia.com/terms/u/up-market-capture-ratio.asp for details. """ return (annual_return(returns, period=period) / annual_return(factor_returns, period=period)) def beta_fragility_heuristic(returns, factor_returns): """Estimate fragility to drops in beta. Parameters ---------- returns : pd.Series or np.ndarray Daily returns of the strategy, noncumulative. - See full explanation in :func:`~empyrical.stats.cum_returns`. factor_returns : pd.Series or np.ndarray Daily noncumulative returns of the factor to which beta is computed. Usually a benchmark such as the market. - This is in the same style as returns. Returns ------- float, np.nan The beta fragility of the strategy. Note ---- A negative return value indicates potential losses could follow volatility in beta. The magnitude of the negative value indicates the size of the potential loss. seealso:: `A New Heuristic Measure of Fragility and Tail Risks: Application to Stress Testing` https://www.imf.org/external/pubs/ft/wp/2012/wp12216.pdf An IMF Working Paper describing the heuristic """ if len(returns) < 3 or len(factor_returns) < 3: return np.nan return beta_fragility_heuristic_aligned( *_aligned_series(returns, factor_returns)) def beta_fragility_heuristic_aligned(returns, factor_returns): """Estimate fragility to drops in beta Parameters ---------- returns : pd.Series or np.ndarray Daily returns of the strategy, noncumulative. - See full explanation in :func:`~empyrical.stats.cum_returns`. factor_returns : pd.Series or np.ndarray Daily noncumulative returns of the factor to which beta is computed. Usually a benchmark such as the market. - This is in the same style as returns. Returns ------- float, np.nan The beta fragility of the strategy. Note ---- If they are pd.Series, expects returns and factor_returns have already been aligned on their labels. If np.ndarray, these arguments should have the same shape. seealso:: `A New Heuristic Measure of Fragility and Tail Risks: Application to Stress Testing` https://www.imf.org/external/pubs/ft/wp/2012/wp12216.pdf An IMF Working Paper describing the heuristic """ if len(returns) < 3 or len(factor_returns) < 3: return np.nan # combine returns and factor returns into pairs returns_series = pd.Series(returns) factor_returns_series = pd.Series(factor_returns) pairs = pd.concat([returns_series, factor_returns_series], axis=1) pairs.columns = ['returns', 'factor_returns'] # exclude any rows where returns are nan pairs = pairs.dropna() # sort by beta pairs = pairs.sort_values(by='factor_returns') # find the three vectors, using median of 3 start_index = 0 mid_index = int(np.around(len(pairs) / 2, 0)) end_index = len(pairs) - 1 (start_returns, start_factor_returns) = pairs.iloc[start_index] (mid_returns, mid_factor_returns) = pairs.iloc[mid_index] (end_returns, end_factor_returns) = pairs.iloc[end_index] factor_returns_range = (end_factor_returns - start_factor_returns) start_returns_weight = 0.5 end_returns_weight = 0.5 # find weights for the start and end returns # using a convex combination if not factor_returns_range == 0: start_returns_weight = \ (mid_factor_returns - start_factor_returns) / \ factor_returns_range end_returns_weight = \ (end_factor_returns - mid_factor_returns) / \ factor_returns_range # calculate fragility heuristic heuristic = (start_returns_weight*start_returns) + \ (end_returns_weight*end_returns) - mid_returns return heuristic def gpd_risk_estimates(returns, var_p=0.01): """Estimate VaR and ES using the Generalized Pareto Distribution (GPD) Parameters ---------- returns : pd.Series or np.ndarray Daily returns of the strategy, noncumulative. - See full explanation in :func:`~empyrical.stats.cum_returns`. var_p : float The percentile to use for estimating the VaR and ES Returns ------- [threshold, scale_param, shape_param, var_estimate, es_estimate] : list[float] threshold - the threshold use to cut off exception tail losses scale_param - a parameter (often denoted by sigma, capturing the scale, related to variance) shape_param - a parameter (often denoted by xi, capturing the shape or type of the distribution) var_estimate - an estimate for the VaR for the given percentile es_estimate - an estimate for the ES for the given percentile Note ---- seealso:: `An Application of Extreme Value Theory for Measuring Risk ` A paper describing how to use the Generalized Pareto Distribution to estimate VaR and ES. """ if len(returns) < 3: result = np.zeros(5) if isinstance(returns, pd.Series): result = pd.Series(result) return result return gpd_risk_estimates_aligned(*_aligned_series(returns, var_p)) def gpd_risk_estimates_aligned(returns, var_p=0.01): """Estimate VaR and ES using the Generalized Pareto Distribution (GPD) Parameters ---------- returns : pd.Series or np.ndarray Daily returns of the strategy, noncumulative. - See full explanation in :func:`~empyrical.stats.cum_returns`. var_p : float The percentile to use for estimating the VaR and ES Returns ------- [threshold, scale_param, shape_param, var_estimate, es_estimate] : list[float] threshold - the threshold use to cut off exception tail losses scale_param - a parameter (often denoted by sigma, capturing the scale, related to variance) shape_param - a parameter (often denoted by xi, capturing the shape or type of the distribution) var_estimate - an estimate for the VaR for the given percentile es_estimate - an estimate for the ES for the given percentile Note ---- seealso:: `An Application of Extreme Value Theory for Measuring Risk ` A paper describing how to use the Generalized Pareto Distribution to estimate VaR and ES. """ result = np.zeros(5) if not len(returns) < 3: DEFAULT_THRESHOLD = 0.2 MINIMUM_THRESHOLD = 0.000000001 try: returns_array = pd.Series(returns).to_numpy() except AttributeError: # while zipline requires support for pandas < 0.25 returns_array = pd.Series(returns).as_matrix() flipped_returns = -1 * returns_array losses = flipped_returns[flipped_returns > 0] threshold = DEFAULT_THRESHOLD finished = False scale_param = 0 shape_param = 0 while not finished and threshold > MINIMUM_THRESHOLD: losses_beyond_threshold = \ losses[losses >= threshold] param_result = \ gpd_loglikelihood_minimizer_aligned(losses_beyond_threshold) if (param_result[0] is not False and param_result[1] is not False): scale_param = param_result[0] shape_param = param_result[1] var_estimate = gpd_var_calculator(threshold, scale_param, shape_param, var_p, len(losses), len(losses_beyond_threshold)) # non-negative shape parameter is required for fat tails # non-negative VaR estimate is required for loss of some kind if (shape_param > 0 and var_estimate > 0): finished = True if (not finished): threshold = threshold / 2 if (finished): es_estimate = gpd_es_calculator(var_estimate, threshold, scale_param, shape_param) result = np.array([threshold, scale_param, shape_param, var_estimate, es_estimate]) if isinstance(returns, pd.Series): result = pd.Series(result) return result def gpd_es_calculator(var_estimate, threshold, scale_param, shape_param): result = 0 if ((1 - shape_param) != 0): # this formula is from Gilli and Kellezi pg. 8 var_ratio = (var_estimate/(1 - shape_param)) param_ratio = ((scale_param - (shape_param * threshold)) / (1 - shape_param)) result = var_ratio + param_ratio return result def gpd_var_calculator(threshold, scale_param, shape_param, probability, total_n, exceedance_n): result = 0 if (exceedance_n > 0 and shape_param > 0): # this formula is from Gilli and Kellezi pg. 12 param_ratio = scale_param / shape_param prob_ratio = (total_n/exceedance_n) * probability result = threshold + (param_ratio * (pow(prob_ratio, -shape_param) - 1)) return result def gpd_loglikelihood_minimizer_aligned(price_data): result = [False, False] DEFAULT_SCALE_PARAM = 1 DEFAULT_SHAPE_PARAM = 1 if (len(price_data) > 0): gpd_loglikelihood_lambda = \ gpd_loglikelihood_factory(price_data) optimization_results = \ optimize.minimize(gpd_loglikelihood_lambda, [DEFAULT_SCALE_PARAM, DEFAULT_SHAPE_PARAM], method='Nelder-Mead') if optimization_results.success: resulting_params = optimization_results.x if len(resulting_params) == 2: result[0] = resulting_params[0] result[1] = resulting_params[1] return result def gpd_loglikelihood_factory(price_data): return lambda params: gpd_loglikelihood(params, price_data) def gpd_loglikelihood(params, price_data): if (params[1] != 0): return -gpd_loglikelihood_scale_and_shape(params[0], params[1], price_data) else: return -gpd_loglikelihood_scale_only(params[0], price_data) def gpd_loglikelihood_scale_and_shape_factory(price_data): # minimize a function of two variables requires a list of params # we are expecting the lambda below to be called as follows: # parameters = [scale, shape] # the final outer negative is added because scipy only minimizes return lambda params: \ -gpd_loglikelihood_scale_and_shape(params[0], params[1], price_data) def gpd_loglikelihood_scale_and_shape(scale, shape, price_data): n = len(price_data) result = -1 * float_info.max if (scale != 0): param_factor = shape / scale if (shape != 0 and param_factor >= 0 and scale >= 0): result = ((-n * np.log(scale)) - (((1 / shape) + 1) * (np.log((shape / scale * price_data) + 1)).sum())) return result def gpd_loglikelihood_scale_only_factory(price_data): # the negative is added because scipy only minimizes return lambda scale: \ -gpd_loglikelihood_scale_only(scale, price_data) def gpd_loglikelihood_scale_only(scale, price_data): n = len(price_data) data_sum = price_data.sum() result = -1 * float_info.max if (scale >= 0): result = ((-n*np.log(scale)) - (data_sum/scale)) return result def up_capture(returns, factor_returns, **kwargs): """ Compute the capture ratio for periods when the benchmark return is positive Parameters ---------- returns : pd.Series or np.ndarray Returns of the strategy, noncumulative. - See full explanation in :func:`~empyrical.stats.cum_returns`. factor_returns : pd.Series or np.ndarray Noncumulative returns of the factor to which beta is computed. Usually a benchmark such as the market. - This is in the same style as returns. period : str, optional Defines the periodicity of the 'returns' data for purposes of annualizing. Value ignored if `annualization` parameter is specified. Defaults are:: 'monthly':12 'weekly': 52 'daily': 252 Returns ------- up_capture : float Note ---- See http://www.investopedia.com/terms/u/up-market-capture-ratio.asp for more information. """ return up(returns, factor_returns, function=capture, **kwargs) def down_capture(returns, factor_returns, **kwargs): """ Compute the capture ratio for periods when the benchmark return is negative Parameters ---------- returns : pd.Series or np.ndarray Returns of the strategy, noncumulative. - See full explanation in :func:`~empyrical.stats.cum_returns`. factor_returns : pd.Series or np.ndarray Noncumulative returns of the factor to which beta is computed. Usually a benchmark such as the market. - This is in the same style as returns. period : str, optional Defines the periodicity of the 'returns' data for purposes of annualizing. Value ignored if `annualization` parameter is specified. Defaults are:: 'monthly':12 'weekly': 52 'daily': 252 Returns ------- down_capture : float Note ---- See http://www.investopedia.com/terms/d/down-market-capture-ratio.asp for more information. """ return down(returns, factor_returns, function=capture, **kwargs) def up_down_capture(returns, factor_returns, **kwargs): """ Computes the ratio of up_capture to down_capture. Parameters ---------- returns : pd.Series or np.ndarray Returns of the strategy, noncumulative. - See full explanation in :func:`~empyrical.stats.cum_returns`. factor_returns : pd.Series or np.ndarray Noncumulative returns of the factor to which beta is computed. Usually a benchmark such as the market. - This is in the same style as returns. period : str, optional Defines the periodicity of the 'returns' data for purposes of annualizing. Value ignored if `annualization` parameter is specified. Defaults are:: 'monthly':12 'weekly': 52 'daily': 252 Returns ------- up_down_capture : float the updown capture ratio """ return (up_capture(returns, factor_returns, **kwargs) / down_capture(returns, factor_returns, **kwargs)) def up_alpha_beta(returns, factor_returns, **kwargs): """ Computes alpha and beta for periods when the benchmark return is positive. Parameters ---------- see documentation for `alpha_beta`. Returns ------- float Alpha. float Beta. """ return up(returns, factor_returns, function=alpha_beta_aligned, **kwargs) def down_alpha_beta(returns, factor_returns, **kwargs): """ Computes alpha and beta for periods when the benchmark return is negative. Parameters ---------- see documentation for `alpha_beta`. Returns ------- alpha : float beta : float """ return down(returns, factor_returns, function=alpha_beta_aligned, **kwargs) def roll_up_capture(returns, factor_returns, window=10, **kwargs): """ Computes the up capture measure over a rolling window. see documentation for :func:`~empyrical.stats.up_capture`. (pass all args, kwargs required) Parameters ---------- returns : pd.Series or np.ndarray Daily returns of the strategy, noncumulative. - See full explanation in :func:`~empyrical.stats.cum_returns`. factor_returns : pd.Series or np.ndarray Noncumulative returns of the factor to which beta is computed. Usually a benchmark such as the market. - This is in the same style as returns. window : int, required Size of the rolling window in terms of the periodicity of the data. - eg window = 60, periodicity=DAILY, represents a rolling 60 day window """ return roll(returns, factor_returns, window=window, function=up_capture, **kwargs) def roll_down_capture(returns, factor_returns, window=10, **kwargs): """ Computes the down capture measure over a rolling window. see documentation for :func:`~empyrical.stats.down_capture`. (pass all args, kwargs required) Parameters ---------- returns : pd.Series or np.ndarray Daily returns of the strategy, noncumulative. - See full explanation in :func:`~empyrical.stats.cum_returns`. factor_returns : pd.Series or np.ndarray Noncumulative returns of the factor to which beta is computed. Usually a benchmark such as the market. - This is in the same style as returns. window : int, required Size of the rolling window in terms of the periodicity of the data. - eg window = 60, periodicity=DAILY, represents a rolling 60 day window """ return roll(returns, factor_returns, window=window, function=down_capture, **kwargs) def roll_up_down_capture(returns, factor_returns, window=10, **kwargs): """ Computes the up/down capture measure over a rolling window. see documentation for :func:`~empyrical.stats.up_down_capture`. (pass all args, kwargs required) Parameters ---------- returns : pd.Series or np.ndarray Daily returns of the strategy, noncumulative. - See full explanation in :func:`~empyrical.stats.cum_returns`. factor_returns : pd.Series or np.ndarray Noncumulative returns of the factor to which beta is computed. Usually a benchmark such as the market. - This is in the same style as returns. window : int, required Size of the rolling window in terms of the periodicity of the data. - eg window = 60, periodicity=DAILY, represents a rolling 60 day window """ return roll(returns, factor_returns, window=window, function=up_down_capture, **kwargs) def value_at_risk(returns, cutoff=0.05): """ Value at risk (VaR) of a returns stream. Parameters ---------- returns : pandas.Series or 1-D numpy.array Non-cumulative daily returns. cutoff : float, optional Decimal representing the percentage cutoff for the bottom percentile of returns. Defaults to 0.05. Returns ------- VaR : float The VaR value. """ return np.percentile(returns, 100 * cutoff) def conditional_value_at_risk(returns, cutoff=0.05): """ Conditional value at risk (CVaR) of a returns stream. CVaR measures the expected single-day returns of an asset on that asset's worst performing days, where "worst-performing" is defined as falling below ``cutoff`` as a percentile of all daily returns. Parameters ---------- returns : pandas.Series or 1-D numpy.array Non-cumulative daily returns. cutoff : float, optional Decimal representing the percentage cutoff for the bottom percentile of returns. Defaults to 0.05. Returns ------- CVaR : float The CVaR value. """ # PERF: Instead of using the 'value_at_risk' function to find the cutoff # value, which requires a call to numpy.percentile, determine the cutoff # index manually and partition out the lowest returns values. The value at # the cutoff index should be included in the partition. cutoff_index = int((len(returns) - 1) * cutoff) return np.mean(np.partition(returns, cutoff_index)[:cutoff_index + 1]) SIMPLE_STAT_FUNCS = [ cum_returns_final, annual_return, annual_volatility, sharpe_ratio, calmar_ratio, stability_of_timeseries, max_drawdown, omega_ratio, sortino_ratio, stats.skew, stats.kurtosis, tail_ratio, cagr, value_at_risk, conditional_value_at_risk, ] FACTOR_STAT_FUNCS = [ excess_sharpe, alpha, beta, beta_fragility_heuristic, gpd_risk_estimates, capture, up_capture, down_capture ] ================================================ FILE: EigenLedger/modules/empyrical/utils.py ================================================ # # Copyright 2018 Quantopian, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from datetime import datetime from functools import wraps from os import makedirs, environ from os.path import expanduser, join, getmtime, isdir import errno import warnings import numpy as np from numpy.lib.stride_tricks import as_strided import pandas as pd from pandas.tseries.offsets import BDay try: from pandas_datareader import data as web except ImportError: msg = ("Unable to import pandas_datareader. Suppressing import error and " "continuing. All data reading functionality will raise errors; but " "has been deprecated and will be removed in a later version.") warnings.warn(msg) from .deprecate import deprecated DATAREADER_DEPRECATION_WARNING = \ ("Yahoo and Google Finance have suffered large API breaks with no " "stable replacement. As a result, any data reading functionality " "in empyrical has been deprecated and will be removed in a future " "version. See README.md for more details: " "\n\n" "\thttps://github.com/quantopian/pyfolio/blob/master/README.md") try: # fast versions import bottleneck as bn def _wrap_function(f): @wraps(f) def wrapped(*args, **kwargs): out = kwargs.pop('out', None) data = f(*args, **kwargs) if out is None: out = data else: out[()] = data return out return wrapped nanmean = _wrap_function(bn.nanmean) nanstd = _wrap_function(bn.nanstd) nansum = _wrap_function(bn.nansum) nanmax = _wrap_function(bn.nanmax) nanmin = _wrap_function(bn.nanmin) nanargmax = _wrap_function(bn.nanargmax) nanargmin = _wrap_function(bn.nanargmin) except ImportError: # slower numpy nanmean = np.nanmean nanstd = np.nanstd nansum = np.nansum nanmax = np.nanmax nanmin = np.nanmin nanargmax = np.nanargmax nanargmin = np.nanargmin def roll(*args, **kwargs): """ Calculates a given statistic across a rolling time period. Parameters ---------- returns : pd.Series or np.ndarray Daily returns of the strategy, noncumulative. - See full explanation in :func:`~empyrical.stats.cum_returns`. factor_returns (optional): float / series Benchmark return to compare returns against. function: the function to run for each rolling window. window (keyword): int the number of periods included in each calculation. (other keywords): other keywords that are required to be passed to the function in the 'function' argument may also be passed in. Returns ------- np.ndarray, pd.Series depends on input type ndarray(s) ==> ndarray Series(s) ==> pd.Series A Series or ndarray of the results of the stat across the rolling window. """ func = kwargs.pop('function') window = kwargs.pop('window') if len(args) > 2: raise ValueError("Cannot pass more than 2 return sets") if len(args) == 2: if not isinstance(args[0], type(args[1])): raise ValueError("The two returns arguments are not the same.") if isinstance(args[0], np.ndarray): return _roll_ndarray(func, window, *args, **kwargs) return _roll_pandas(func, window, *args, **kwargs) def up(returns, factor_returns, **kwargs): """ Calculates a given statistic filtering only positive factor return periods. Parameters ---------- returns : pd.Series or np.ndarray Daily returns of the strategy, noncumulative. - See full explanation in :func:`~empyrical.stats.cum_returns`. factor_returns (optional): float / series Benchmark return to compare returns against. function: the function to run for each rolling window. (other keywords): other keywords that are required to be passed to the function in the 'function' argument may also be passed in. Returns ------- Same as the return of the function """ func = kwargs.pop('function') returns = returns[factor_returns > 0] factor_returns = factor_returns[factor_returns > 0] return func(returns, factor_returns, **kwargs) def down(returns, factor_returns, **kwargs): """ Calculates a given statistic filtering only negative factor return periods. Parameters ---------- returns : pd.Series or np.ndarray Daily returns of the strategy, noncumulative. - See full explanation in :func:`~empyrical.stats.cum_returns`. factor_returns (optional): float / series Benchmark return to compare returns against. function: the function to run for each rolling window. (other keywords): other keywords that are required to be passed to the function in the 'function' argument may also be passed in. Returns ------- Same as the return of the 'function' """ func = kwargs.pop('function') returns = returns[factor_returns < 0] factor_returns = factor_returns[factor_returns < 0] return func(returns, factor_returns, **kwargs) def _roll_ndarray(func, window, *args, **kwargs): data = [] for i in range(window, len(args[0]) + 1): rets = [s[i-window:i] for s in args] data.append(func(*rets, **kwargs)) return np.array(data) def _roll_pandas(func, window, *args, **kwargs): data = {} index_values = [] for i in range(window, len(args[0]) + 1): rets = [s.iloc[i-window:i] for s in args] index_value = args[0].index[i - 1] index_values.append(index_value) data[index_value] = func(*rets, **kwargs) return pd.Series(data, index=type(args[0].index)(index_values)) @deprecated(msg=DATAREADER_DEPRECATION_WARNING) def cache_dir(environ=environ): try: return environ['EMPYRICAL_CACHE_DIR'] except KeyError: return join( environ.get( 'XDG_CACHE_HOME', expanduser('~/.cache/'), ), 'empyrical', ) @deprecated(msg=DATAREADER_DEPRECATION_WARNING) def data_path(name): return join(cache_dir(), name) @deprecated(msg=DATAREADER_DEPRECATION_WARNING) def ensure_directory(path): """ Ensure that a directory named "path" exists. """ try: makedirs(path) except OSError as exc: if exc.errno != errno.EEXIST or not isdir(path): raise def get_utc_timestamp(dt): """ Returns the Timestamp/DatetimeIndex with either localized or converted to UTC. Parameters ---------- dt : Timestamp/DatetimeIndex the date(s) to be converted Returns ------- same type as input date(s) converted to UTC """ dt = pd.to_datetime(dt) try: dt = dt.tz_localize('UTC') except TypeError: dt = dt.tz_convert('UTC') return dt _1_bday = BDay() def _1_bday_ago(): return pd.Timestamp.now().normalize() - _1_bday @deprecated(msg=DATAREADER_DEPRECATION_WARNING) def get_fama_french(): """ Retrieve Fama-French factors via pandas-datareader Returns ------- pandas.DataFrame Percent change of Fama-French factors """ start = '1/1/1970' research_factors = web.DataReader('F-F_Research_Data_Factors_daily', 'famafrench', start=start)[0] momentum_factor = web.DataReader('F-F_Momentum_Factor_daily', 'famafrench', start=start)[0] five_factors = research_factors.join(momentum_factor).dropna() five_factors /= 100. five_factors.index = five_factors.index.tz_localize('utc') five_factors.columns = five_factors.columns.str.strip() return five_factors @deprecated(msg=DATAREADER_DEPRECATION_WARNING) def get_returns_cached(filepath, update_func, latest_dt, **kwargs): """ Get returns from a cached file if the cache is recent enough, otherwise, try to retrieve via a provided update function and update the cache file. Parameters ---------- filepath : str Path to cached csv file update_func : function Function to call in case cache is not up-to-date. latest_dt : pd.Timestamp (tz=UTC) Latest datetime required in csv file. **kwargs : Keyword arguments Optional keyword arguments will be passed to update_func() Returns ------- pandas.DataFrame DataFrame containing returns """ update_cache = False try: mtime = getmtime(filepath) except OSError as e: if e.errno != errno.ENOENT: raise update_cache = True else: file_dt = pd.Timestamp(mtime, unit='s') if latest_dt.tzinfo: file_dt = file_dt.tz_localize('utc') if file_dt < latest_dt: update_cache = True else: returns = pd.read_csv(filepath, index_col=0, parse_dates=True) returns.index = returns.index.tz_localize("UTC") if update_cache: returns = update_func(**kwargs) try: ensure_directory(cache_dir()) except OSError as e: warnings.warn( 'could not update cache: {}. {}: {}'.format( filepath, type(e).__name__, e, ), UserWarning, ) try: returns.to_csv(filepath) except OSError as e: warnings.warn( 'could not update cache {}. {}: {}'.format( filepath, type(e).__name__, e, ), UserWarning, ) return returns @deprecated(msg=DATAREADER_DEPRECATION_WARNING) def load_portfolio_risk_factors(filepath_prefix=None, start=None, end=None): """ Load risk factors Mkt-Rf, SMB, HML, Rf, and UMD. Data is stored in HDF5 file. If the data is more than 2 days old, redownload from Dartmouth. Returns ------- five_factors : pd.DataFrame Risk factors timeseries. """ if start is None: start = '1/1/1970' if end is None: end = _1_bday_ago() start = get_utc_timestamp(start) end = get_utc_timestamp(end) if filepath_prefix is None: filepath = data_path('factors.csv') else: filepath = filepath_prefix five_factors = get_returns_cached(filepath, get_fama_french, end) return five_factors.loc[start:end] @deprecated(msg=DATAREADER_DEPRECATION_WARNING) def get_treasury_yield(start=None, end=None, period='3MO'): """ Load treasury yields from FRED. Parameters ---------- start : date, optional Earliest date to fetch data for. Defaults to earliest date available. end : date, optional Latest date to fetch data for. Defaults to latest date available. period : {'1MO', '3MO', '6MO', 1', '5', '10'}, optional Which maturity to use. Returns ------- pd.Series Annual treasury yield for every day. """ if start is None: start = '1/1/1970' if end is None: end = _1_bday_ago() treasury = web.DataReader("DGS3{}".format(period), "fred", start, end) treasury = treasury.ffill() return treasury @deprecated(msg=DATAREADER_DEPRECATION_WARNING) def get_symbol_returns_from_yahoo(symbol, start=None, end=None): """ Wrapper for pandas.io.data.get_data_yahoo(). Retrieves prices for symbol from yahoo and computes returns based on adjusted closing prices. Parameters ---------- symbol : str Symbol name to load, e.g. 'SPY' start : pandas.Timestamp compatible, optional Start date of time period to retrieve end : pandas.Timestamp compatible, optional End date of time period to retrieve Returns ------- pandas.DataFrame Returns of symbol in requested period. """ try: px = web.get_data_yahoo(symbol, start=start, end=end) px['date'] = pd.to_datetime(px['date']) px.set_index('date', drop=False, inplace=True) rets = px[['adjclose']].pct_change().dropna() except Exception as e: warnings.warn( 'Yahoo Finance read failed: {}, falling back to Google'.format(e), UserWarning) px = web.get_data_google(symbol, start=start, end=end) rets = px[['Close']].pct_change().dropna() rets.index = rets.index.tz_localize("UTC") rets.columns = [symbol] return rets @deprecated(msg=DATAREADER_DEPRECATION_WARNING) def default_returns_func(symbol, start=None, end=None): """ Gets returns for a symbol. Queries Yahoo Finance. Attempts to cache SPY. Parameters ---------- symbol : str Ticker symbol, e.g. APPL. start : date, optional Earliest date to fetch data for. Defaults to earliest date available. end : date, optional Latest date to fetch data for. Defaults to latest date available. Returns ------- pd.Series Daily returns for the symbol. - See full explanation in tears.create_full_tear_sheet (returns). """ if start is None: start = '1/1/1970' if end is None: end = _1_bday_ago() start = get_utc_timestamp(start) end = get_utc_timestamp(end) if symbol == 'SPY': filepath = data_path('spy.csv') rets = get_returns_cached(filepath, get_symbol_returns_from_yahoo, end, symbol='SPY', start='1/1/1970', end=datetime.now()) rets = rets[start:end] else: rets = get_symbol_returns_from_yahoo(symbol, start=start, end=end) return rets[symbol] def rolling_window(array, length, mutable=False): """ Restride an array of shape (X_0, ... X_N) into an array of shape (length, X_0 - length + 1, ... X_N) where each slice at index i along the first axis is equivalent to result[i] = array[length * i:length * (i + 1)] Parameters ---------- array : np.ndarray The base array. length : int Length of the synthetic first axis to generate. mutable : bool, optional Return a mutable array? The returned array shares the same memory as the input array. This means that writes into the returned array affect ``array``. The returned array also uses strides to map the same values to multiple indices. Writes to a single index may appear to change many values in the returned array. Returns ------- out : np.ndarray Example ------- >>> from numpy import arange >>> a = arange(25).reshape(5, 5) >>> a array([[ 0, 1, 2, 3, 4], [ 5, 6, 7, 8, 9], [10, 11, 12, 13, 14], [15, 16, 17, 18, 19], [20, 21, 22, 23, 24]]) >>> rolling_window(a, 2) array([[[ 0, 1, 2, 3, 4], [ 5, 6, 7, 8, 9]], [[ 5, 6, 7, 8, 9], [10, 11, 12, 13, 14]], [[10, 11, 12, 13, 14], [15, 16, 17, 18, 19]], [[15, 16, 17, 18, 19], [20, 21, 22, 23, 24]]]) """ if not length: raise ValueError("Can't have 0-length window") orig_shape = array.shape if not orig_shape: raise IndexError("Can't restride a scalar.") elif orig_shape[0] < length: raise IndexError( "Can't restride array of shape {shape} with" " a window length of {len}".format( shape=orig_shape, len=length, ) ) num_windows = (orig_shape[0] - length + 1) new_shape = (num_windows, length) + orig_shape[1:] new_strides = (array.strides[0],) + array.strides out = as_strided(array, new_shape, new_strides) out.setflags(write=mutable) return out ================================================ FILE: EigenLedger/run.py ================================================ from main import portfolio_analysis, Engine import pandas as pd # Define custom data portfolio_data = pd.DataFrame({ "AAPL": [145.0, 147.0, 149.0], "MSFT": [240.0, 242.0, 245.0], "GOOGL": [2700.0, 2725.0, 2750.0], }, index=pd.to_datetime(["2023-01-01", "2023-01-02", "2023-01-03"])) benchmark_data = pd.DataFrame({ "TGT": [420.0, 425.0, 430.0], }, index=pd.to_datetime(["2023-01-01", "2023-01-02", "2023-01-03"])) portfolio = Engine( start_date="2023-01-01", portfolio=["AAPL", "MSFT", "GOOGL"], weights=[0.4, 0.3, 0.3], data=portfolio_data, benchmark=["TGT"], benchmark_data=benchmark_data ) # Fetch benchmark data and analyze portfolio.fetch_benchmark_data() portfolio_analysis(portfolio) ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "{}" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright 2024 Santosh P. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: README.md ================================================ #### 📢 Announcement Good news! You can now use a patched version of the library [empyrical](https://github.com/quantopian/empyrical) through EigenLedger! 🎉
👉 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).
# By Investors, For Investors.




![](https://img.shields.io/badge/Downloads-245k-brightgreen) ![](https://img.shields.io/badge/license-MIT-orange) ![](https://img.shields.io/badge/version-2.1.6-blueviolet) ![](https://img.shields.io/badge/language-python🐍-blue) ![](https://img.shields.io/badge/Open%20source-💜-white) [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/drive/1TyNgudyFcsgob7o49PwfDJHLaHvluxaU?usp=sharing)

Want to read this in **Mandarin 🇨🇳**? Click [**here**](README_CN.md) EigenLedger (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**. EigenLedger **empowers portfolio management** by bringing the best of **performance and risk analysis** in an **easy-to-understand**, **flexible** and **powerful framework**. With 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**.

| Table of Contents 📖 | | -- | 1. [Installation](#installation) | | 2. [Documentation](#documentation) | | 3. [Quickstart](#quickstart) | | 4. [Contribution and Issues](#contribution-and-issues) | | 5. [Contributors](#contributors) | | 6. [Contact](#contact) | | 7. [License](#license) |
## Installation You can install EigenLedger using pip: ``` pip install EigenLedger ``` For a better experience, **we advise you to use EigenLedger on a notebook** (e.g., Jupyter, Google Colab) _Note: macOS users will need to install [Xcode Command Line Tools](https://osxdaily.com/2014/02/12/install-command-line-tools-mac-os-x/)._ _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))_ ## Documentation Here 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. ## Quickstart ```py from EigenLedger import portfolio_analysis, Engine portfolio = Engine( start_date = "2018-08-01", portfolio = ["BABA", "PDD", "KO", "AMD","^IXIC"], weights = [0.2, 0.2, 0.2, 0.2, 0.2], # equal weighting is set by default benchmark = ["SPY"] # SPY is set by default ) portfolio_analysis(portfolio) ```
![image](https://user-images.githubusercontent.com/61618641/126879140-ea03ff17-a7c6-481a-bb3e-61c055b31267.png) ![image](https://user-images.githubusercontent.com/61618641/126879203-4390813c-a4f2-41b9-916b-e03dd8bafffb.png) ![image](https://user-images.githubusercontent.com/61618641/128025087-04afed7e-96ab-4730-9bd8-98f5491b2b5d.png) ![image](https://user-images.githubusercontent.com/61618641/126879204-01fe1eca-00b8-438e-b489-0213535dd31b.png) ![image](https://user-images.githubusercontent.com/61618641/126879210-9fd61e2b-01ab-4bfd-b679-3b1867d9302d.png) ![image](https://user-images.githubusercontent.com/61618641/126879215-e24c929a-55be-4912-8e2c-043e31ff2a95.png) ![image](https://user-images.githubusercontent.com/61618641/126879221-455b8ffa-c958-4ac9-ae98-d15b4c5f0826.png) ![image](https://user-images.githubusercontent.com/61618641/126879222-08906643-16db-441e-a099-7ac3b00bdbd7.png) ![image](https://user-images.githubusercontent.com/61618641/126879223-f1116dc3-cceb-493c-93b3-2d3810cae789.png) ![image](https://user-images.githubusercontent.com/61618641/126879225-dc879b71-2070-46ed-a8ad-e90880050be8.png) ![image](https://user-images.githubusercontent.com/61618641/126879297-cb78743a-6d43-465b-8021-d4b62a659828.png)
## Stargazers over time
![追星族的时间](https://starchart.cc/ssantoshp/empyrial.svg)
## Contribution and Issues EigenLedger uses GitHub to host its source code. *Learn more about the [Github flow](https://docs.github.com/en/get-started/quickstart/github-flow).* For larger changes (e.g., new feature request, large refactoring), please open an issue to discuss first. * If you wish to create a new Issue, then [click here to create a new issue](https://github.com/ssantoshp/EigenLedger/issues/new/choose). Smaller improvements (e.g., document improvements, bugfixes) can be handled by the Pull Request process of GitHub: [pull requests](https://github.com/ssantoshp/EigenLedger/pulls). * To contribute to the code, you will need to do the following: * [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. * [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``` *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/)* * [Create a Pull Request](https://github.com/ssantoshp/EigenLedger/pulls) using **your fork** as the `compare head repository`. You contributions will be reviewed, potentially modified, and hopefully merged into EigenLedger. ## Contributors Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): [![All Contributors](https://img.shields.io/badge/all_contributors-11-orange.svg?style=flat-square)](#contributors-)

Brendan Glancy

💻 🐛

Renan Lopes

💻 🐛

Mark Thebault

💻

Diego Alvarez

💻🐛

Rakesh Bhat

💻

Anh Le

🐛

Tony Zhang

💻

Ikko Ashimine

✒️

QuantNomad

📹

Buckley

✒️💻

Adam Nelsson

💻

Ranjan Grover

🐛💻
This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. **Contributions of any kind are welcome!** ## Credit This library has also been made possible because of the work of these incredible people: - [**Ran Aroussi**](https://github.com/ranaroussi) for the [**Quantstats library**](https://github.com/ranaroussi/quantstats) - [**Robert Martin**](https://github.com/robertmartin8) for the [**PyPortfolioOpt**](https://github.com/robertmartin8/PyPortfolioOpt) ## Contact You are welcome to contact us by email at **santoshpassoubady@gmail.com** or in EigenLedger's [discussion space](https://github.com/ssantoshp/EigenLedger/discussions) ## License Apache License 2.0 ================================================ FILE: README_CN.md ================================================ #### 📢 公告 好消息!你现在可以通过 EigenLedger 使用维护的 [empyrical](https://github.com/quantopian/empyrical) 库版本了!🎉
👉 在[这里](https://eigenledger.gitbook.io/eigenledger/using-empyrical/using-empyrical)了解如何使用它,并阅读[此公告帖子](https://github.com/santoshlite/EigenLedger/discussions/128)了解更多信息。
# 投资者为投资者打造




![](https://img.shields.io/badge/Downloads-245k-brightgreen) ![](https://img.shields.io/badge/license-MIT-orange) ![](https://img.shields.io/badge/version-2.1.6-blueviolet) ![](https://img.shields.io/badge/language-python🐍-blue) ![](https://img.shields.io/badge/Open%20source-💜-white) [![在 Colab 中打开](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/drive/1TyNgudyFcsgob7o49PwfDJHLaHvluxaU?usp=sharing)

想要阅读**英文版 🇺🇸**?请点击[**这里**](README.md) EigenLedger(原名 "Empyrial")是一个基于 Python 的**开源量化投资**库,专为**金融机构**和**散户投资者**打造,正式发布于 2021 年。EigenLedger 已被**数千名金融行业人士**使用,旨在成为集**投资组合管理**、**分析**和**优化**于一体的平台。 EigenLedger 通过在一个**易于理解**、**灵活**和**强大**的框架中提供最佳的**绩效和风险分析**,**赋能投资组合管理**。 使用 EigenLedger,您可以轻松分析证券或投资组合,以**获得最佳洞察**。它主要是**Quantstats** 和 **PyPortfolioOpt** 等金融分析库的**封装器**。

| 目录 📖 | | -- | 1. [安装](#安装) | | 2. [文档](#文档) | | 3. [快速开始](#快速开始) | | 4. [贡献和问题](#贡献和问题) | | 5. [贡献者](#贡献者) | | 6. [联系方式](#联系方式) | | 7. [许可证](#许可证) |
## 安装 您可以使用 pip 安装 EigenLedger: ``` pip install EigenLedger ``` 为了获得更好的体验,**我们建议您在笔记本环境中使用 EigenLedger**(例如,Jupyter,Google Colab) _注意:macOS 用户需要安装 [Xcode 命令行工具](https://osxdaily.com/2014/02/12/install-command-line-tools-mac-os-x/)。_ _注意:Windows 用户需要安装 C++。([下载](https://visualstudio.microsoft.com/thank-you-downloading-visual-studio/?sku=BuildTools&rel=16),[安装说明](https://drive.google.com/file/d/0B4GsMXCRaSSIOWpYQkstajlYZ0tPVkNQSElmTWh1dXFaYkJr/view))_ ## 文档 这是我们的完整[文档](https://eigenledger.gitbook.io/documentation)!查看我们的完整文档,获取详细指南、所有功能,以及充分利用此库的技巧。 ## 快速开始 ```py from EigenLedger import portfolio_analysis, Engine portfolio = Engine( start_date = "2018-08-01", portfolio = ["BABA", "PDD", "KO", "AMD","^IXIC"], weights = [0.2, 0.2, 0.2, 0.2, 0.2], # 默认设置为等权重 benchmark = ["SPY"] # 默认设置为 SPY ) portfolio_analysis(portfolio) ```
![image](https://user-images.githubusercontent.com/61618641/126879140-ea03ff17-a7c6-481a-bb3e-61c055b31267.png) ![image](https://user-images.githubusercontent.com/61618641/126879203-4390813c-a4f2-41b9-916b-e03dd8bafffb.png) ![image](https://user-images.githubusercontent.com/61618641/128025087-04afed7e-96ab-4730-9bd8-98f5491b2b5d.png) ![image](https://user-images.githubusercontent.com/61618641/126879204-01fe1eca-00b8-438e-b489-0213535dd31b.png) ![image](https://user-images.githubusercontent.com/61618641/126879210-9fd61e2b-01ab-4bfd-b679-3b1867d9302d.png) ![image](https://user-images.githubusercontent.com/61618641/126879215-e24c929a-55be-4912-8e2c-043e31ff2a95.png) ![image](https://user-images.githubusercontent.com/61618641/126879221-455b8ffa-c958-4ac9-ae98-d15b4c5f0826.png) ![image](https://user-images.githubusercontent.com/61618641/126879222-08906643-16db-441e-a099-7ac3b00bdbd7.png) ![image](https://user-images.githubusercontent.com/61618641/126879223-f1116dc3-cceb-493c-93b3-2d3810cae789.png) ![image](https://user-images.githubusercontent.com/61618641/126879225-dc879b71-2070-46ed-a8ad-e90880050be8.png) ![image](https://user-images.githubusercontent.com/61618641/126879297-cb78743a-6d43-465b-8021-d4b62a659828.png)
## 星标数随时间变化
![星标数随时间变化](https://starchart.cc/ssantoshp/empyrial.svg)
## 贡献和问题 EigenLedger 使用 GitHub 来托管其源代码。*了解更多关于 [GitHub 流程](https://docs.github.com/en/get-started/quickstart/github-flow)的信息。* 对于较大的更改(例如,新功能请求、大型重构),请先打开一个 issue 进行讨论。 * 如果您想创建一个新的 Issue,请[点击这里创建新 Issue](https://github.com/ssantoshp/EigenLedger/issues/new/choose)。 较小的改进(例如,文档改进、错误修复)可以通过 GitHub 的 Pull Request 流程处理:[拉取请求](https://github.com/ssantoshp/EigenLedger/pulls)。 * 要贡献代码,您需要执行以下操作: * [Fork](https://docs.github.com/en/get-started/quickstart/fork-a-repo#forking-a-repository) [EigenLedger](https://github.com/ssantoshp/EigenLedger) - 点击本页面右上角的 **Fork** 按钮。 * [克隆您自己的 fork](https://docs.github.com/en/get-started/quickstart/fork-a-repo#cloning-your-forked-repository)。例如,```git clone https://github.com/ssantoshp/EigenLedger.git``` *如果您的 fork 过期了,您需要手动同步您的 fork:[同步方法](https://help.github.com/articles/syncing-a-fork/)* * 使用您的 **fork** 作为 `compare head repository`,[创建一个 Pull Request](https://github.com/ssantoshp/EigenLedger/pulls)。 您的贡献将被审核,可能会被修改,并希望合并到 EigenLedger 中。 ## 贡献者 感谢这些了不起的人([emoji 说明](https://allcontributors.org/docs/en/emoji-key)): [![All Contributors](https://img.shields.io/badge/all_contributors-11-orange.svg?style=flat-square)](#contributors-)

Brendan Glancy

💻 🐛

Renan Lopes

💻 🐛

Mark Thebault

💻

Diego Alvarez

💻🐛

Rakesh Bhat

💻

Anh Le

🐛

Tony Zhang

💻

Ikko Ashimine

✒️

QuantNomad

📹

Buckley

✒️💻

Adam Nelsson

💻

Ranjan Grover

🐛💻
本项目遵循 [all-contributors](https://github.com/all-contributors/all-contributors) 规范。**欢迎任何形式的贡献!** ## 致谢 由于这些令人难以置信的人的工作,这个库才成为可能: - [**Ran Aroussi**](https://github.com/ranaroussi) 的 [**Quantstats 库**](https://github.com/ranaroussi/quantstats) - [**Robert Martin**](https://github.com/robertmartin8) 的 [**PyPortfolioOpt**](https://github.com/robertmartin8/PyPortfolioOpt) ## 联系方式 欢迎通过电子邮件 **santoshpassoubady@gmail.com** 或在 EigenLedger 的[讨论空间](https://github.com/ssantoshp/EigenLedger/discussions)与我们联系 ## 许可证 Apache 许可证 2.0 ================================================ FILE: pyproject.toml ================================================ [tool.poetry] name = "EigenLedger" authors = ["Santosh "] version = "2.1.6" description = "An Open Source Portfolio Management Framework for Everyone 投资组合管理" readme = "README.md" license = "LICENSE" homepage = "https://github.com/ssantoshp/EigenLedger" [tool.poetry.dependencies] python = ">=3.0" numpy = "^1.21.0" matplotlib = "^3.4.0" quantstats = "^0.0.62" yfinance = "^0.1.0" ipython = "^7.16.0" fpdf = "^1.7.2" pyportfolioopt = "^1.4.0" [[tool.poetry.packages]] include = "EigenLedger" [build-system] requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api"