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.
@@ -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)
@@ -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