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 `<https://www.sunrisecapital.com/wp-content/uploads/2014/06/Futures_
Mag_Sortino_0213.pdf>`__ 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 `<https://www.sunrisecapital.com/wp-content/uploads/2014/06/Futures_
Mag_Sortino_0213.pdf>`__ 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 <https://link.springer.com/article/10.1007/s10614-006-9025-7>`
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 <https://link.springer.com/article/10.1007/s10614-006-9025-7>`
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]],
<BLANKLINE>
[[ 5, 6, 7, 8, 9],
[10, 11, 12, 13, 14]],
<BLANKLINE>
[[10, 11, 12, 13, 14],
[15, 16, 17, 18, 19]],
<BLANKLINE>
[[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! 🎉
<br>
👉 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).
<br>
# By Investors, For Investors.
<br>
<div align="center">
<img src="https://github.com/user-attachments/assets/470f1d59-09c6-4b95-af7e-f142764d8195"/>
<br><br><br><br>





[](https://colab.research.google.com/drive/1TyNgudyFcsgob7o49PwfDJHLaHvluxaU?usp=sharing)
</div>
<br>
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**.
<br>
<br>
<div align="center">
| 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) |
</div>
## 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)
```
<div align="center">











</div>
## Stargazers over time
<div align="center">

</div>
## 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)):
[](#contributors-)
<table>
<tr>
<td align="center"><a href="https://github.com/BrendanGlancy"><img src="https://avatars.githubusercontent.com/u/61941978?v=4" width="100px;" alt=""/><br /><sub><b>Brendan Glancy</b></sub></a><br /><a title="Code">💻</a> <a title="Bug report">🐛</a></td>
<td align="center"><a href="https://github.com/rslopes"><img src="https://avatars.githubusercontent.com/u/24928343?v=4" width="100px;" alt=""/><br /><sub><b>Renan Lopes</b></sub></a><br /><a title="Code">💻</a> <a title="Bug report">🐛</a></td>
<td align="center"><a href="https://github.com/markthebault"><img src="https://avatars.githubusercontent.com/u/3846664?v=4" width="100px;" alt=""/><br /><sub><b>Mark Thebault</b></sub></a><br /><a title="Code">💻</a></td>
<td align="center"><a href="https://github.com/diegodalvarez"><img src="https://avatars.githubusercontent.com/u/48641554?v=4" width="100px;" alt=""/><br /><sub><b>Diego Alvarez</b></sub></a><br /><a title="Code">💻🐛</a></td>
<td align="center"><a href="https://github.com/rakeshbhat9"><img src="https://avatars.githubusercontent.com/u/11472305?v=4" width="100px;" alt=""/><br /><sub><b>Rakesh Bhat</b></sub></a><br /><a title="Code">💻</a></td>
<td align="center"><a href="https://github.com/Haizzz"><img src="https://avatars.githubusercontent.com/u/5275680?v=4" width="100px;" alt=""/><br /><sub><b>Anh Le</b></sub></a><br /><a title="Bug report">🐛</a></td>
<td align="center"><a href="https://github.com/TonyZhangkz"><img src="https://avatars.githubusercontent.com/u/65281213?v=4" width="100px;" alt=""/><br /><sub><b>Tony Zhang</b></sub></a><br /><a title="Code">💻</a></td>
<td align="center"><a href="https://github.com/eltociear"><img src="https://avatars.githubusercontent.com/u/22633385?v=4" width="100px;" alt=""/><br /><sub><b>Ikko Ashimine</b></sub></a><br /><a title="Code">✒️</a></td>
<td align="center"><a href="https://www.youtube.com/watch?v=-4qx3tbtTgs"><img src="https://avatars.githubusercontent.com/u/50767660?v=4" width="100px;" alt=""/><br /><sub><b>QuantNomad</b></sub></a><br /><a title="Code">📹</a></td>
<td align="center"><a href="https://github.com/buckleyc"><img src="https://avatars.githubusercontent.com/u/4175900?v=4" width="100px;" alt=""/><br /><sub><b>Buckley</b></sub></a><br /><a title="Code">✒️💻</a></td>
<td align="center"><a href="https://github.com/agn35"><img src="https://lh3.googleusercontent.com/a-/AOh14GhXGFHHpVQTL2r23oEXFssH0f7RyoGDihrS_HmT=s48" width="100px;" alt=""/><br /><sub><b>Adam Nelsson</b></sub></a><br /><a title="Code">💻</a></td>
<td align="center"><a href="https://github.com/rgleavenworth"><img src="https://avatars.githubusercontent.com/u/87843950?v=4" width="100px;" alt=""/><br /><sub><b>Ranjan Grover</b></sub></a><br /><a title="Code">🐛💻</a></td>
</tr>
</table>
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) 库版本了!🎉
<br>
👉 在[这里](https://eigenledger.gitbook.io/eigenledger/using-empyrical/using-empyrical)了解如何使用它,并阅读[此公告帖子](https://github.com/santoshlite/EigenLedger/discussions/128)了解更多信息。
<br>
# 投资者为投资者打造
<br>
<div align="center">
<img src="https://github.com/user-attachments/assets/470f1d59-09c6-4b95-af7e-f142764d8195"/>
<br><br><br><br>





[](https://colab.research.google.com/drive/1TyNgudyFcsgob7o49PwfDJHLaHvluxaU?usp=sharing)
</div>
<br>
想要阅读**英文版 🇺🇸**?请点击[**这里**](README.md)
EigenLedger(原名 "Empyrial")是一个基于 Python 的**开源量化投资**库,专为**金融机构**和**散户投资者**打造,正式发布于 2021 年。EigenLedger 已被**数千名金融行业人士**使用,旨在成为集**投资组合管理**、**分析**和**优化**于一体的平台。
EigenLedger 通过在一个**易于理解**、**灵活**和**强大**的框架中提供最佳的**绩效和风险分析**,**赋能投资组合管理**。
使用 EigenLedger,您可以轻松分析证券或投资组合,以**获得最佳洞察**。它主要是**Quantstats** 和 **PyPortfolioOpt** 等金融分析库的**封装器**。
<br>
<br>
<div align="center">
| 目录 📖 |
| --
| 1. [安装](#安装) |
| 2. [文档](#文档) |
| 3. [快速开始](#快速开始) |
| 4. [贡献和问题](#贡献和问题) |
| 5. [贡献者](#贡献者) |
| 6. [联系方式](#联系方式) |
| 7. [许可证](#许可证) |
</div>
## 安装
您可以使用 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)
```
<div align="center">











</div>
## 星标数随时间变化
<div align="center">

</div>
## 贡献和问题
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)):
[](#contributors-)
<table>
<tr>
<td align="center"><a href="https://github.com/BrendanGlancy"><img src="https://avatars.githubusercontent.com/u/61941978?v=4" width="100px;" alt=""/><br /><sub><b>Brendan Glancy</b></sub></a><br /><a title="Code">💻</a> <a title="Bug report">🐛</a></td>
<td align="center"><a href="https://github.com/rslopes"><img src="https://avatars.githubusercontent.com/u/24928343?v=4" width="100px;" alt=""/><br /><sub><b>Renan Lopes</b></sub></a><br /><a title="Code">💻</a> <a title="Bug report">🐛</a></td>
<td align="center"><a href="https://github.com/markthebault"><img src="https://avatars.githubusercontent.com/u/3846664?v=4" width="100px;" alt=""/><br /><sub><b>Mark Thebault</b></sub></a><br /><a title="Code">💻</a></td>
<td align="center"><a href="https://github.com/diegodalvarez"><img src="https://avatars.githubusercontent.com/u/48641554?v=4" width="100px;" alt=""/><br /><sub><b>Diego Alvarez</b></sub></a><br /><a title="Code">💻🐛</a></td>
<td align="center"><a href="https://github.com/rakeshbhat9"><img src="https://avatars.githubusercontent.com/u/11472305?v=4" width="100px;" alt=""/><br /><sub><b>Rakesh Bhat</b></sub></a><br /><a title="Code">💻</a></td>
<td align="center"><a href="https://github.com/Haizzz"><img src="https://avatars.githubusercontent.com/u/5275680?v=4" width="100px;" alt=""/><br /><sub><b>Anh Le</b></sub></a><br /><a title="Bug report">🐛</a></td>
<td align="center"><a href="https://github.com/TonyZhangkz"><img src="https://avatars.githubusercontent.com/u/65281213?v=4" width="100px;" alt=""/><br /><sub><b>Tony Zhang</b></sub></a><br /><a title="Code">💻</a></td>
<td align="center"><a href="https://github.com/eltociear"><img src="https://avatars.githubusercontent.com/u/22633385?v=4" width="100px;" alt=""/><br /><sub><b>Ikko Ashimine</b></sub></a><br /><a title="Code">✒️</a></td>
<td align="center"><a href="https://www.youtube.com/watch?v=-4qx3tbtTgs"><img src="https://avatars.githubusercontent.com/u/50767660?v=4" width="100px;" alt=""/><br /><sub><b>QuantNomad</b></sub></a><br /><a title="Code">📹</a></td>
<td align="center"><a href="https://github.com/buckleyc"><img src="https://avatars.githubusercontent.com/u/4175900?v=4" width="100px;" alt=""/><br /><sub><b>Buckley</b></sub></a><br /><a title="Code">✒️💻</a></td>
<td align="center"><a href="https://github.com/agn35"><img src="https://lh3.googleusercontent.com/a-/AOh14GhXGFHHpVQTL2r23oEXFssH0f7RyoGDihrS_HmT=s48" width="100px;" alt=""/><br /><sub><b>Adam Nelsson</b></sub></a><br /><a title="Code">💻</a></td>
<td align="center"><a href="https://github.com/rgleavenworth"><img src="https://avatars.githubusercontent.com/u/87843950?v=4" width="100px;" alt=""/><br /><sub><b>Ranjan Grover</b></sub></a><br /><a title="Code">🐛💻</a></td>
</tr>
</table>
本项目遵循 [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 <santoshpassoubadyp@gmail.com>"]
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"
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
SYMBOL INDEX (116 symbols across 6 files)
FILE: EigenLedger/main.py
class Engine (line 69) | class Engine:
method __init__ (line 70) | def __init__(
method fetch_benchmark_data (line 145) | def fetch_benchmark_data(self):
class PortfolioAnalysisResult (line 161) | class PortfolioAnalysisResult:
function get_returns (line 164) | def get_returns(stocks, wts, start_date, end_date=TODAY):
function get_returns_from_data (line 232) | def get_returns_from_data(data, wts, stocks):
function get_returns_from_benchmark_data (line 241) | def get_returns_from_benchmark_data(data, wts, stocks):
function calculate_information_ratio (line 253) | def calculate_information_ratio(returns, benchmark_returns, days=252) ->...
function graph_allocation (line 260) | def graph_allocation(my_portfolio):
function portfolio_analysis (line 273) | def portfolio_analysis(my_portfolio, rf=0.0, sigma_value=1, confidence_v...
function flatten (line 731) | def flatten(subject) -> list:
function graph_opt (line 741) | def graph_opt(my_portfolio, my_weights, pie_size, font_size, save=False):
function equal_weighting (line 752) | def equal_weighting(my_portfolio) -> list:
function efficient_frontier (line 755) | def efficient_frontier(my_portfolio, perf=True) -> list:
function hrp (line 797) | def hrp(my_portfolio, perf=True) -> list:
function mean_var (line 830) | def mean_var(my_portfolio, vol_max=0.15, perf=True) -> list:
function min_var (line 875) | def min_var(my_portfolio, perf=True) -> list:
function optimize_portfolio (line 915) | def optimize_portfolio(my_portfolio, vol_max=25, pie_size=5, font_size=14):
function check_schedule (line 979) | def check_schedule(rebalance) -> bool:
function valid_range (line 986) | def valid_range(start_date, end_date, rebalance) -> tuple:
function get_date_range (line 1009) | def get_date_range(start_date, end_date, rebalance) -> list:
function make_rebalance (line 1033) | def make_rebalance(
FILE: EigenLedger/modules/empyrical/_version.py
function get_keywords (line 20) | def get_keywords():
class VersioneerConfig (line 32) | class VersioneerConfig:
function get_config (line 36) | def get_config():
class NotThisMethod (line 50) | class NotThisMethod(Exception):
function register_vcs_handler (line 58) | def register_vcs_handler(vcs, method): # decorator
function run_command (line 69) | def run_command(commands, args, cwd=None, verbose=False, hide_stderr=Fal...
function versions_from_parentdir (line 103) | def versions_from_parentdir(parentdir_prefix, root, verbose):
function git_get_keywords (line 121) | def git_get_keywords(versionfile_abs):
function git_versions_from_keywords (line 146) | def git_versions_from_keywords(keywords, tag_prefix, verbose):
function git_pieces_from_vcs (line 192) | def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_comma...
function plus_or_dot (line 275) | def plus_or_dot(pieces):
function render_pep440 (line 282) | def render_pep440(pieces):
function render_pep440_pre (line 307) | def render_pep440_pre(pieces):
function render_pep440_post (line 323) | def render_pep440_post(pieces):
function render_pep440_old (line 350) | def render_pep440_old(pieces):
function render_git_describe (line 372) | def render_git_describe(pieces):
function render_git_describe_long (line 392) | def render_git_describe_long(pieces):
function render (line 412) | def render(pieces, style):
function get_versions (line 442) | def get_versions():
FILE: EigenLedger/modules/empyrical/deprecate.py
function deprecated (line 20) | def deprecated(msg=None, stacklevel=2):
FILE: EigenLedger/modules/empyrical/perf_attrib.py
function perf_attrib (line 5) | def perf_attrib(returns,
function compute_exposures (line 120) | def compute_exposures(positions, factor_loadings):
FILE: EigenLedger/modules/empyrical/stats.py
function _create_unary_vectorized_roll_function (line 31) | def _create_unary_vectorized_roll_function(function):
function _create_binary_vectorized_roll_function (line 77) | def _create_binary_vectorized_roll_function(function):
function _flatten (line 130) | def _flatten(arr):
function _adjust_returns (line 134) | def _adjust_returns(returns, adjustment_factor):
function annualization_factor (line 153) | def annualization_factor(period, annualization):
function simple_returns (line 193) | def simple_returns(prices):
function cum_returns (line 219) | def cum_returns(returns, starting_value=0, out=None):
function cum_returns_final (line 280) | def cum_returns_final(returns, starting_value=0):
function aggregate_returns (line 316) | def aggregate_returns(returns, convert_to):
function max_drawdown (line 352) | def max_drawdown(returns, out=None):
function annual_return (line 408) | def annual_return(returns, period=DAILY, annualization=None):
function cagr (line 450) | def cagr(returns, period=DAILY, annualization=None):
function annual_volatility (line 487) | def annual_volatility(returns,
function calmar_ratio (line 548) | def calmar_ratio(returns, period=DAILY, annualization=None):
function omega_ratio (line 599) | def omega_ratio(returns, risk_free=0.0, required_return=0.0,
function sharpe_ratio (line 652) | def sharpe_ratio(returns,
function sortino_ratio (line 727) | def sortino_ratio(returns,
function downside_risk (line 811) | def downside_risk(returns,
function excess_sharpe (line 894) | def excess_sharpe(returns, factor_returns, out=None):
function _to_pandas (line 946) | def _to_pandas(ob):
function _aligned_series (line 972) | def _aligned_series(*many_series):
function alpha_beta (line 1004) | def alpha_beta(returns,
function roll_alpha_beta (line 1058) | def roll_alpha_beta(returns, factor_returns, window=10, **kwargs):
function alpha_beta_aligned (line 1086) | def alpha_beta_aligned(returns,
function alpha (line 1154) | def alpha(returns,
function alpha_aligned (line 1219) | def alpha_aligned(returns,
function beta (line 1314) | def beta(returns, factor_returns, risk_free=0.0, out=None):
function beta_aligned (line 1352) | def beta_aligned(returns, factor_returns, risk_free=0.0, out=None):
function stability_of_timeseries (line 1471) | def stability_of_timeseries(returns):
function tail_ratio (line 1501) | def tail_ratio(returns):
function capture (line 1531) | def capture(returns, factor_returns, period=DAILY):
function beta_fragility_heuristic (line 1565) | def beta_fragility_heuristic(returns, factor_returns):
function beta_fragility_heuristic_aligned (line 1602) | def beta_fragility_heuristic_aligned(returns, factor_returns):
function gpd_risk_estimates (line 1675) | def gpd_risk_estimates(returns, var_p=0.01):
function gpd_risk_estimates_aligned (line 1714) | def gpd_risk_estimates_aligned(returns, var_p=0.01):
function gpd_es_calculator (line 1792) | def gpd_es_calculator(var_estimate, threshold, scale_param,
function gpd_var_calculator (line 1804) | def gpd_var_calculator(threshold, scale_param, shape_param,
function gpd_loglikelihood_minimizer_aligned (line 1816) | def gpd_loglikelihood_minimizer_aligned(price_data):
function gpd_loglikelihood_factory (line 1836) | def gpd_loglikelihood_factory(price_data):
function gpd_loglikelihood (line 1840) | def gpd_loglikelihood(params, price_data):
function gpd_loglikelihood_scale_and_shape_factory (line 1849) | def gpd_loglikelihood_scale_and_shape_factory(price_data):
function gpd_loglikelihood_scale_and_shape (line 1860) | def gpd_loglikelihood_scale_and_shape(scale, shape, price_data):
function gpd_loglikelihood_scale_only_factory (line 1872) | def gpd_loglikelihood_scale_only_factory(price_data):
function gpd_loglikelihood_scale_only (line 1878) | def gpd_loglikelihood_scale_only(scale, price_data):
function up_capture (line 1887) | def up_capture(returns, factor_returns, **kwargs):
function down_capture (line 1921) | def down_capture(returns, factor_returns, **kwargs):
function up_down_capture (line 1955) | def up_down_capture(returns, factor_returns, **kwargs):
function up_alpha_beta (line 1986) | def up_alpha_beta(returns, factor_returns, **kwargs):
function down_alpha_beta (line 2004) | def down_alpha_beta(returns, factor_returns, **kwargs):
function roll_up_capture (line 2020) | def roll_up_capture(returns, factor_returns, window=10, **kwargs):
function roll_down_capture (line 2045) | def roll_down_capture(returns, factor_returns, window=10, **kwargs):
function roll_up_down_capture (line 2070) | def roll_up_down_capture(returns, factor_returns, window=10, **kwargs):
function value_at_risk (line 2095) | def value_at_risk(returns, cutoff=0.05):
function conditional_value_at_risk (line 2115) | def conditional_value_at_risk(returns, cutoff=0.05):
FILE: EigenLedger/modules/empyrical/utils.py
function _wrap_function (line 46) | def _wrap_function(f):
function roll (line 78) | def roll(*args, **kwargs):
function up (line 121) | def up(returns, factor_returns, **kwargs):
function down (line 147) | def down(returns, factor_returns, **kwargs):
function _roll_ndarray (line 173) | def _roll_ndarray(func, window, *args, **kwargs):
function _roll_pandas (line 181) | def _roll_pandas(func, window, *args, **kwargs):
function cache_dir (line 193) | def cache_dir(environ=environ):
function data_path (line 208) | def data_path(name):
function ensure_directory (line 213) | def ensure_directory(path):
function get_utc_timestamp (line 225) | def get_utc_timestamp(dt):
function _1_bday_ago (line 252) | def _1_bday_ago():
function get_fama_french (line 257) | def get_fama_french():
function get_returns_cached (line 281) | def get_returns_cached(filepath, update_func, latest_dt, **kwargs):
function load_portfolio_risk_factors (line 349) | def load_portfolio_risk_factors(filepath_prefix=None, start=None, end=No...
function get_treasury_yield (line 379) | def get_treasury_yield(start=None, end=None, period='3MO'):
function get_symbol_returns_from_yahoo (line 413) | def get_symbol_returns_from_yahoo(symbol, start=None, end=None):
function default_returns_func (line 452) | def default_returns_func(symbol, start=None, end=None):
function rolling_window (line 498) | def rolling_window(array, length, mutable=False):
Condensed preview — 20 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (194K chars).
[
{
"path": ".github/ISSUE_TEMPLATE/bug_report.md",
"chars": 665,
"preview": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n**Describe the b"
},
{
"path": ".github/ISSUE_TEMPLATE/feature_request.md",
"chars": 605,
"preview": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n**Is your fea"
},
{
"path": ".github/workflows/python-publish.yml",
"chars": 431,
"preview": "name: Publish to PyPI.org\non:\n release:\n types: [published]\njobs:\n pypi:\n runs-on: ubuntu-latest\n steps:\n "
},
{
"path": "EigenLedger/__init__.py",
"chars": 52,
"preview": "from .main import *\nfrom .modules.empyrical import *"
},
{
"path": "EigenLedger/main.py",
"chars": 38369,
"preview": "import numpy as np\nimport pandas as pd\nimport datetime as dt\nimport quantstats as qs\nfrom IPython.display import display"
},
{
"path": "EigenLedger/modules/empyrical/.gitattributes",
"chars": 35,
"preview": "empyrical/_version.py export-subst\n"
},
{
"path": "EigenLedger/modules/empyrical/.gitignore",
"chars": 784,
"preview": "# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packagi"
},
{
"path": "EigenLedger/modules/empyrical/.travis.yml",
"chars": 1679,
"preview": "language: python\nsudo: false\n\nmatrix:\n include:\n - python: 2.7\n env: PANDAS_VERSION=0.24.2 NUMPY_VERSION=1.12.1"
},
{
"path": "EigenLedger/modules/empyrical/__init__.py",
"chars": 1878,
"preview": "#\n# Copyright 2016 Quantopian, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not us"
},
{
"path": "EigenLedger/modules/empyrical/_version.py",
"chars": 16749,
"preview": "\n# This file helps to compute a version number in source trees obtained from\n# git-archive tarball (such as those provid"
},
{
"path": "EigenLedger/modules/empyrical/deprecate.py",
"chars": 1499,
"preview": "\"\"\"Utilities for marking deprecated functions.\"\"\"\n# Copyright 2018 Quantopian, Inc.\n#\n# Licensed under the Apache Licens"
},
{
"path": "EigenLedger/modules/empyrical/perf_attrib.py",
"chars": 5834,
"preview": "from collections import OrderedDict\nimport pandas as pd\n\n\ndef perf_attrib(returns,\n positions,\n "
},
{
"path": "EigenLedger/modules/empyrical/periods.py",
"chars": 378,
"preview": "APPROX_BDAYS_PER_MONTH = 21\nAPPROX_BDAYS_PER_YEAR = 252\n\nMONTHS_PER_YEAR = 12\nWEEKS_PER_YEAR = 52\nQTRS_PER_YEAR = 4\n\nDAI"
},
{
"path": "EigenLedger/modules/empyrical/stats.py",
"chars": 66882,
"preview": "#\n# Copyright 2016 Quantopian, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not us"
},
{
"path": "EigenLedger/modules/empyrical/utils.py",
"chars": 16676,
"preview": "#\n# Copyright 2018 Quantopian, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not us"
},
{
"path": "EigenLedger/run.py",
"chars": 737,
"preview": "from main import portfolio_analysis, Engine\nimport pandas as pd\n\n# Define custom data\nportfolio_data = pd.DataFrame({\n "
},
{
"path": "LICENSE",
"chars": 11340,
"preview": " Apache License\n Version 2.0, January 2004\n "
},
{
"path": "README.md",
"chars": 10028,
"preview": "#### 📢 Announcement \nGood news! You can now use a patched version of the library [empyrical](https://github.com/quantopi"
},
{
"path": "README_CN.md",
"chars": 8275,
"preview": "#### 📢 公告\n\n好消息!你现在可以通过 EigenLedger 使用维护的 [empyrical](https://github.com/quantopian/empyrical) 库版本了!🎉\n<br>\n👉 在[这里](https:"
},
{
"path": "pyproject.toml",
"chars": 616,
"preview": "[tool.poetry]\nname = \"EigenLedger\"\nauthors = [\"Santosh <santoshpassoubadyp@gmail.com>\"]\nversion = \"2.1.6\"\ndescription = "
}
]
About this extraction
This page contains the full source code of the ssantoshp/trafalgar GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 20 files (179.2 KB), approximately 45.9k tokens, and a symbol index with 116 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.