pivotalpath 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pivotalpath/__init__.py +65 -0
- pivotalpath/cli/__init__.py +0 -0
- pivotalpath/core/__init__.py +24 -0
- pivotalpath/core/examples.py +86 -0
- pivotalpath/core/important_tickers.py +3 -0
- pivotalpath/core/main_library.py +47 -0
- pivotalpath/data/__init__.py +115 -0
- pivotalpath/data/api.py +35 -0
- pivotalpath/data/utils.py +40 -0
- pivotalpath/llm/__init__.py +19 -0
- pivotalpath/llm/interface.py +227 -0
- pivotalpath/stats/__init__.py +55 -0
- pivotalpath/stats/catalog.py +164 -0
- pivotalpath/stats/kernel.py +205 -0
- pivotalpath/stats/params.py +179 -0
- pivotalpath/stats/wrapper.py +51 -0
- pivotalpath/version.py +2 -0
- pivotalpath-1.0.0.dist-info/METADATA +12 -0
- pivotalpath-1.0.0.dist-info/RECORD +21 -0
- pivotalpath-1.0.0.dist-info/WHEEL +5 -0
- pivotalpath-1.0.0.dist-info/top_level.txt +1 -0
pivotalpath/__init__.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PivotalPath: The Industry Standard for Hedge Fund INDEX Analytics
|
|
3
|
+
|
|
4
|
+
Analyzes hedge fund indices - composite benchmarks tracking groups of hedge funds.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .version import __version__
|
|
8
|
+
|
|
9
|
+
# Core functionality
|
|
10
|
+
from .core.main_library import (
|
|
11
|
+
get_catalog,
|
|
12
|
+
get_returns,
|
|
13
|
+
get_info,
|
|
14
|
+
get_asset_list,
|
|
15
|
+
get_stats,
|
|
16
|
+
get_stats_list,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
# Important tickers and examples
|
|
20
|
+
from .core import important_tickers
|
|
21
|
+
from .core import examples
|
|
22
|
+
|
|
23
|
+
# Stats catalog
|
|
24
|
+
from .stats.catalog import stats_catalog
|
|
25
|
+
|
|
26
|
+
# LLM-optimized interface for hedge fund indices
|
|
27
|
+
try:
|
|
28
|
+
from .llm.interface import (
|
|
29
|
+
quick_index_stats,
|
|
30
|
+
analyze_hedge_fund_index,
|
|
31
|
+
compare_hedge_fund_indices,
|
|
32
|
+
list_hedge_fund_indices,
|
|
33
|
+
list_available_metrics,
|
|
34
|
+
generate_index_report,
|
|
35
|
+
)
|
|
36
|
+
LLM_AVAILABLE = True
|
|
37
|
+
except ImportError as e:
|
|
38
|
+
print(f"LLM interface import failed: {e}")
|
|
39
|
+
LLM_AVAILABLE = False
|
|
40
|
+
|
|
41
|
+
# Export everything
|
|
42
|
+
__all__ = [
|
|
43
|
+
"__version__",
|
|
44
|
+
"get_catalog",
|
|
45
|
+
"get_returns",
|
|
46
|
+
"get_info",
|
|
47
|
+
"get_asset_list",
|
|
48
|
+
"get_stats",
|
|
49
|
+
"get_stats_list",
|
|
50
|
+
"stats_catalog",
|
|
51
|
+
"important_tickers",
|
|
52
|
+
"examples",
|
|
53
|
+
"LLM_AVAILABLE",
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
# Add LLM functions if available
|
|
57
|
+
if LLM_AVAILABLE:
|
|
58
|
+
__all__.extend([
|
|
59
|
+
"quick_index_stats",
|
|
60
|
+
"analyze_hedge_fund_index",
|
|
61
|
+
"compare_hedge_fund_indices",
|
|
62
|
+
"list_hedge_fund_indices",
|
|
63
|
+
"list_available_metrics",
|
|
64
|
+
"generate_index_report",
|
|
65
|
+
])
|
|
File without changes
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Core functionality for hedge fund analysis."""
|
|
2
|
+
|
|
3
|
+
from .main_library import (
|
|
4
|
+
get_catalog,
|
|
5
|
+
get_returns,
|
|
6
|
+
get_info,
|
|
7
|
+
get_asset_list,
|
|
8
|
+
get_stats,
|
|
9
|
+
get_stats_list,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
from . import important_tickers
|
|
13
|
+
from . import examples
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"get_catalog",
|
|
17
|
+
"get_returns",
|
|
18
|
+
"get_info",
|
|
19
|
+
"get_asset_list",
|
|
20
|
+
"get_stats",
|
|
21
|
+
"get_stats_list",
|
|
22
|
+
"important_tickers",
|
|
23
|
+
"examples",
|
|
24
|
+
]
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
|
|
2
|
+
from . import main_library as mli
|
|
3
|
+
from . import important_tickers as it
|
|
4
|
+
|
|
5
|
+
"""
|
|
6
|
+
USECASES AND EXAMPLES OF THE MAIN LIBRARY FUNCTIONS
|
|
7
|
+
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
"""
|
|
11
|
+
example1: example of tickers
|
|
12
|
+
"""
|
|
13
|
+
def example1():
|
|
14
|
+
return [it.market_ticker, it.riskfree_ticker, it.hedge_fund_composite_index_ticker]
|
|
15
|
+
|
|
16
|
+
"""
|
|
17
|
+
example2: hedge fund index catalog
|
|
18
|
+
"""
|
|
19
|
+
def example2():
|
|
20
|
+
return mli.get_catalog(asset_class="index")
|
|
21
|
+
|
|
22
|
+
"""
|
|
23
|
+
example3: list all hedge fund index tickers
|
|
24
|
+
"""
|
|
25
|
+
def example3():
|
|
26
|
+
return mli.get_asset_list(asset_class="index")
|
|
27
|
+
|
|
28
|
+
"""
|
|
29
|
+
example4: list all hedge fund index names
|
|
30
|
+
"""
|
|
31
|
+
def example4():
|
|
32
|
+
return mli.get_asset_list(asset_class="index", select="name")
|
|
33
|
+
"""
|
|
34
|
+
example5: market returns
|
|
35
|
+
"""
|
|
36
|
+
def example5():
|
|
37
|
+
return mli.get_returns(ticker=it.market_ticker)
|
|
38
|
+
|
|
39
|
+
"""
|
|
40
|
+
example6: list available stats tickers
|
|
41
|
+
"""
|
|
42
|
+
def example6():
|
|
43
|
+
return mli.get_stats_list()
|
|
44
|
+
"""
|
|
45
|
+
example7: list available stats names
|
|
46
|
+
"""
|
|
47
|
+
def example7():
|
|
48
|
+
return mli.get_stats_list(select="name")
|
|
49
|
+
"""
|
|
50
|
+
example8: example stats for first 2 hedge fund indices
|
|
51
|
+
"""
|
|
52
|
+
def example8():
|
|
53
|
+
return mli.get_stats(
|
|
54
|
+
target=mli.get_asset_list(asset_class="index")[:2],
|
|
55
|
+
base=it.market_ticker,
|
|
56
|
+
start="2020-01",
|
|
57
|
+
end="2024-12",
|
|
58
|
+
stat=['annret','annvol','sharpe']
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
"""
|
|
62
|
+
example9: example stats with custom labels
|
|
63
|
+
"""
|
|
64
|
+
def example9():
|
|
65
|
+
return mli.get_stats(
|
|
66
|
+
target=mli.get_asset_list(asset_class='index')+['SP500'],
|
|
67
|
+
base=it.market_ticker,
|
|
68
|
+
start="2020-01",
|
|
69
|
+
end="2024-12",
|
|
70
|
+
stat=mli.get_stats_list(),
|
|
71
|
+
label_as="shortname",
|
|
72
|
+
)
|
|
73
|
+
"""
|
|
74
|
+
example10: example structured stats
|
|
75
|
+
"""
|
|
76
|
+
def example10():
|
|
77
|
+
return mli.get_stats(
|
|
78
|
+
target=mli.get_asset_list(asset_class='index'),
|
|
79
|
+
base=it.market_ticker,
|
|
80
|
+
start="2020-01",
|
|
81
|
+
end="2024-12",
|
|
82
|
+
stat=['sharpe','annvol'],
|
|
83
|
+
label_as="name+period",
|
|
84
|
+
index_as="name",
|
|
85
|
+
)
|
|
86
|
+
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
|
|
2
|
+
from ..data import get_catalog, get_returns, get_info
|
|
3
|
+
from ..stats import stats_calculator,stats_catalog
|
|
4
|
+
import pandas as pd
|
|
5
|
+
|
|
6
|
+
def _notdf(df):
|
|
7
|
+
return not(df is None) and not (isinstance(df,pd.DataFrame))
|
|
8
|
+
|
|
9
|
+
def get_stats(
|
|
10
|
+
target=None,
|
|
11
|
+
base=None,
|
|
12
|
+
riskfree=None,
|
|
13
|
+
index_as="ticker",
|
|
14
|
+
transpose=False,
|
|
15
|
+
**kwargs
|
|
16
|
+
):
|
|
17
|
+
if _notdf(target):
|
|
18
|
+
target=get_returns(ticker=target)
|
|
19
|
+
if _notdf(base):
|
|
20
|
+
base=get_returns(ticker=base)
|
|
21
|
+
if _notdf(riskfree):
|
|
22
|
+
riskfree=get_returns(ticker=riskfree)
|
|
23
|
+
return pd.DataFrame()
|
|
24
|
+
df=stats_calculator(target=target,base=base,riskfree=riskfree,**kwargs)
|
|
25
|
+
if isinstance(index_as,str) and index_as!="ticker":
|
|
26
|
+
try:
|
|
27
|
+
df_info=get_info(ticker=df.index.tolist())
|
|
28
|
+
if isinstance(df_info,pd.DataFrame) and not (df_info.empty):
|
|
29
|
+
info_map=df_info.set_index("ticker")[index_as].to_dict()
|
|
30
|
+
df.index=[info_map.get(_,_) for _ in df.index.tolist()]
|
|
31
|
+
df.index=df[index_as]
|
|
32
|
+
df.index.name=index_as
|
|
33
|
+
except:
|
|
34
|
+
pass
|
|
35
|
+
if transpose:
|
|
36
|
+
df=df.T
|
|
37
|
+
return df
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def get_stats_list(select="ticker")->list:
|
|
41
|
+
return [_.get(select) for _ in stats_catalog]
|
|
42
|
+
|
|
43
|
+
def get_asset_list(asset_class="index",select="ticker")->list:
|
|
44
|
+
df=get_catalog(asset_class=asset_class,select=select)
|
|
45
|
+
if isinstance(df,pd.DataFrame) and not (df.empty):
|
|
46
|
+
return df[select].tolist()
|
|
47
|
+
return []
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
|
|
2
|
+
import pandas as pd
|
|
3
|
+
from .api import GetFilteredDataFrame
|
|
4
|
+
from . import utils as tsutil
|
|
5
|
+
|
|
6
|
+
def _ticker2class(ticker):
|
|
7
|
+
if isinstance(ticker,list):
|
|
8
|
+
return [_ticker2class(_) for _ in ticker]
|
|
9
|
+
else:
|
|
10
|
+
# tba: add confirmation
|
|
11
|
+
if ticker.startswith("PP"):
|
|
12
|
+
return "index"
|
|
13
|
+
else:
|
|
14
|
+
return "factor"
|
|
15
|
+
|
|
16
|
+
def _ticker2map(ticker=[]):
|
|
17
|
+
out=dict(
|
|
18
|
+
index=[_ for _ in ticker if _.startswith("PP")],
|
|
19
|
+
factor=[_ for _ in ticker if not (_.startswith("PP"))]
|
|
20
|
+
)
|
|
21
|
+
return {_:__ for _,__ in out.items() if __}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
"""
|
|
25
|
+
main functions
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def get_catalog(asset_class="index",filter_by=dict(),select="*")->pd.DataFrame:
|
|
29
|
+
if isinstance(asset_class,list):
|
|
30
|
+
df=pd.DataFrame()
|
|
31
|
+
for _ in asset_class:
|
|
32
|
+
df_i=get_catalog(asset_class=_,filter_by=filter_by,select="*")
|
|
33
|
+
df=pd.concat([df,df_i]).set_index("ticker")
|
|
34
|
+
return df
|
|
35
|
+
table_name=f"{asset_class}_catalog"
|
|
36
|
+
return GetFilteredDataFrame(table_name,filter_by=filter_by)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def get_info(ticker=None):
|
|
40
|
+
|
|
41
|
+
if not ticker:
|
|
42
|
+
return {}
|
|
43
|
+
|
|
44
|
+
if isinstance(ticker,str):
|
|
45
|
+
asset_class=_ticker2class(ticker)
|
|
46
|
+
table_name=f"{asset_class}_catalog"
|
|
47
|
+
return GetFilteredDataFrame(table_name,filter_by=dict(ticker=ticker)).iloc[0].to_dict()
|
|
48
|
+
|
|
49
|
+
if isinstance(ticker,list):
|
|
50
|
+
tickermap=_ticker2map(ticker)
|
|
51
|
+
out=pd.DataFrame()
|
|
52
|
+
for asset_i,ticker_i in tickermap.items():
|
|
53
|
+
table_name=f"{asset_i}_catalog"
|
|
54
|
+
df_i=GetFilteredDataFrame(table_name, filter_by=dict(ticker=ticker_i))
|
|
55
|
+
if isinstance(df_i,pd.DataFrame) and not (df_i.empty):
|
|
56
|
+
out=pd.concat([out,df_i],axis=0)
|
|
57
|
+
return out
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def get_returns(
|
|
61
|
+
ticker=[],
|
|
62
|
+
month_list=[],
|
|
63
|
+
start=None,
|
|
64
|
+
end=None,
|
|
65
|
+
lookback=None,
|
|
66
|
+
column_label="ticker",
|
|
67
|
+
freq="M",
|
|
68
|
+
freq_aggregation="compounded",
|
|
69
|
+
annualized=False,
|
|
70
|
+
rounding_places=6,
|
|
71
|
+
pivot=True,
|
|
72
|
+
|
|
73
|
+
)->pd.DataFrame:
|
|
74
|
+
|
|
75
|
+
"""
|
|
76
|
+
Get returns data for specified tickers.
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
pd.DataFrame or None: DataFrame with returns data, or None if no ticker provided
|
|
80
|
+
"""
|
|
81
|
+
if not ticker:
|
|
82
|
+
return pd.DataFrame()
|
|
83
|
+
if isinstance(ticker,str):
|
|
84
|
+
ticker=[ticker]
|
|
85
|
+
tickermap=_ticker2map(ticker=ticker)
|
|
86
|
+
if not month_list:
|
|
87
|
+
month_list=tsutil.build_month_list(start=start,end=end,lookback=lookback)
|
|
88
|
+
df=pd.DataFrame()
|
|
89
|
+
for asset_i, ticker_i in tickermap.items():
|
|
90
|
+
table_name=f"{asset_i}_returns"
|
|
91
|
+
rows=GetFilteredDataFrame(table_name, filter_by=dict(ticker=ticker_i))
|
|
92
|
+
if start:
|
|
93
|
+
rows=rows[rows['month']>=start]
|
|
94
|
+
if end:
|
|
95
|
+
rows=rows[rows['month']<=end]
|
|
96
|
+
|
|
97
|
+
if df.empty:
|
|
98
|
+
df=rows
|
|
99
|
+
else:
|
|
100
|
+
df=pd.concat([df,rows])
|
|
101
|
+
|
|
102
|
+
df=df[df['ticker'].isin(ticker)]
|
|
103
|
+
if freq!="M":
|
|
104
|
+
pivot=True
|
|
105
|
+
|
|
106
|
+
if pivot:
|
|
107
|
+
df=df.pivot_table(index='month', columns='ticker', values='return')
|
|
108
|
+
df=df[[_ for _ in ticker if _ in df.columns]]
|
|
109
|
+
|
|
110
|
+
if freq!="M":
|
|
111
|
+
df=tsutil.frequency_aggregation(df, freq=freq,freq_aggregation=freq_aggregation,annualized=annualized)
|
|
112
|
+
|
|
113
|
+
return round(df,rounding_places)
|
|
114
|
+
|
|
115
|
+
|
pivotalpath/data/api.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
|
|
2
|
+
import pandas as pd
|
|
3
|
+
import warnings
|
|
4
|
+
import requests
|
|
5
|
+
|
|
6
|
+
class ApiCaller:
|
|
7
|
+
|
|
8
|
+
base_url = "http://apps.pivotalpath.com/p3ypi/resources/api/v1/"
|
|
9
|
+
api_key = "p3k_7H9mK2pL8qX3nR5vW1cY4jZ6sA0uE9"
|
|
10
|
+
|
|
11
|
+
@classmethod
|
|
12
|
+
def get_headers(cls):
|
|
13
|
+
return {
|
|
14
|
+
"Authorization": f"Bearer {cls.api_key}",
|
|
15
|
+
"Content-Type": "application/json"
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
@classmethod
|
|
19
|
+
def get_data(cls,endpoint=None,params=dict()):
|
|
20
|
+
url=cls.base_url+endpoint
|
|
21
|
+
headers=cls.get_headers()
|
|
22
|
+
response=requests.get(url,headers=headers,params=params)
|
|
23
|
+
if response.status_code==200:
|
|
24
|
+
try:
|
|
25
|
+
return pd.DataFrame(response.json())
|
|
26
|
+
except ValueError:
|
|
27
|
+
warnings.warn("Failed to parse JSON response")
|
|
28
|
+
return pd.DataFrame()
|
|
29
|
+
else:
|
|
30
|
+
warnings.warn(f"API call failed with status code {response.status_code}")
|
|
31
|
+
return pd.DataFrame()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def GetFilteredDataFrame(table_name=None, filter_by=dict())->pd.DataFrame:
|
|
35
|
+
return ApiCaller.get_data(endpoint=table_name,params=filter_by)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
|
|
3
|
+
def _do_compound(df):
|
|
4
|
+
return (1 +df).prod(min_count=1) - 1
|
|
5
|
+
def _do_sum(df):
|
|
6
|
+
return df.sum()
|
|
7
|
+
def _compute_default_end():
|
|
8
|
+
return (pd.Timestamp.today()-pd.DateOffset(months=1)).strftime("%Y-%m")
|
|
9
|
+
def _compute_default_start():
|
|
10
|
+
return "1998-01"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def build_month_list(start=None,end=None,lookback=None, _default_start="1998-01",_default_rule="rule_of_7"):
|
|
14
|
+
if not end:
|
|
15
|
+
end=_compute_default_end()
|
|
16
|
+
if not start:
|
|
17
|
+
if lookback:
|
|
18
|
+
start=(pd.Timestamp(end)-pd.DateOffset(months=lookback-1)).strftime("%Y-%m")
|
|
19
|
+
else:
|
|
20
|
+
start=_compute_default_start()
|
|
21
|
+
return [_.strftime("%Y-%m") for _ in pd.period_range(start=start,end=end,freq="M")]
|
|
22
|
+
|
|
23
|
+
def timeseries_aggregation(df,freq="Q",freq_aggregation="compounded",annualized=False,rounding_places=6):
|
|
24
|
+
if not isinstance(df.index, pd.DatetimeIndex):
|
|
25
|
+
df.index = pd.to_datetime(df.index, format='%Y-%m')
|
|
26
|
+
df_agg=df.resample(freq)
|
|
27
|
+
if freq_aggregation=="compounded":
|
|
28
|
+
out=df_agg.apply(_do_compound)
|
|
29
|
+
if annualized:
|
|
30
|
+
out=(1+out)**4-1
|
|
31
|
+
elif freq_aggregation=="sum":
|
|
32
|
+
out= df_agg.apply(_do_sum)
|
|
33
|
+
if annualized:
|
|
34
|
+
out=4*out
|
|
35
|
+
if rounding_places:
|
|
36
|
+
out=round(out,rounding_places)
|
|
37
|
+
out.index=out.index.to_period(freq).astype(str)
|
|
38
|
+
if freq=="Y":
|
|
39
|
+
out.index=[f"Y{_}" for _ in out.index]
|
|
40
|
+
return out
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""LLM-optimized interface for hedge fund index analysis."""
|
|
2
|
+
|
|
3
|
+
from .interface import (
|
|
4
|
+
quick_index_stats,
|
|
5
|
+
analyze_hedge_fund_index,
|
|
6
|
+
compare_hedge_fund_indices,
|
|
7
|
+
list_hedge_fund_indices,
|
|
8
|
+
list_available_metrics,
|
|
9
|
+
generate_index_report,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"quick_index_stats",
|
|
14
|
+
"analyze_hedge_fund_index",
|
|
15
|
+
"compare_hedge_fund_indices",
|
|
16
|
+
"list_hedge_fund_indices",
|
|
17
|
+
"list_available_metrics",
|
|
18
|
+
"generate_index_report",
|
|
19
|
+
]
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
"""
|
|
2
|
+
LLM-optimized interface for PivotalPath.
|
|
3
|
+
Designed for hedge fund INDEX analysis only.
|
|
4
|
+
|
|
5
|
+
PivotalPath analyzes hedge fund indices - composite benchmarks that track
|
|
6
|
+
groups of hedge funds by strategy. It does NOT analyze individual hedge funds.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from typing import Dict, List, Optional, Union, Any
|
|
10
|
+
import pandas as pd
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
|
|
13
|
+
# Import from your existing modules
|
|
14
|
+
from ..core.main_library import get_stats, get_returns, get_asset_list, get_catalog
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def quick_index_stats(
|
|
18
|
+
index_ticker: str,
|
|
19
|
+
benchmark: str = "SP500",
|
|
20
|
+
period: str = "5Y"
|
|
21
|
+
) -> Dict[str, Any]:
|
|
22
|
+
"""
|
|
23
|
+
Get essential performance stats for a hedge fund index.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
index_ticker: Hedge fund index ticker (e.g., "PP-HFC")
|
|
27
|
+
benchmark: Market benchmark (default: "SP500")
|
|
28
|
+
period: Time period - "1Y", "3Y", "5Y", "YTD"
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
Dictionary with essential index performance metrics
|
|
32
|
+
"""
|
|
33
|
+
import pandas as pd
|
|
34
|
+
|
|
35
|
+
end_date = (pd.Timestamp.now() - pd.DateOffset(months=1)).strftime("%Y-%m")
|
|
36
|
+
|
|
37
|
+
if period == "YTD":
|
|
38
|
+
start_date = f"{pd.Timestamp.now().year}-01"
|
|
39
|
+
elif period.endswith("Y"):
|
|
40
|
+
years = int(period[:-1])
|
|
41
|
+
start_date = (pd.Timestamp.now() - pd.DateOffset(years=years)).strftime("%Y-%m")
|
|
42
|
+
else:
|
|
43
|
+
start_date = "2020-01"
|
|
44
|
+
|
|
45
|
+
essential_stats = ['annret', 'annvol', 'sharpe', 'maxddc', 'beta']
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
results = get_stats(
|
|
49
|
+
target=index_ticker,
|
|
50
|
+
base=benchmark,
|
|
51
|
+
start=start_date,
|
|
52
|
+
end=end_date,
|
|
53
|
+
stat=essential_stats
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
if isinstance(results, pd.DataFrame) and not results.empty:
|
|
57
|
+
data = results.iloc[:, 0].to_dict()
|
|
58
|
+
data.update({
|
|
59
|
+
'index_ticker': index_ticker,
|
|
60
|
+
'asset_type': 'hedge_fund_index',
|
|
61
|
+
'benchmark': benchmark,
|
|
62
|
+
'period': period,
|
|
63
|
+
'analysis_date': pd.Timestamp.now().strftime("%Y-%m-%d")
|
|
64
|
+
})
|
|
65
|
+
return data
|
|
66
|
+
else:
|
|
67
|
+
return {
|
|
68
|
+
'error': f"No data available for index {index_ticker}",
|
|
69
|
+
'suggested_indices': list_hedge_fund_indices()[:3]
|
|
70
|
+
}
|
|
71
|
+
except Exception as e:
|
|
72
|
+
return {
|
|
73
|
+
'error': f"Index analysis failed: {str(e)}",
|
|
74
|
+
'index_ticker': index_ticker
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def analyze_hedge_fund_index(
|
|
79
|
+
index_ticker: str,
|
|
80
|
+
benchmark: str = "SP500",
|
|
81
|
+
start_date: str = "2020-01",
|
|
82
|
+
end_date: str = "2024-12"
|
|
83
|
+
) -> Dict[str, Any]:
|
|
84
|
+
"""
|
|
85
|
+
Comprehensive analysis of a hedge fund index.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
index_ticker: Hedge fund index ticker (e.g., "PP-HFC")
|
|
89
|
+
benchmark: Market benchmark (default: "SP500")
|
|
90
|
+
start_date: Start date in YYYY-MM format
|
|
91
|
+
end_date: End date in YYYY-MM format
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
Dictionary with comprehensive index performance metrics
|
|
95
|
+
"""
|
|
96
|
+
all_stats = ['annret', 'annvol', 'sharpe', 'beta', 'alpha', 'maxddc', 'sortino', 'calmar']
|
|
97
|
+
|
|
98
|
+
try:
|
|
99
|
+
results = get_stats(
|
|
100
|
+
target=index_ticker,
|
|
101
|
+
base=benchmark,
|
|
102
|
+
start=start_date,
|
|
103
|
+
end=end_date,
|
|
104
|
+
stat=all_stats
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
if isinstance(results, pd.DataFrame) and not results.empty:
|
|
108
|
+
analysis_dict = results.iloc[:, 0].to_dict()
|
|
109
|
+
analysis_dict.update({
|
|
110
|
+
'index_ticker': index_ticker,
|
|
111
|
+
'asset_type': 'hedge_fund_index',
|
|
112
|
+
'benchmark': benchmark,
|
|
113
|
+
'period': f"{start_date} to {end_date}",
|
|
114
|
+
'analysis_date': datetime.now().strftime("%Y-%m-%d")
|
|
115
|
+
})
|
|
116
|
+
return analysis_dict
|
|
117
|
+
else:
|
|
118
|
+
return {
|
|
119
|
+
'error': f"No data available for index {index_ticker}",
|
|
120
|
+
'index_ticker': index_ticker
|
|
121
|
+
}
|
|
122
|
+
except Exception as e:
|
|
123
|
+
return {
|
|
124
|
+
'error': f"Index analysis failed: {str(e)}",
|
|
125
|
+
'index_ticker': index_ticker
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def compare_hedge_fund_indices(
|
|
130
|
+
index_tickers: List[str],
|
|
131
|
+
benchmark: str = "SP500",
|
|
132
|
+
metrics: List[str] = ['annret', 'annvol', 'sharpe', 'maxddc'],
|
|
133
|
+
start_date: str = "2020-01",
|
|
134
|
+
end_date: str = "2024-12"
|
|
135
|
+
) -> pd.DataFrame:
|
|
136
|
+
"""
|
|
137
|
+
Compare multiple hedge fund indices side by side.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
index_tickers: List of hedge fund index tickers
|
|
141
|
+
benchmark: Market benchmark (default: "SP500")
|
|
142
|
+
metrics: Metrics to compare
|
|
143
|
+
start_date: Start date in YYYY-MM format
|
|
144
|
+
end_date: End date in YYYY-MM format
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
DataFrame comparing indices
|
|
148
|
+
"""
|
|
149
|
+
try:
|
|
150
|
+
results = get_stats(
|
|
151
|
+
target=index_tickers,
|
|
152
|
+
base=benchmark,
|
|
153
|
+
start=start_date,
|
|
154
|
+
end=end_date,
|
|
155
|
+
stat=metrics,
|
|
156
|
+
transpose=True
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
if isinstance(results, pd.DataFrame) and not results.empty:
|
|
160
|
+
return results.round(4)
|
|
161
|
+
else:
|
|
162
|
+
return pd.DataFrame(index=metrics, columns=index_tickers)
|
|
163
|
+
|
|
164
|
+
except Exception as e:
|
|
165
|
+
print(f"Index comparison failed: {e}")
|
|
166
|
+
return pd.DataFrame(index=metrics, columns=index_tickers)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def list_hedge_fund_indices() -> List[str]:
|
|
170
|
+
"""Get list of all available hedge fund index tickers."""
|
|
171
|
+
try:
|
|
172
|
+
return get_asset_list(asset_class="index", select="ticker")
|
|
173
|
+
except Exception:
|
|
174
|
+
return ["PP-HFC", "PP-L-EH", "PP-L-MA", "PP-L-ED", "PP-L-EM"]
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def list_available_metrics() -> List[str]:
|
|
178
|
+
"""Get list of all available performance metrics."""
|
|
179
|
+
try:
|
|
180
|
+
from ..core.main_library import get_stats_list
|
|
181
|
+
return get_stats_list(select="ticker")
|
|
182
|
+
except Exception:
|
|
183
|
+
return ['annret', 'annvol', 'sharpe', 'maxddc', 'beta', 'alpha', 'sortino', 'calmar']
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def generate_index_report(
|
|
187
|
+
index_ticker: str,
|
|
188
|
+
benchmark: str = "SP500",
|
|
189
|
+
start_date: str = "2020-01",
|
|
190
|
+
end_date: str = "2024-12"
|
|
191
|
+
) -> str:
|
|
192
|
+
"""
|
|
193
|
+
Generate a formatted report for a hedge fund index.
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
Formatted string report for the index
|
|
197
|
+
"""
|
|
198
|
+
analysis = analyze_hedge_fund_index(index_ticker, benchmark, start_date, end_date)
|
|
199
|
+
|
|
200
|
+
if 'error' in analysis:
|
|
201
|
+
return f"Index Report Failed: {analysis['error']}"
|
|
202
|
+
|
|
203
|
+
return f"""
|
|
204
|
+
HEDGE FUND INDEX PERFORMANCE REPORT
|
|
205
|
+
{'='*50}
|
|
206
|
+
|
|
207
|
+
Index: {analysis.get('index_ticker', 'N/A')}
|
|
208
|
+
Period: {analysis.get('period', 'N/A')}
|
|
209
|
+
Benchmark: {analysis.get('benchmark', 'N/A')}
|
|
210
|
+
|
|
211
|
+
PERFORMANCE METRICS
|
|
212
|
+
{''*25}
|
|
213
|
+
Annual Return: {analysis.get('annret', 0):>8.1%}
|
|
214
|
+
Volatility: {analysis.get('annvol', 0):>8.1%}
|
|
215
|
+
Sharpe Ratio: {analysis.get('sharpe', 0):>8.2f}
|
|
216
|
+
Max Drawdown: {analysis.get('maxddc', 0):>8.1%}
|
|
217
|
+
|
|
218
|
+
MARKET EXPOSURE
|
|
219
|
+
{''*25}
|
|
220
|
+
Beta: {analysis.get('beta', 0):>8.2f}
|
|
221
|
+
Alpha: {analysis.get('alpha', 0):>8.1%}
|
|
222
|
+
|
|
223
|
+
Analysis Date: {analysis.get('analysis_date', 'N/A')}
|
|
224
|
+
|
|
225
|
+
NOTE: This report analyzes a hedge fund INDEX, which represents
|
|
226
|
+
aggregated performance of multiple hedge funds, not an individual fund.
|
|
227
|
+
"""
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
|
|
2
|
+
import warnings
|
|
3
|
+
import pandas as pd
|
|
4
|
+
from .params import configureParams
|
|
5
|
+
from .wrapper import basicstats
|
|
6
|
+
from .catalog import stats_catalog
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def stats_calculator(
|
|
10
|
+
target=None,
|
|
11
|
+
base="SP500",
|
|
12
|
+
riskfree="TBILL3M",
|
|
13
|
+
configured_params=[],
|
|
14
|
+
**kwargs
|
|
15
|
+
)->pd.DataFrame:
|
|
16
|
+
|
|
17
|
+
if not kwargs.get("_default_start"):
|
|
18
|
+
kwargs.update(_default_start=target.index[0])
|
|
19
|
+
|
|
20
|
+
if not kwargs.get("_default_end"):
|
|
21
|
+
kwargs.update(_default_end=target.index[-1])
|
|
22
|
+
|
|
23
|
+
if not configured_params:
|
|
24
|
+
# note: configured_params will most certainly be empty, unles it is repurposed from a different call
|
|
25
|
+
configured_params=configureParams(**kwargs)
|
|
26
|
+
|
|
27
|
+
df=target
|
|
28
|
+
out=None
|
|
29
|
+
|
|
30
|
+
for i,param_i in enumerate(configured_params):
|
|
31
|
+
stat_i=basicstats(df,base=base,riskfree=riskfree,**param_i)
|
|
32
|
+
if stat_i is None:
|
|
33
|
+
continue
|
|
34
|
+
stat_i=pd.DataFrame(stat_i)
|
|
35
|
+
if out is None:
|
|
36
|
+
out=stat_i.copy()
|
|
37
|
+
else:
|
|
38
|
+
try:
|
|
39
|
+
out=out.join(stat_i)
|
|
40
|
+
except:
|
|
41
|
+
warnings.warn("could not join the ith stat output")
|
|
42
|
+
|
|
43
|
+
return out
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# Import everything from catalog
|
|
47
|
+
from .catalog import stats_catalog, stat_dict
|
|
48
|
+
|
|
49
|
+
__all__ = [
|
|
50
|
+
"stats_calculator",
|
|
51
|
+
"stats_catalog",
|
|
52
|
+
"stat_dict",
|
|
53
|
+
"basicstats",
|
|
54
|
+
"configureParams",
|
|
55
|
+
]
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
stats_catalog=[
|
|
2
|
+
dict(
|
|
3
|
+
ticker='totret',
|
|
4
|
+
name='Total Return',
|
|
5
|
+
shortname='TotRet',
|
|
6
|
+
description="full sample return",
|
|
7
|
+
latex_expression='$\prod_{i=1}^N(1+r_i)-1$',
|
|
8
|
+
),
|
|
9
|
+
dict(
|
|
10
|
+
ticker='annret',
|
|
11
|
+
name="Annualized Return",
|
|
12
|
+
shortname='AnnRet',
|
|
13
|
+
description="annualized mean return",
|
|
14
|
+
latex_expression='$(12/N)\sum_{i=1}^Nr_i$',
|
|
15
|
+
),
|
|
16
|
+
|
|
17
|
+
dict(
|
|
18
|
+
ticker='annretc',
|
|
19
|
+
name="Compounded Annualized Return",
|
|
20
|
+
shortname='AnnRet(c)',
|
|
21
|
+
description="compounded annualized return",
|
|
22
|
+
latex_expression='$(\prod_{i=1}^N(1+r_i))^{12/N}-1$',
|
|
23
|
+
),
|
|
24
|
+
|
|
25
|
+
dict(
|
|
26
|
+
ticker='annvol',
|
|
27
|
+
name='Annualized Volatility',
|
|
28
|
+
shortname='AnnVol',
|
|
29
|
+
description="annualized volatily",
|
|
30
|
+
latex_expression="$\sqrt{12}\\times\mathbb E[(r_i-\\bar{r_i})^2]^{1/2}$",
|
|
31
|
+
),
|
|
32
|
+
|
|
33
|
+
dict(
|
|
34
|
+
ticker='maxddc',
|
|
35
|
+
name='Max Drawdown',
|
|
36
|
+
shortname='MaxDD',
|
|
37
|
+
description='max loss (peak to trough)',
|
|
38
|
+
latex_expression="$\\min_{1\leq s\leq t\leq N}\prod_{i=s}^t(1+r_i)-1$",
|
|
39
|
+
),
|
|
40
|
+
|
|
41
|
+
dict(
|
|
42
|
+
ticker='skewness',
|
|
43
|
+
name='Skewness',
|
|
44
|
+
shortname='Skewness',
|
|
45
|
+
description="skewness",
|
|
46
|
+
latex_expression='$\mathbb E[(\\frac{r_i-\mu}{\sigma})^3]$',
|
|
47
|
+
),
|
|
48
|
+
|
|
49
|
+
dict(
|
|
50
|
+
ticker='exckurt',
|
|
51
|
+
name='Excess Kurtosis',
|
|
52
|
+
shortname='Kurt(exc)',
|
|
53
|
+
description="excess kurtosis",
|
|
54
|
+
latex_expression='$\mathbb E[(\\frac{r_i-\mu}{\sigma})^4]-3$',
|
|
55
|
+
),
|
|
56
|
+
|
|
57
|
+
dict(
|
|
58
|
+
ticker='sharpe',
|
|
59
|
+
name='Sharpe Ratio',
|
|
60
|
+
shortname='Sharpe',
|
|
61
|
+
latex_expression='AnnReturn/AnnVol'
|
|
62
|
+
),
|
|
63
|
+
|
|
64
|
+
dict(
|
|
65
|
+
ticker='sharpec',
|
|
66
|
+
name='Sharpe Ratio (compounded)',
|
|
67
|
+
shortname='SR(c)',
|
|
68
|
+
latex_expression='AnnReturn(c)/AnnVol'
|
|
69
|
+
),
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
dict(
|
|
73
|
+
ticker='sharperf',
|
|
74
|
+
name='Sharpe Ratio (riskfree)',
|
|
75
|
+
shortname='SR(rf)',
|
|
76
|
+
latex_expression='Sharpe(return-TBill3M)'
|
|
77
|
+
),
|
|
78
|
+
|
|
79
|
+
dict(
|
|
80
|
+
ticker="sharpeadj",
|
|
81
|
+
name="Adjusted Sharpe Ratio",
|
|
82
|
+
shortname="SR(adj)",
|
|
83
|
+
latex_expression='$\operatorname{SR}(1 + (1/6) \cdot \operatorname{Skewness} \cdot \operatorname{SR} - (1/24)\operatorname{ExcKurt}\cdot \operatorname{SR}^2)$',
|
|
84
|
+
),
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
dict(
|
|
88
|
+
ticker='calmar',
|
|
89
|
+
name='Calmar',
|
|
90
|
+
shortname='Calmar',
|
|
91
|
+
description="calmar ratio (36M)",
|
|
92
|
+
latex_expression='AnnReturn(c)/MDD',
|
|
93
|
+
),
|
|
94
|
+
|
|
95
|
+
dict(
|
|
96
|
+
ticker='omega',
|
|
97
|
+
name='Omega',
|
|
98
|
+
shortname='Omega',
|
|
99
|
+
description="omega ratio",
|
|
100
|
+
latex_expression='$\mathbb E[r_i|r_i>0]/\mathbb E[|r_i||r_i<0]$ '
|
|
101
|
+
),
|
|
102
|
+
|
|
103
|
+
dict(
|
|
104
|
+
ticker='sortino',
|
|
105
|
+
name='Sortino Ratio',
|
|
106
|
+
shortname='Sortino',
|
|
107
|
+
latex_expression='AnnRet/($\sqrt{12}\\times$DDEV)'
|
|
108
|
+
),
|
|
109
|
+
|
|
110
|
+
dict(
|
|
111
|
+
ticker='ddev',
|
|
112
|
+
name='Downside Deviation',
|
|
113
|
+
shortname='DDEV',
|
|
114
|
+
latex_expression='$\mathbb E[\\min(0,r_i)^2]^{1/2}$',
|
|
115
|
+
),
|
|
116
|
+
|
|
117
|
+
dict(
|
|
118
|
+
ticker='hitratio',
|
|
119
|
+
name='Hit Ratio',
|
|
120
|
+
shortname='HR',
|
|
121
|
+
description='proportion of positive returns',
|
|
122
|
+
latex_expression="Prob(r_i≥0)",
|
|
123
|
+
),
|
|
124
|
+
|
|
125
|
+
dict(
|
|
126
|
+
ticker='alpha',
|
|
127
|
+
name='Annualized Alpha',
|
|
128
|
+
shortname='Alpha',
|
|
129
|
+
description="annualized alpha",
|
|
130
|
+
latex_expression='$12\\times(\mathbb E[r_i]-\\beta\mathbb E[f_i])$',
|
|
131
|
+
),
|
|
132
|
+
|
|
133
|
+
dict(
|
|
134
|
+
ticker='beta',
|
|
135
|
+
name='Beta',
|
|
136
|
+
shortname='Beta',
|
|
137
|
+
description="beta",
|
|
138
|
+
latex_expression='$\operatorname{cov}(r_i,f_i)/\operatorname{var}(f_i)$',
|
|
139
|
+
),
|
|
140
|
+
|
|
141
|
+
dict(
|
|
142
|
+
ticker='corr',
|
|
143
|
+
name='Correlation',
|
|
144
|
+
shortname='Corr',
|
|
145
|
+
description='Pearson correlation coefficient',
|
|
146
|
+
latex_expression='$\operatorname{cov}(r_i,f_i)/(\operatorname{std}(r_i)\operatorname{std}(f_i))$',
|
|
147
|
+
),
|
|
148
|
+
|
|
149
|
+
dict(
|
|
150
|
+
ticker='r2',
|
|
151
|
+
name='RSquared',
|
|
152
|
+
shortname='R2',
|
|
153
|
+
description='r squared',
|
|
154
|
+
latex_expression='$\operatorname{var}[\epsilon_i]$',
|
|
155
|
+
),
|
|
156
|
+
|
|
157
|
+
]
|
|
158
|
+
|
|
159
|
+
stat_dict={
|
|
160
|
+
item['ticker']:item
|
|
161
|
+
for item in stats_catalog
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
import numpy as np
|
|
3
|
+
import pdb
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def _zeroInsert(df):
|
|
7
|
+
if "date" in df.columns.tolist():
|
|
8
|
+
df=df.set_index("date")
|
|
9
|
+
df.index.name="date"
|
|
10
|
+
un_indexify=True
|
|
11
|
+
else:
|
|
12
|
+
un_indexify=False
|
|
13
|
+
newrow=pd.DataFrame({k:[0] for k in df.columns},index=["0000-00"])
|
|
14
|
+
df=pd.concat([newrow,df],axis=0)
|
|
15
|
+
if un_indexify:
|
|
16
|
+
df=df.reset_index().rename(columns={'index':'date'})
|
|
17
|
+
return df
|
|
18
|
+
|
|
19
|
+
def _outputNull(df,title='stat'):
|
|
20
|
+
return pd.DataFrame(pd.Series(index=df.columns,name=title))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _nanAlign(df,base):
|
|
25
|
+
try:
|
|
26
|
+
base=pd.concat([base] * (df.shape[1]), axis=1, ignore_index=True)
|
|
27
|
+
base.columns=df.columns
|
|
28
|
+
base[df.isnull()] = df.copy()
|
|
29
|
+
df[base.isnull()] = base.copy()
|
|
30
|
+
return df,base
|
|
31
|
+
except:
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def totret(df,**kwargs):
|
|
36
|
+
return (1+df).prod(axis=0)-1
|
|
37
|
+
|
|
38
|
+
def annret(df,**kwargs):
|
|
39
|
+
return 12*df.mean(axis=0)
|
|
40
|
+
|
|
41
|
+
def annretc(df,**kwargs):
|
|
42
|
+
A=(1+df).prod(axis=0)
|
|
43
|
+
B=df.notna().sum(axis=0)
|
|
44
|
+
B[B==0]=np.nan
|
|
45
|
+
return (A**(12/B))-1
|
|
46
|
+
|
|
47
|
+
def annvol(df,**kwargs):
|
|
48
|
+
return np.sqrt(12)*df.std(axis=0,ddof=1)
|
|
49
|
+
|
|
50
|
+
def maxdd(df,**kwargs):
|
|
51
|
+
nav=_zeroInsert(df).cumsum()
|
|
52
|
+
peak=nav.expanding(1).max()
|
|
53
|
+
return (nav-peak).iloc[1:].min(axis=0)
|
|
54
|
+
|
|
55
|
+
def maxddc(df,**kwargs):
|
|
56
|
+
nav=(1+_zeroInsert(df)).cumprod()
|
|
57
|
+
peak=nav.expanding(1).max()
|
|
58
|
+
return ((nav/peak)-1.0).iloc[1:].min(axis=0)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def skewness(df,**kwargs):
|
|
62
|
+
#scistats.skew(df,nan_policy='omit')
|
|
63
|
+
sigma=df.std(axis=0,ddof=0)
|
|
64
|
+
sigma[sigma==0]=np.nan
|
|
65
|
+
df0=(df-df.mean(axis=0))/sigma
|
|
66
|
+
return (df0**3).mean(axis=0)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def exckurt(df,**kwargs):
|
|
71
|
+
# scistats.kurtosis(df,nan_policy='omit')
|
|
72
|
+
sigma=df.std(axis=0,ddof=0)
|
|
73
|
+
sigma[sigma==0]=np.nan
|
|
74
|
+
df0=(df-df.mean(axis=0))/sigma
|
|
75
|
+
return (df0**4).mean(axis=0)-3
|
|
76
|
+
|
|
77
|
+
def sharpe(df,**kwargs):
|
|
78
|
+
R=12*df.mean(axis=0)
|
|
79
|
+
V=np.sqrt(12)*df.std(axis=0,ddof=1)
|
|
80
|
+
return R/V
|
|
81
|
+
|
|
82
|
+
def sharpec(df,**kwargs):
|
|
83
|
+
A=(1+df).prod(axis=0)
|
|
84
|
+
B=df.notna().sum(axis=0)
|
|
85
|
+
B[B==0]=np.nan
|
|
86
|
+
R=(A**(12/B))-1
|
|
87
|
+
V=np.sqrt(12)*df.std(axis=0,ddof=1)
|
|
88
|
+
return R/V
|
|
89
|
+
|
|
90
|
+
def sharperf(df,riskfree=None,**kwargs):
|
|
91
|
+
if riskfree is None:
|
|
92
|
+
riskfree=0
|
|
93
|
+
if isinstance(riskfree,pd.DataFrame) and not riskfree.empty:
|
|
94
|
+
riskfree=riskfree[riskfree.columns[0]]
|
|
95
|
+
df=df.sub(riskfree,axis=0)
|
|
96
|
+
R=12*df.mean(axis=0)
|
|
97
|
+
V=np.sqrt(12)*df.std(axis=0,ddof=1)
|
|
98
|
+
return R/V
|
|
99
|
+
|
|
100
|
+
def sharpeadj(df,base=None,**kwargs):
|
|
101
|
+
R=12*df.mean(axis=0)
|
|
102
|
+
V=np.sqrt(12)*df.std(axis=0,ddof=1)
|
|
103
|
+
SR=R/V
|
|
104
|
+
lambda_1=(1/6)*skewness(df)
|
|
105
|
+
lambda_2=(1/24)*exckurt(df)
|
|
106
|
+
adjustment_factor=1 + (lambda_1* SR) - (lambda_2* (SR**2))
|
|
107
|
+
return SR * adjustment_factor
|
|
108
|
+
|
|
109
|
+
def calmar(df,lookback=36,**kwargs):
|
|
110
|
+
# compute maxdd
|
|
111
|
+
if len(df)>36:
|
|
112
|
+
df=df.iloc[-lookback:]
|
|
113
|
+
nav=(1+_zeroInsert(df)).cumprod()
|
|
114
|
+
peak=nav.expanding(1).max()
|
|
115
|
+
currDD=(nav/peak)-1.0
|
|
116
|
+
maxDD=-currDD.iloc[1:].min(axis=0)
|
|
117
|
+
maxDD[maxDD==0]=np.nan
|
|
118
|
+
numPts=df.notna().sum()
|
|
119
|
+
annRet=(((1+df).prod(min_count=1))**(12/numPts))-1
|
|
120
|
+
out=annRet/maxDD
|
|
121
|
+
#out[:]="n/d"
|
|
122
|
+
#out[maxDD.isna()]="n/a (no drawdown)"
|
|
123
|
+
return out
|
|
124
|
+
|
|
125
|
+
def omega(df,**kwargs):
|
|
126
|
+
ypos=df.copy()
|
|
127
|
+
ypos[ypos<=0]=np.nan
|
|
128
|
+
yneg=df.copy()
|
|
129
|
+
yneg[yneg>=0]=np.nan
|
|
130
|
+
A=ypos.mean(axis=0)
|
|
131
|
+
B=-yneg.mean(axis=0)
|
|
132
|
+
return A/B
|
|
133
|
+
|
|
134
|
+
def sortino(df,**kwargs):
|
|
135
|
+
# as on pivotalbase
|
|
136
|
+
MAR=0
|
|
137
|
+
ye=df-MAR
|
|
138
|
+
yn=ye.clip(upper=0)
|
|
139
|
+
exrt=ye.mean(axis=0)
|
|
140
|
+
ddev=np.sqrt((yn**2).mean(axis=0))
|
|
141
|
+
ddev[ddev==0]=np.nan
|
|
142
|
+
return np.sqrt(12)*exrt/ddev
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def ddev(df,**kwargs):
|
|
146
|
+
yn=df.clip(upper=0)
|
|
147
|
+
return np.sqrt((yn**2).mean(axis=0))
|
|
148
|
+
|
|
149
|
+
def hitratio(df,**kwargs):
|
|
150
|
+
A=(df>=0).sum(axis=0)
|
|
151
|
+
B=df.notna().sum(axis=0)
|
|
152
|
+
B[B==0]=np.nan
|
|
153
|
+
return A/B
|
|
154
|
+
|
|
155
|
+
#=========================================
|
|
156
|
+
|
|
157
|
+
def alpha(df,base=None,**kwargs):
|
|
158
|
+
if base is None:
|
|
159
|
+
return _outputNull(df,title='alpha')
|
|
160
|
+
df,base=_nanAlign(df,base)
|
|
161
|
+
ex=base.mean()
|
|
162
|
+
ey=df.mean()
|
|
163
|
+
base=base-ex
|
|
164
|
+
df=df-ey
|
|
165
|
+
exy=(base*df).mean()
|
|
166
|
+
exx=(base**2).mean()
|
|
167
|
+
exx[exx.abs()<1e-17]=np.nan
|
|
168
|
+
beta=exy/exx
|
|
169
|
+
alpha=ey-beta*ex
|
|
170
|
+
return 12*alpha
|
|
171
|
+
|
|
172
|
+
def beta(df,base=None,**kwargs):
|
|
173
|
+
if base is None:
|
|
174
|
+
return _outputNull(df,title='beta')
|
|
175
|
+
df,base=_nanAlign(df,base)
|
|
176
|
+
base=base-base.mean()
|
|
177
|
+
df=df-df.mean()
|
|
178
|
+
exy=(base*df).mean()
|
|
179
|
+
exx=(base**2).mean()
|
|
180
|
+
beta=exy/exx
|
|
181
|
+
return beta
|
|
182
|
+
|
|
183
|
+
def corr(df,base=None,**kwargs):
|
|
184
|
+
if base is None:
|
|
185
|
+
return _outputNull(df,title='corr')
|
|
186
|
+
if isinstance(base,pd.DataFrame):
|
|
187
|
+
base=base[base.columns[0]]
|
|
188
|
+
return df.apply(lambda x:x.corr(base))
|
|
189
|
+
|
|
190
|
+
def r2(df,base=None,**kwargs):
|
|
191
|
+
if base is None:
|
|
192
|
+
return _outputNull(df,title='r2')
|
|
193
|
+
if isinstance(base,pd.DataFrame):
|
|
194
|
+
base=base[base.columns[0]]
|
|
195
|
+
return df.apply(lambda x:x.corr(base))**2
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def numpts(df,**kwargs):
|
|
199
|
+
return df.notna().sum(axis=0)
|
|
200
|
+
|
|
201
|
+
def currentvalue(df,**kwargs):
|
|
202
|
+
return df.iloc[-1]
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import warnings
|
|
2
|
+
import pandas as pd
|
|
3
|
+
import pdb
|
|
4
|
+
|
|
5
|
+
from .catalog import stat_dict
|
|
6
|
+
|
|
7
|
+
def _islist(x):
|
|
8
|
+
return isinstance(x,list)
|
|
9
|
+
|
|
10
|
+
def _isscalar(x):
|
|
11
|
+
return not isinstance(x,list)
|
|
12
|
+
|
|
13
|
+
def _computestart(start=None,end=None,lookback=None):
|
|
14
|
+
if start:
|
|
15
|
+
return start
|
|
16
|
+
else:
|
|
17
|
+
return None if (not all([end,lookback])) else (pd.Timestamp(end)-pd.DateOffset(months=lookback-1)).strftime("%Y-%m")
|
|
18
|
+
|
|
19
|
+
def _computelookback(start,end):
|
|
20
|
+
start_dt,end_dt = [pd.to_datetime(_) for _ in [start,end]]
|
|
21
|
+
return (12*(end_dt.year - start_dt.year) + (end_dt.month - start_dt.month))+1
|
|
22
|
+
|
|
23
|
+
def _computeperiod(start,end):
|
|
24
|
+
if all([start,end]):
|
|
25
|
+
return f"{start}:{end}"
|
|
26
|
+
elif start:
|
|
27
|
+
return f"{start}:"
|
|
28
|
+
elif end:
|
|
29
|
+
return f":{end}"
|
|
30
|
+
else:
|
|
31
|
+
return ""
|
|
32
|
+
|
|
33
|
+
def _computeperiodformat(start,end):
|
|
34
|
+
def _safestr(_):
|
|
35
|
+
return pd.Timestamp(_).strftime("%b%y") if _ else ""
|
|
36
|
+
start,end=_safestr(start),_safestr(end)
|
|
37
|
+
if all([start,end]):
|
|
38
|
+
return f"{start}-{end}"
|
|
39
|
+
elif start:
|
|
40
|
+
return f"{start}-"
|
|
41
|
+
elif end:
|
|
42
|
+
return f"-{end}"
|
|
43
|
+
else:
|
|
44
|
+
return ""
|
|
45
|
+
|
|
46
|
+
def _completeparams(params={},ticker=None,label=None,label_as=None):
|
|
47
|
+
if not label:
|
|
48
|
+
label=_computelabel(params=params,ticker=ticker,label_as=label_as)
|
|
49
|
+
out={_:__ for _,__ in params.items()}
|
|
50
|
+
out.update(label=label,ticker=ticker)
|
|
51
|
+
return out
|
|
52
|
+
|
|
53
|
+
def _build_period_struct(
|
|
54
|
+
period=None, # has precedence over start, end
|
|
55
|
+
start=None,
|
|
56
|
+
end=None,
|
|
57
|
+
lookback=None,
|
|
58
|
+
_default_start=None,
|
|
59
|
+
_default_end=None,
|
|
60
|
+
):
|
|
61
|
+
# period => start,end
|
|
62
|
+
|
|
63
|
+
if period:
|
|
64
|
+
if _isscalar(period):
|
|
65
|
+
period=[period]
|
|
66
|
+
period=[_ if isinstance(_,str) else "" for _ in period]
|
|
67
|
+
start=[]
|
|
68
|
+
end=[]
|
|
69
|
+
period_parse=[_.split(":") if ":" in _ else ["",""] for _ in period]
|
|
70
|
+
start=[_[0] for _ in period_parse]
|
|
71
|
+
end=[_[1] for _ in period_parse]
|
|
72
|
+
|
|
73
|
+
# align start,end
|
|
74
|
+
if _isscalar(start):
|
|
75
|
+
start=[start]
|
|
76
|
+
if _isscalar(end):
|
|
77
|
+
end=[end]
|
|
78
|
+
if _isscalar(lookback):
|
|
79
|
+
lookback=[lookback]
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
num_periods=max(len(start),len(end),len(lookback))
|
|
83
|
+
if len(start)==1:
|
|
84
|
+
start=start*num_periods
|
|
85
|
+
if len(end)==1:
|
|
86
|
+
end=end*num_periods
|
|
87
|
+
if len(lookback)==1:
|
|
88
|
+
lookback=lookback*num_periods
|
|
89
|
+
if len(start)!=num_periods:
|
|
90
|
+
warnings.warn('mismatch len(start)!=num_periods')
|
|
91
|
+
if len(end)!=num_periods:
|
|
92
|
+
warnings.warn('mismatch len(end)!=num_periods')
|
|
93
|
+
if len(lookback)!=num_periods:
|
|
94
|
+
warnings.warn('mismatch len(lookback)!=num_periods')
|
|
95
|
+
|
|
96
|
+
# end x lookback => start
|
|
97
|
+
end=[_ if _ else _default_end for _ in end]
|
|
98
|
+
start=[_computestart(start_i,end_i,lookback_i) for start_i,end_i,lookback_i in zip(start,end,lookback)]
|
|
99
|
+
start=[_ if _ else _default_start for _ in start]
|
|
100
|
+
|
|
101
|
+
return [
|
|
102
|
+
dict(
|
|
103
|
+
start=start_i,
|
|
104
|
+
end=end_i,
|
|
105
|
+
lookback=_computelookback(start_i,end_i),
|
|
106
|
+
period=_computeperiod(start_i,end_i),
|
|
107
|
+
periodformat=_computeperiodformat(start_i,end_i),
|
|
108
|
+
)
|
|
109
|
+
for start_i, end_i in zip(start,end)
|
|
110
|
+
]
|
|
111
|
+
|
|
112
|
+
def _computelabel(params=None,ticker=None,label_as=None):
|
|
113
|
+
if not label_as:
|
|
114
|
+
label_as="ticker"
|
|
115
|
+
if "+" in label_as:
|
|
116
|
+
prefix_type,suffix_type=label_as.split("+")
|
|
117
|
+
else:
|
|
118
|
+
prefix_type,suffix_type=label_as,None
|
|
119
|
+
prefix=stat_dict[ticker][prefix_type]
|
|
120
|
+
if suffix_type=="lookback":
|
|
121
|
+
suffix=f"{params['lookback']}M"
|
|
122
|
+
elif suffix_type=="period":
|
|
123
|
+
suffix=params['periodformat']
|
|
124
|
+
else:
|
|
125
|
+
suffix=None
|
|
126
|
+
return f"{prefix} ({suffix})" if suffix else prefix
|
|
127
|
+
|
|
128
|
+
def configureParams(
|
|
129
|
+
_usecase=None,
|
|
130
|
+
stat=None,
|
|
131
|
+
label=None,
|
|
132
|
+
label_as=None,
|
|
133
|
+
**providedPeriodInputs
|
|
134
|
+
):
|
|
135
|
+
usecasemap={
|
|
136
|
+
1:dict(start=None,end=None,stat="sharpe"),
|
|
137
|
+
2:dict(lookback=[12,18],label_as="ticker+lookback",stat=['sharpe','annvol']),
|
|
138
|
+
3:dict(start="2020-01",label_as="name+period",stat=['sharpe','annvol']),
|
|
139
|
+
}
|
|
140
|
+
if _usecase:
|
|
141
|
+
return configureParams(_default_start="2020-01",_default_end="2025-07",**usecasemap[_usecase])
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
params=_build_period_struct(**providedPeriodInputs)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
if _isscalar(stat):
|
|
148
|
+
stats=[stat]
|
|
149
|
+
else:
|
|
150
|
+
stats=stat
|
|
151
|
+
|
|
152
|
+
if len(params)==1 and len(stats)>1:
|
|
153
|
+
params=params*len(stats)
|
|
154
|
+
|
|
155
|
+
elif len(params)>1 and len(stats)==1:
|
|
156
|
+
stats=stats*len(params)
|
|
157
|
+
|
|
158
|
+
if len(params)!=len(stats):
|
|
159
|
+
warnings.warn("mismatch len(params)!=len(stat_tickers)")
|
|
160
|
+
|
|
161
|
+
if _isscalar(label):
|
|
162
|
+
label=[label]
|
|
163
|
+
if len(label)==1:
|
|
164
|
+
label=label*len(params)
|
|
165
|
+
if len(params)!=len(label):
|
|
166
|
+
warnings.warn("mismatch len(params)!=len(label)")
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
out=[
|
|
170
|
+
_completeparams(params=param_i,ticker=ticker_i,label=label_i,label_as=label_as)
|
|
171
|
+
for param_i, ticker_i,label_i in zip(params,stats,label)
|
|
172
|
+
]
|
|
173
|
+
|
|
174
|
+
return out
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
import pdb
|
|
3
|
+
|
|
4
|
+
from . import kernel as statsKernel
|
|
5
|
+
|
|
6
|
+
def _align2target(df,target=None):
|
|
7
|
+
out=pd.DataFrame(index=target.index,columns=df.columns)
|
|
8
|
+
common_idx=[_ for _ in target.index.tolist() if _ in df.index.tolist()]
|
|
9
|
+
out.loc[common_idx,:]=df.loc[common_idx,:].copy()
|
|
10
|
+
return out
|
|
11
|
+
|
|
12
|
+
def basicstats(
|
|
13
|
+
df,
|
|
14
|
+
base=None,
|
|
15
|
+
riskfree=None,
|
|
16
|
+
ticker=None,
|
|
17
|
+
start=None,
|
|
18
|
+
end=None,
|
|
19
|
+
label=None,
|
|
20
|
+
_roundoff=6,
|
|
21
|
+
**overflowParams,
|
|
22
|
+
):
|
|
23
|
+
|
|
24
|
+
if not ticker:
|
|
25
|
+
return pd.DataFrame()
|
|
26
|
+
|
|
27
|
+
if any([start,end]):
|
|
28
|
+
df=df.loc[start:end]
|
|
29
|
+
if base is not None:
|
|
30
|
+
base=base.loc[start:end]
|
|
31
|
+
if riskfree is not None:
|
|
32
|
+
riskfree=riskfree.loc[start:end]
|
|
33
|
+
|
|
34
|
+
if base is not None:
|
|
35
|
+
base=_align2target(base,target=df)
|
|
36
|
+
if riskfree is not None:
|
|
37
|
+
riskfree=_align2target(riskfree,target=df)
|
|
38
|
+
if not label:
|
|
39
|
+
label=ticker
|
|
40
|
+
|
|
41
|
+
out=getattr(statsKernel,ticker)(df,base=base,riskfree=riskfree,start=None,end=None,label=None)
|
|
42
|
+
|
|
43
|
+
if isinstance(out,pd.DataFrame):
|
|
44
|
+
out=out[out.columns[0]]
|
|
45
|
+
if isinstance(out,pd.Series):
|
|
46
|
+
out.name=label
|
|
47
|
+
out=round(out,_roundoff)
|
|
48
|
+
|
|
49
|
+
return out
|
|
50
|
+
|
|
51
|
+
|
pivotalpath/version.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: pivotalpath
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: The industry standard for hedge fund indices data and analytics
|
|
5
|
+
Author-email: Your Name <your.email@example.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: >=3.7
|
|
8
|
+
Requires-Dist: pandas >=1.3.0
|
|
9
|
+
Requires-Dist: numpy >=1.20.0
|
|
10
|
+
Requires-Dist: requests >=2.25.0
|
|
11
|
+
Requires-Dist: python-dateutil >=2.8.0
|
|
12
|
+
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
pivotalpath/__init__.py,sha256=JXuQsd1ojdAlHk2WPGh7L_votDt-BSWX3OH-Gqwju20,1463
|
|
2
|
+
pivotalpath/version.py,sha256=i_J42cJKAhNEnqxy7xhrN3kJTwi8BRU5dVTdVERm0gc,69
|
|
3
|
+
pivotalpath/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
pivotalpath/core/__init__.py,sha256=EzD2XiPCsjHJCSrgVcZAcHgkFKPr6dEu7tMJriDQj_k,417
|
|
5
|
+
pivotalpath/core/examples.py,sha256=CtCqNMCC2-P2mrInQeMvnpbGn-rtvkhzZ_a42mbz0eo,1970
|
|
6
|
+
pivotalpath/core/important_tickers.py,sha256=xHOAR8wMTZXS5_0PH8_V6WFQpayxpqmcIPsi_yfnIp0,92
|
|
7
|
+
pivotalpath/core/main_library.py,sha256=oTbxiYaVDXjmO3Jr18hjj3Y15PrI-R9KSBFBTM_yn-w,1486
|
|
8
|
+
pivotalpath/data/__init__.py,sha256=tNyy_5DF0XGYxqO_SG0vWJfZmBuC437HY1dAwnH31Fw,3288
|
|
9
|
+
pivotalpath/data/api.py,sha256=my_APfA0Mv_Fy97x2XHCswWfaotSJW_aMu2aPZXlcKQ,1132
|
|
10
|
+
pivotalpath/data/utils.py,sha256=OYmRB2HSlX-7ESU2VokbnanaY97ACvWdFe72ZRH1rf0,1435
|
|
11
|
+
pivotalpath/llm/__init__.py,sha256=q786GSBYq76CmF9xxAuwN-y1d04c25v0eI5hhPUjddY,459
|
|
12
|
+
pivotalpath/llm/interface.py,sha256=P8V9GxEYJ-WLP8K_c9TXzOx3GhptHVNy-rt455ybMUc,7053
|
|
13
|
+
pivotalpath/stats/__init__.py,sha256=z9XQKid32JI3G8KZBEfukmqhOCjMooD_9x-yZJ9bkJg,1378
|
|
14
|
+
pivotalpath/stats/catalog.py,sha256=LJ_5JpV04b0i6DPa_LwWzXlXhzn2wsxkSS74jeV0uho,4306
|
|
15
|
+
pivotalpath/stats/kernel.py,sha256=1X2oFYrjJBhEJ630PMIJWmgZimKgyjgVrmwbxRhDXcE,4940
|
|
16
|
+
pivotalpath/stats/params.py,sha256=UuKMqFHB6nBslzTbCwzJKoEzSnEybdPdYnbJ7p3ECpY,5115
|
|
17
|
+
pivotalpath/stats/wrapper.py,sha256=CW8R892dHufRh1fwm4aKtKPeZefMPqcgM92pwdF_P0g,1297
|
|
18
|
+
pivotalpath-1.0.0.dist-info/METADATA,sha256=JlbGppWdC-pB6ZSLb8Qc4COIREeqKAgfeMQSpPKH8-M,358
|
|
19
|
+
pivotalpath-1.0.0.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
|
|
20
|
+
pivotalpath-1.0.0.dist-info/top_level.txt,sha256=qFWuReiHrH_6fShiOREDcVd0PEbCvMhilLJDpOLQBY8,12
|
|
21
|
+
pivotalpath-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pivotalpath
|