pyalloq-core 0.1.10__tar.gz
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.
- pyalloq_core-0.1.10/PKG-INFO +50 -0
- pyalloq_core-0.1.10/README.md +39 -0
- pyalloq_core-0.1.10/pyproject.toml +28 -0
- pyalloq_core-0.1.10/pyproject.toml.orig +22 -0
- pyalloq_core-0.1.10/src/pyalloq_core/__init__.py +0 -0
- pyalloq_core-0.1.10/src/pyalloq_core/data.py +74 -0
- pyalloq_core-0.1.10/src/pyalloq_core/enums.py +21 -0
- pyalloq_core-0.1.10/src/pyalloq_core/interfaces.py +101 -0
- pyalloq_core-0.1.10/src/pyalloq_core/pipeline.py +43 -0
- pyalloq_core-0.1.10/src/pyalloq_core/py.typed +1 -0
- pyalloq_core-0.1.10/src/pyalloq_core/results.py +26 -0
- pyalloq_core-0.1.10/src/pyalloq_core/utils.py +8 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: pyalloq-core
|
|
3
|
+
Version: 0.1.10
|
|
4
|
+
Summary: Pure interfaces, Enums, StrategyPipeline, and MarketData for PyAlloq.
|
|
5
|
+
Author: Siddeshkanth
|
|
6
|
+
Author-email: Siddeshkanth <pyalloq-info@alloq-alpha.com>
|
|
7
|
+
Requires-Dist: numpy>=1.24
|
|
8
|
+
Requires-Dist: pandas>=2.0
|
|
9
|
+
Requires-Python: >=3.11
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
12
|
+
# pyalloq-core
|
|
13
|
+
|
|
14
|
+
`pyalloq-core` provides the foundational data structures, pure interfaces, and core abstractions for the **PyAlloq** quantitative portfolio optimization SDK.
|
|
15
|
+
|
|
16
|
+
## Key Modules & Abstractions
|
|
17
|
+
|
|
18
|
+
- **`MarketData`**: Standardized parameter object holding aligned price time-series (`pd.DataFrame`), optional asset features (`dict[str, pd.DataFrame]`), cross-sectional data, asset lists, risk-free rates, and risk aversion parameters. Includes zero-lookahead time slicing (`slice_time`).
|
|
19
|
+
- **`BaseAllocator`**: Abstract base class for all portfolio allocation engines (Markowitz, Risk Parity, HRP, NCO, Deep Learning allocators, etc.).
|
|
20
|
+
- **`BaseReturnEstimator`**: Abstract base class for expected return estimators (Classical, EWMA, Black-Litterman, Factor models, Deep Learning).
|
|
21
|
+
- **`BaseCovarianceEstimator`**: Abstract base class for covariance matrix estimators (Empirical, EWMA, Ledoit-Wolf, Semi-covariance, RMT).
|
|
22
|
+
- **`StrategyPipeline`**: Pipeline orchestrator linking return estimators, covariance estimators, and portfolio allocators into an end-to-end strategy execution object.
|
|
23
|
+
- **`OptimizationResult`**: Standardized result container storing optimized portfolio weights, solver status, and metadata.
|
|
24
|
+
|
|
25
|
+
## Installation
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
uv add pyalloq-core
|
|
29
|
+
# Or inside workspace
|
|
30
|
+
uv sync
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Quick Example
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
import pandas as pd
|
|
37
|
+
from pyalloq_core.data import MarketData
|
|
38
|
+
from pyalloq_core.enums import ObjectiveFunction
|
|
39
|
+
|
|
40
|
+
# Create MarketData container
|
|
41
|
+
prices = pd.DataFrame(
|
|
42
|
+
{"AAPL": [150.0, 152.5, 151.0], "MSFT": [300.0, 305.0, 302.0]},
|
|
43
|
+
index=pd.date_range("2024-01-01", periods=3)
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
data = MarketData(prices=prices, risk_free_rate=0.04)
|
|
47
|
+
|
|
48
|
+
print(data.assets) # ['AAPL', 'MSFT']
|
|
49
|
+
print(data.prices.head())
|
|
50
|
+
```
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# pyalloq-core
|
|
2
|
+
|
|
3
|
+
`pyalloq-core` provides the foundational data structures, pure interfaces, and core abstractions for the **PyAlloq** quantitative portfolio optimization SDK.
|
|
4
|
+
|
|
5
|
+
## Key Modules & Abstractions
|
|
6
|
+
|
|
7
|
+
- **`MarketData`**: Standardized parameter object holding aligned price time-series (`pd.DataFrame`), optional asset features (`dict[str, pd.DataFrame]`), cross-sectional data, asset lists, risk-free rates, and risk aversion parameters. Includes zero-lookahead time slicing (`slice_time`).
|
|
8
|
+
- **`BaseAllocator`**: Abstract base class for all portfolio allocation engines (Markowitz, Risk Parity, HRP, NCO, Deep Learning allocators, etc.).
|
|
9
|
+
- **`BaseReturnEstimator`**: Abstract base class for expected return estimators (Classical, EWMA, Black-Litterman, Factor models, Deep Learning).
|
|
10
|
+
- **`BaseCovarianceEstimator`**: Abstract base class for covariance matrix estimators (Empirical, EWMA, Ledoit-Wolf, Semi-covariance, RMT).
|
|
11
|
+
- **`StrategyPipeline`**: Pipeline orchestrator linking return estimators, covariance estimators, and portfolio allocators into an end-to-end strategy execution object.
|
|
12
|
+
- **`OptimizationResult`**: Standardized result container storing optimized portfolio weights, solver status, and metadata.
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
uv add pyalloq-core
|
|
18
|
+
# Or inside workspace
|
|
19
|
+
uv sync
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Quick Example
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
import pandas as pd
|
|
26
|
+
from pyalloq_core.data import MarketData
|
|
27
|
+
from pyalloq_core.enums import ObjectiveFunction
|
|
28
|
+
|
|
29
|
+
# Create MarketData container
|
|
30
|
+
prices = pd.DataFrame(
|
|
31
|
+
{"AAPL": [150.0, 152.5, 151.0], "MSFT": [300.0, 305.0, 302.0]},
|
|
32
|
+
index=pd.date_range("2024-01-01", periods=3)
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
data = MarketData(prices=prices, risk_free_rate=0.04)
|
|
36
|
+
|
|
37
|
+
print(data.assets) # ['AAPL', 'MSFT']
|
|
38
|
+
print(data.prices.head())
|
|
39
|
+
```
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["uv_build>=0.10.9,<0.11.0"]
|
|
3
|
+
build-backend = "uv_build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pyalloq-core"
|
|
7
|
+
version = "0.1.10"
|
|
8
|
+
description = "Pure interfaces, Enums, StrategyPipeline, and MarketData for PyAlloq."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"numpy>=1.24",
|
|
13
|
+
"pandas>=2.0",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
[[project.authors]]
|
|
17
|
+
name = "Siddeshkanth"
|
|
18
|
+
email = "pyalloq-info@alloq-alpha.com"
|
|
19
|
+
|
|
20
|
+
[dependency-groups]
|
|
21
|
+
dev = [
|
|
22
|
+
"pytest>=8",
|
|
23
|
+
"ruff>=0.6",
|
|
24
|
+
"mypy>=1.11",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[tool.setuptools.packages.find]
|
|
28
|
+
where = ["src/"]
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["uv_build>=0.10.9,<0.11.0"]
|
|
3
|
+
build-backend = "uv_build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pyalloq-core"
|
|
7
|
+
version = "0.1.10"
|
|
8
|
+
description = "Pure interfaces, Enums, StrategyPipeline, and MarketData for PyAlloq."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
authors = [{ name = "Siddeshkanth", email = "pyalloq-info@alloq-alpha.com" }]
|
|
12
|
+
|
|
13
|
+
dependencies = [
|
|
14
|
+
"numpy>=1.24",
|
|
15
|
+
"pandas>=2.0",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
[dependency-groups]
|
|
19
|
+
dev = ["pytest>=8", "ruff>=0.6", "mypy>=1.11"]
|
|
20
|
+
|
|
21
|
+
[tool.setuptools.packages.find]
|
|
22
|
+
where = ["src/"]
|
|
File without changes
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
from dataclasses import dataclass, field
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@dataclass(kw_only=True)
|
|
6
|
+
class MarketData:
|
|
7
|
+
"""
|
|
8
|
+
Standardized Parameter Object for all financial data.
|
|
9
|
+
Serves as a single source of truth across the pyalloq SDK
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
assets: list[str] = field(default_factory=list)
|
|
13
|
+
prices: pd.DataFrame
|
|
14
|
+
# Time Series features (e.g: Volume, Factor returns, Macro Indicators, Alternative Data)
|
|
15
|
+
features: dict[str, pd.DataFrame] = field(default_factory=dict)
|
|
16
|
+
# Cross Sectional Data (e.g: Market Caps, Sector Mappings)
|
|
17
|
+
cross_sectional: pd.DataFrame | None = None
|
|
18
|
+
# Risk free rate (Contant or Time Series)
|
|
19
|
+
risk_free_rate: pd.Series | float = 0.0
|
|
20
|
+
|
|
21
|
+
risk_aversion: pd.Series | float = 1.0
|
|
22
|
+
|
|
23
|
+
def __post_init__(self) -> None:
|
|
24
|
+
if not self.assets and self.prices is not None and not self.prices.empty:
|
|
25
|
+
self.assets = list(self.prices.columns)
|
|
26
|
+
self.validate_alignment()
|
|
27
|
+
|
|
28
|
+
def validate_alignment(self) -> None:
|
|
29
|
+
"""
|
|
30
|
+
Ensures Time Series Data aligns perfectly to prevent look-ahead bias.
|
|
31
|
+
"""
|
|
32
|
+
for feat_name, df_feat in self.features.items():
|
|
33
|
+
if not self.prices.index.equals(df_feat.index):
|
|
34
|
+
raise ValueError(
|
|
35
|
+
f"Data misalignment: Feature: {feat_name} index does not perfectly match 'prices' index."
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
if isinstance(self.risk_free_rate, pd.Series):
|
|
39
|
+
if not self.prices.index.equals(self.risk_free_rate.index):
|
|
40
|
+
raise ValueError(
|
|
41
|
+
"Data misalignment: 'risk_free_rate' series index does not perfectly match 'prices' index."
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
def slice_time(
|
|
45
|
+
self,
|
|
46
|
+
end_date: pd.Timestamp,
|
|
47
|
+
lookback: int | None = None,
|
|
48
|
+
) -> "MarketData":
|
|
49
|
+
"""
|
|
50
|
+
Returns a new MarketData instance safely sliced for a backtest window.
|
|
51
|
+
"""
|
|
52
|
+
sliced_prices = self.prices.loc[:end_date]
|
|
53
|
+
if lookback is not None:
|
|
54
|
+
sliced_prices = sliced_prices.iloc[-lookback:]
|
|
55
|
+
|
|
56
|
+
sliced_features: dict[str, pd.DataFrame] = {}
|
|
57
|
+
for name, feat in self.features.items():
|
|
58
|
+
sliced_feat = feat.loc[:end_date]
|
|
59
|
+
if lookback is not None:
|
|
60
|
+
sliced_feat = sliced_feat.iloc[-lookback:]
|
|
61
|
+
sliced_features[name] = sliced_feat
|
|
62
|
+
|
|
63
|
+
sliced_rf = self.risk_free_rate
|
|
64
|
+
if isinstance(self.risk_free_rate, pd.Series):
|
|
65
|
+
sliced_rf = self.risk_free_rate.loc[:end_date]
|
|
66
|
+
if lookback is not None:
|
|
67
|
+
sliced_rf = sliced_rf.iloc[-lookback:]
|
|
68
|
+
|
|
69
|
+
return self.__class__(
|
|
70
|
+
prices=sliced_prices,
|
|
71
|
+
features=sliced_features,
|
|
72
|
+
cross_sectional=self.cross_sectional,
|
|
73
|
+
risk_free_rate=sliced_rf,
|
|
74
|
+
)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from enum import Enum, auto
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ObjectiveFunction(Enum):
|
|
5
|
+
MAX_SHARPE = auto()
|
|
6
|
+
MIN_VOLATILITY = auto()
|
|
7
|
+
MAX_RETURN = auto()
|
|
8
|
+
RISK_PARITY = auto()
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ConstraintType(Enum):
|
|
12
|
+
LONG_ONLY = auto()
|
|
13
|
+
MARKET_NEUTRAL = auto()
|
|
14
|
+
CARDINALITY = auto() # Max number of assets
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class DataFrequency(Enum):
|
|
18
|
+
DAILY = 252
|
|
19
|
+
WEEKLY = 52
|
|
20
|
+
MONTHLY = 12
|
|
21
|
+
CRYPTO_DAILY = 365
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
import pandas as pd
|
|
3
|
+
from typing import Any
|
|
4
|
+
from .enums import DataFrequency
|
|
5
|
+
from .data import MarketData
|
|
6
|
+
from .results import OptimizationResult
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class BaseAllocator(ABC):
|
|
10
|
+
def __init__(self, tickers: list[str]) -> None:
|
|
11
|
+
self.tickers = tickers
|
|
12
|
+
|
|
13
|
+
def _resolve_annualization_factor(
|
|
14
|
+
self,
|
|
15
|
+
returns: pd.DataFrame,
|
|
16
|
+
frequency: DataFrequency | str | int | None = None,
|
|
17
|
+
) -> float:
|
|
18
|
+
"""Determines annualization factor from Enum, int or inferred pandas index."""
|
|
19
|
+
if isinstance(frequency, DataFrequency):
|
|
20
|
+
return float(frequency.value)
|
|
21
|
+
if isinstance(frequency, (int, float)):
|
|
22
|
+
return float(frequency)
|
|
23
|
+
if isinstance(frequency, str):
|
|
24
|
+
try:
|
|
25
|
+
float(DataFrequency[frequency.upper()].value)
|
|
26
|
+
except KeyError:
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
if isinstance(returns.index, pd.DatetimeIndex):
|
|
30
|
+
inferred = pd.infer_freq(returns.index)
|
|
31
|
+
if inferred:
|
|
32
|
+
if "W" in inferred:
|
|
33
|
+
return DataFrequency.WEEKLY.value
|
|
34
|
+
if "M" in inferred or "MS" in inferred:
|
|
35
|
+
return DataFrequency.MONTHLY.value
|
|
36
|
+
if "B" in inferred or "D" in inferred:
|
|
37
|
+
return DataFrequency.DAILY.value
|
|
38
|
+
|
|
39
|
+
return DataFrequency.DAILY.value
|
|
40
|
+
|
|
41
|
+
def _prepare_inputs(
|
|
42
|
+
self,
|
|
43
|
+
prices: pd.DataFrame,
|
|
44
|
+
expected_returns: pd.Series | None = None,
|
|
45
|
+
cov_matrix: pd.DataFrame | None = None,
|
|
46
|
+
window_len: int | None = None,
|
|
47
|
+
frequency: DataFrequency | str | int = DataFrequency.DAILY,
|
|
48
|
+
) -> tuple[pd.Series, pd.DataFrame]:
|
|
49
|
+
"""
|
|
50
|
+
Internal helper to resolve inputs. If raw prices are given,
|
|
51
|
+
it computes basic historical matrices. Otherwise, it uses the provided ones.
|
|
52
|
+
"""
|
|
53
|
+
final_returns = expected_returns
|
|
54
|
+
final_cov = cov_matrix
|
|
55
|
+
|
|
56
|
+
returns = prices.iloc[-window_len:] if window_len else prices
|
|
57
|
+
scale_factor = self._resolve_annualization_factor(returns, frequency)
|
|
58
|
+
df_returns = returns.pct_change().dropna()
|
|
59
|
+
|
|
60
|
+
if final_returns is None:
|
|
61
|
+
final_returns = df_returns.mean() * scale_factor
|
|
62
|
+
|
|
63
|
+
if final_cov is None:
|
|
64
|
+
final_cov = df_returns.cov() * scale_factor
|
|
65
|
+
|
|
66
|
+
if final_returns is None or final_cov is None:
|
|
67
|
+
raise ValueError(
|
|
68
|
+
"You must provide either raw 'prices' or BOTH 'expected_returns' and 'cov_matrix'"
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
return (final_returns, final_cov)
|
|
72
|
+
|
|
73
|
+
@abstractmethod
|
|
74
|
+
def allocate(
|
|
75
|
+
self,
|
|
76
|
+
data: MarketData,
|
|
77
|
+
cov_matrix: pd.DataFrame,
|
|
78
|
+
expected_returns: pd.Series | None = None,
|
|
79
|
+
**kwargs: Any,
|
|
80
|
+
) -> OptimizationResult:
|
|
81
|
+
"""
|
|
82
|
+
Base allocate function all subclasses must implement
|
|
83
|
+
"""
|
|
84
|
+
...
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class BaseReturnEstimator(ABC):
|
|
88
|
+
@abstractmethod
|
|
89
|
+
def estimate(self, data: MarketData, **kwargs: Any) -> pd.Series:
|
|
90
|
+
"""
|
|
91
|
+
Takes raw prices (and optional macro/technical features) and
|
|
92
|
+
returns an Nx1 pandas Series of expected returns for the assets.
|
|
93
|
+
"""
|
|
94
|
+
pass
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class BaseCovarianceEstimator(ABC):
|
|
98
|
+
@abstractmethod
|
|
99
|
+
def estimate(self, data: MarketData, **kwargs: Any) -> pd.DataFrame:
|
|
100
|
+
"""Takes a price DataFrame and returns an NxN covariance matrix."""
|
|
101
|
+
pass
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
from typing import Any
|
|
3
|
+
from pyalloq_core.interfaces import (
|
|
4
|
+
BaseAllocator,
|
|
5
|
+
BaseReturnEstimator,
|
|
6
|
+
BaseCovarianceEstimator,
|
|
7
|
+
)
|
|
8
|
+
from pyalloq.estimators.returns.classical.ewma import EWMAReturnEstimator
|
|
9
|
+
from pyalloq.estimators.covariance.empirical import EmpiricalCovariance
|
|
10
|
+
from pyalloq_core.data import MarketData
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class StrategyPipeline:
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
allocator: BaseAllocator,
|
|
17
|
+
returns_estimator: BaseReturnEstimator | None = None,
|
|
18
|
+
cov_estimator: BaseCovarianceEstimator | None = None,
|
|
19
|
+
allocator_kwargs: dict[str, Any] | None = None,
|
|
20
|
+
) -> None:
|
|
21
|
+
self.allocator = allocator
|
|
22
|
+
self.returns_estimator = (
|
|
23
|
+
returns_estimator
|
|
24
|
+
if returns_estimator is not None
|
|
25
|
+
else EWMAReturnEstimator()
|
|
26
|
+
)
|
|
27
|
+
self.cov_estimator = (
|
|
28
|
+
cov_estimator if cov_estimator is not None else EmpiricalCovariance()
|
|
29
|
+
)
|
|
30
|
+
self.allocator_kwargs = allocator_kwargs or {}
|
|
31
|
+
|
|
32
|
+
def generate_weights(
|
|
33
|
+
self,
|
|
34
|
+
data: MarketData,
|
|
35
|
+
) -> pd.Series:
|
|
36
|
+
expected_returns = self.returns_estimator.estimate(data)
|
|
37
|
+
cov_matrix = self.cov_estimator.estimate(data)
|
|
38
|
+
|
|
39
|
+
result = self.allocator.allocate(
|
|
40
|
+
data, cov_matrix=cov_matrix, expected_returns=expected_returns
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
return result.weights
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Marker file for PEP 561
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from typing import Dict, Any, Optional
|
|
3
|
+
import pandas as pd
|
|
4
|
+
import numpy as np
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class OptimizationResult:
|
|
9
|
+
"""Standardized output for all portfolio optimizers."""
|
|
10
|
+
|
|
11
|
+
name = str
|
|
12
|
+
weights: pd.Series
|
|
13
|
+
status: str # e.g., "OPTIMAL", "INFEASIBLE", "SUBOPTIMAL"
|
|
14
|
+
|
|
15
|
+
expected_return: Optional[float] = None
|
|
16
|
+
volatility: Optional[float] = None
|
|
17
|
+
sharpe_ratio: Optional[float] = None
|
|
18
|
+
|
|
19
|
+
# Store solver-specific data (e.g., cvxpy objective value, RL episode reward, HRP linkages)
|
|
20
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
21
|
+
|
|
22
|
+
def clean_weights(self, cutoff: float = 1e-4) -> pd.Series:
|
|
23
|
+
clean = self.weights.copy()
|
|
24
|
+
clean[np.abs(clean) < cutoff] = 0.0
|
|
25
|
+
|
|
26
|
+
return clean / clean.sum()
|