squarequant 0.1.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.
- squarequant/__init__.py +41 -0
- squarequant/api.py +548 -0
- squarequant/constants.py +35 -0
- squarequant/core/__init__.py +6 -0
- squarequant/core/base.py +166 -0
- squarequant/core/data.py +106 -0
- squarequant/core/metrics.py +1633 -0
- squarequant/plot/__init__.py +7 -0
- squarequant/plot/plot.py +972 -0
- squarequant/utils/__init__.py +6 -0
- squarequant/utils/helpers.py +85 -0
- squarequant-0.1.0.dist-info/LICENSE +21 -0
- squarequant-0.1.0.dist-info/METADATA +217 -0
- squarequant-0.1.0.dist-info/RECORD +16 -0
- squarequant-0.1.0.dist-info/WHEEL +5 -0
- squarequant-0.1.0.dist-info/top_level.txt +1 -0
squarequant/core/base.py
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base classes and helpers for risk metrics
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import pandas as pd
|
|
6
|
+
import numpy as np
|
|
7
|
+
from typing import List, Optional, Union, Dict, Any
|
|
8
|
+
|
|
9
|
+
from squarequant.constants import TRADING_DAYS_PER_YEAR
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class RiskMetricBase:
|
|
13
|
+
"""
|
|
14
|
+
Base class for risk metric calculations that handles common operations
|
|
15
|
+
like date filtering, validation, and window processing.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(self,
|
|
19
|
+
data: pd.DataFrame,
|
|
20
|
+
assets: List[str],
|
|
21
|
+
window: int = TRADING_DAYS_PER_YEAR,
|
|
22
|
+
start: Optional[str] = None,
|
|
23
|
+
end: Optional[str] = None,
|
|
24
|
+
use_returns: bool = True,
|
|
25
|
+
min_periods: Optional[int] = None,
|
|
26
|
+
**kwargs):
|
|
27
|
+
"""
|
|
28
|
+
Initialize the risk metric calculator with common parameters.
|
|
29
|
+
|
|
30
|
+
Parameters:
|
|
31
|
+
data (DataFrame): DataFrame with asset price/returns data
|
|
32
|
+
assets (List[str]): List of asset columns to calculate for
|
|
33
|
+
window (int): Rolling window size in trading days
|
|
34
|
+
start (str, optional): Start date in format 'YYYY-MM-DD'
|
|
35
|
+
end (str, optional): End date in format 'YYYY-MM-DD'
|
|
36
|
+
use_returns (bool): If True, converts price data to returns
|
|
37
|
+
min_periods (int, optional): Minimum periods for rolling calculations
|
|
38
|
+
**kwargs: Additional parameters specific to the metric
|
|
39
|
+
"""
|
|
40
|
+
self.original_data = data
|
|
41
|
+
self.assets = assets
|
|
42
|
+
self.window = window
|
|
43
|
+
self.start = start
|
|
44
|
+
self.end = end
|
|
45
|
+
self.min_periods = min_periods if min_periods is not None else window
|
|
46
|
+
self.kwargs = kwargs
|
|
47
|
+
|
|
48
|
+
# Prepare and validate data
|
|
49
|
+
if use_returns:
|
|
50
|
+
self.data = self._prepare_returns_data()
|
|
51
|
+
else:
|
|
52
|
+
self.data = self._filter_by_date(data)
|
|
53
|
+
|
|
54
|
+
# Get valid assets (those present in the data)
|
|
55
|
+
self.valid_assets = [asset for asset in assets if asset in self.data.columns]
|
|
56
|
+
|
|
57
|
+
# Prepare result DataFrame
|
|
58
|
+
self.result = pd.DataFrame(index=self.data.index, columns=assets)
|
|
59
|
+
|
|
60
|
+
def _prepare_returns_data(self) -> pd.DataFrame:
|
|
61
|
+
"""
|
|
62
|
+
Calculate returns from price data and filter by date.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
DataFrame: Returns data filtered by date range
|
|
66
|
+
"""
|
|
67
|
+
returns = self.original_data.pct_change().dropna()
|
|
68
|
+
return self._filter_by_date(returns)
|
|
69
|
+
|
|
70
|
+
def _filter_by_date(self, df: pd.DataFrame) -> pd.DataFrame:
|
|
71
|
+
"""
|
|
72
|
+
Filter DataFrame by specified date range.
|
|
73
|
+
|
|
74
|
+
Parameters:
|
|
75
|
+
df (DataFrame): DataFrame to filter
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
DataFrame: Filtered DataFrame
|
|
79
|
+
"""
|
|
80
|
+
if self.start or self.end:
|
|
81
|
+
mask = pd.Series(True, index=df.index)
|
|
82
|
+
if self.start:
|
|
83
|
+
mask = mask & (df.index >= self.start)
|
|
84
|
+
if self.end:
|
|
85
|
+
mask = mask & (df.index <= self.end)
|
|
86
|
+
return df[mask]
|
|
87
|
+
return df
|
|
88
|
+
|
|
89
|
+
def _apply_rolling(self,
|
|
90
|
+
func: callable,
|
|
91
|
+
data: Optional[pd.DataFrame] = None,
|
|
92
|
+
**kwargs) -> pd.DataFrame:
|
|
93
|
+
"""
|
|
94
|
+
Apply a rolling window function to the data.
|
|
95
|
+
|
|
96
|
+
Parameters:
|
|
97
|
+
func (callable): Function to apply to rolling window
|
|
98
|
+
data (DataFrame, optional): Data to use (defaults to self.data)
|
|
99
|
+
**kwargs: Additional arguments to pass to the rolling function
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
DataFrame: Result of applying rolling function
|
|
103
|
+
"""
|
|
104
|
+
if data is None:
|
|
105
|
+
data = self.data[self.valid_assets]
|
|
106
|
+
|
|
107
|
+
return data.rolling(
|
|
108
|
+
window=self.window,
|
|
109
|
+
min_periods=self.min_periods
|
|
110
|
+
).apply(func, raw=True, **kwargs)
|
|
111
|
+
|
|
112
|
+
def _finalize_result(self, data: pd.DataFrame) -> pd.DataFrame:
|
|
113
|
+
"""
|
|
114
|
+
Finalize the result DataFrame by filling in valid assets and cleaning up.
|
|
115
|
+
|
|
116
|
+
Parameters:
|
|
117
|
+
data (DataFrame): Calculation result for valid assets
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
DataFrame: Finalized result with all requested assets
|
|
121
|
+
"""
|
|
122
|
+
if not data.empty and self.valid_assets:
|
|
123
|
+
self.result[self.valid_assets] = data
|
|
124
|
+
|
|
125
|
+
# Remove rows with all NaN values
|
|
126
|
+
return self.result.dropna(how='all')
|
|
127
|
+
|
|
128
|
+
def calculate(self) -> pd.DataFrame:
|
|
129
|
+
"""
|
|
130
|
+
Calculate the risk metric. Must be implemented by subclasses.
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
DataFrame: Risk metric for specified assets
|
|
134
|
+
"""
|
|
135
|
+
raise NotImplementedError("Subclasses must implement calculate()")
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class RiskFreeRateHelper:
|
|
139
|
+
"""Helper class to handle risk-free rate calculations"""
|
|
140
|
+
|
|
141
|
+
@staticmethod
|
|
142
|
+
def get_risk_free_rate(returns: pd.DataFrame,
|
|
143
|
+
freerate: Optional[str] = None,
|
|
144
|
+
freerate_value: Optional[float] = None) -> pd.Series:
|
|
145
|
+
"""
|
|
146
|
+
Get daily risk-free rate series without smoothing.
|
|
147
|
+
|
|
148
|
+
Parameters:
|
|
149
|
+
returns (DataFrame): Returns data
|
|
150
|
+
freerate (str, optional): Column name for risk-free rate in returns DataFrame
|
|
151
|
+
freerate_value (float, optional): Constant risk-free rate to use if no column provided
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
Series: Daily risk-free rate series
|
|
155
|
+
"""
|
|
156
|
+
if freerate:
|
|
157
|
+
if freerate not in returns.columns:
|
|
158
|
+
raise ValueError(f"Risk-free rate column '{freerate}' not found in data")
|
|
159
|
+
return returns[freerate]
|
|
160
|
+
elif freerate_value is not None:
|
|
161
|
+
return pd.Series(freerate_value, index=returns.index)
|
|
162
|
+
else:
|
|
163
|
+
# Use a more realistic default risk-free rate of 2% annual
|
|
164
|
+
# Convert to daily rate
|
|
165
|
+
daily_rate = 0.02 / TRADING_DAYS_PER_YEAR
|
|
166
|
+
return pd.Series(daily_rate, index=returns.index)
|
squarequant/core/data.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Data retrieval and processing functionality
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import pandas as pd
|
|
6
|
+
import yfinance as yf
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from datetime import datetime, timedelta
|
|
9
|
+
from typing import List, Optional, Dict, Any
|
|
10
|
+
|
|
11
|
+
from squarequant.constants import VALID_INTERVALS, VALID_COLUMNS
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class DownloadConfig:
|
|
16
|
+
"""Configuration for ticker data download"""
|
|
17
|
+
start_date: str = (datetime.now() - timedelta(days=365)).strftime('%Y-%m-%d')
|
|
18
|
+
end_date: str = datetime.now().strftime('%Y-%m-%d')
|
|
19
|
+
interval: str = '1d'
|
|
20
|
+
columns: Optional[List[str]] = None
|
|
21
|
+
|
|
22
|
+
def __post_init__(self):
|
|
23
|
+
"""Validate configuration parameters"""
|
|
24
|
+
"""Validate configuration parameters"""
|
|
25
|
+
if self.interval not in VALID_INTERVALS:
|
|
26
|
+
raise ValueError(f"Invalid interval. Must be one of {VALID_INTERVALS}")
|
|
27
|
+
try:
|
|
28
|
+
datetime.strptime(self.start_date, '%Y-%m-%d')
|
|
29
|
+
datetime.strptime(self.end_date, '%Y-%m-%d')
|
|
30
|
+
except ValueError:
|
|
31
|
+
raise ValueError("Dates must be in YYYY-MM-DD format")
|
|
32
|
+
|
|
33
|
+
if self.columns is not None:
|
|
34
|
+
invalid_columns = set(self.columns) - VALID_COLUMNS
|
|
35
|
+
if invalid_columns:
|
|
36
|
+
raise ValueError(f"Invalid columns: {invalid_columns}. Must be from {VALID_COLUMNS}")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def download_tickers(tickers: List[str], config: Optional[DownloadConfig] = None) -> pd.DataFrame:
|
|
40
|
+
"""
|
|
41
|
+
Download data for multiple tickers using specified configuration
|
|
42
|
+
|
|
43
|
+
Parameters:
|
|
44
|
+
tickers (List[str]): List of ticker symbols
|
|
45
|
+
config (DownloadConfig, optional): Download configuration
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
pd.DataFrame: DataFrame with ticker data
|
|
49
|
+
"""
|
|
50
|
+
if config is None:
|
|
51
|
+
config = DownloadConfig()
|
|
52
|
+
|
|
53
|
+
# For single ticker, group by column to get a simpler structure
|
|
54
|
+
group_by = 'column' if len(tickers) == 1 else 'ticker'
|
|
55
|
+
|
|
56
|
+
df = yf.download(
|
|
57
|
+
tickers=tickers,
|
|
58
|
+
start=config.start_date,
|
|
59
|
+
end=config.end_date,
|
|
60
|
+
interval=config.interval,
|
|
61
|
+
group_by=group_by,
|
|
62
|
+
auto_adjust=True,
|
|
63
|
+
progress=False
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
if df.empty:
|
|
67
|
+
print(f"Warning: No data found for tickers {tickers}")
|
|
68
|
+
return pd.DataFrame()
|
|
69
|
+
|
|
70
|
+
# Create a new DataFrame with renamed columns
|
|
71
|
+
result = pd.DataFrame(index=df.index)
|
|
72
|
+
|
|
73
|
+
# If specific columns are requested
|
|
74
|
+
if config.columns is not None:
|
|
75
|
+
columns_to_use = config.columns
|
|
76
|
+
else:
|
|
77
|
+
# Use all available columns if none specified
|
|
78
|
+
if len(tickers) == 1:
|
|
79
|
+
columns_to_use = df.columns.tolist()
|
|
80
|
+
else:
|
|
81
|
+
# For multiple tickers, get the unique second level column names
|
|
82
|
+
columns_to_use = df.columns.levels[1].tolist() if isinstance(df.columns, pd.MultiIndex) else []
|
|
83
|
+
|
|
84
|
+
# Single column download case - use ticker as column name
|
|
85
|
+
if len(columns_to_use) == 1:
|
|
86
|
+
single_column = columns_to_use[0]
|
|
87
|
+
for ticker in tickers:
|
|
88
|
+
if len(tickers) == 1:
|
|
89
|
+
if single_column in df.columns:
|
|
90
|
+
result[ticker] = df[single_column]
|
|
91
|
+
else:
|
|
92
|
+
if ticker in df.columns.levels[0] and single_column in df[ticker].columns:
|
|
93
|
+
result[ticker] = df[(ticker, single_column)]
|
|
94
|
+
|
|
95
|
+
# Multiple columns download case - use TICKER_COLUMN format
|
|
96
|
+
else:
|
|
97
|
+
for ticker in tickers:
|
|
98
|
+
for column in columns_to_use:
|
|
99
|
+
if len(tickers) == 1:
|
|
100
|
+
if column in df.columns:
|
|
101
|
+
result[f"{ticker}_{column}"] = df[column]
|
|
102
|
+
else:
|
|
103
|
+
if ticker in df.columns.levels[0] and column in df[ticker].columns:
|
|
104
|
+
result[f"{ticker}_{column}"] = df[(ticker, column)]
|
|
105
|
+
|
|
106
|
+
return result
|