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.
@@ -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()
@@ -0,0 +1,8 @@
1
+ import numpy as np
2
+
3
+
4
+ def cov_to_corr(cov: np.ndarray) -> np.ndarray:
5
+ """Helper to convert covariance matrix to correlation matrix."""
6
+ vols = np.sqrt(np.diag(cov))
7
+ outer_vols = np.outer(vols, vols)
8
+ return cov / outer_vols