Compare commits

..
Author SHA1 Message Date
ao gong 58a677f8a7 fix(ci): verify the unified quant runtime
CI / lite (pull_request) Successful in 8s
2026-08-24 20:56:07 +08:00
ao gong efaa6fb143 chore: standardize Python 3.13 runtime
CI / lite (pull_request) Successful in 3s
2026-08-23 19:23:20 +08:00
17 changed files with 472 additions and 899 deletions
+15 -2
View File
@@ -16,6 +16,8 @@ permissions:
jobs: jobs:
lite: lite:
runs-on: ubuntu-latest runs-on: ubuntu-latest
env:
UV_PYTHON_DOWNLOADS: never
steps: steps:
- uses: actions/checkout@524e936cd9e579adf00e308bfdf971aebc7de09e - uses: actions/checkout@524e936cd9e579adf00e308bfdf971aebc7de09e
with: with:
@@ -26,7 +28,18 @@ jobs:
if git ls-files .DS_Store | grep -q .; then echo "跟踪 .DS_Store"; exit 1; fi if git ls-files .DS_Store | grep -q .; then echo "跟踪 .DS_Store"; exit 1; fi
if git grep -n -I -E 'sk-[A-Za-z0-9]{20,}|AKIA[0-9A-Z]{16}|ghp_[A-Za-z0-9]{36}|xox[baprs]-[A-Za-z0-9-]{10,}' HEAD | grep -q .; then echo "检出疑似凭证"; exit 1; fi if git grep -n -I -E 'sk-[A-Za-z0-9]{20,}|AKIA[0-9A-Z]{16}|ghp_[A-Za-z0-9]{36}|xox[baprs]-[A-Za-z0-9-]{10,}' HEAD | grep -q .; then echo "检出疑似凭证"; exit 1; fi
echo "Gitea 合规校验通过" echo "Gitea 合规校验通过"
- name: 验证并同步共享运行时
run: |
test "$(python3 --version)" = "Python 3.13.15"
test "$(uv --version | cut -d' ' -f1-2)" = "uv 0.12.3"
uv sync --locked --extra dev
uv run --locked --no-sync python -c 'import sys; assert sys.version_info[:2] == (3, 13)'
- name: 架构模块契约测试 - name: 架构模块契约测试
run: python3 tests/governance/test_module_spec.py run: |
uv run --locked --no-sync python tests/governance/test_module_spec.py
uv run --locked --no-sync python tests/governance/test_ci_contract.py
- name: Syntax check - name: Syntax check
run: git ls-files -z '*.py' | xargs -0 python3 -m py_compile run: git ls-files -z '*.py' | xargs -0 uv run --locked --no-sync python -m py_compile
+1
View File
@@ -0,0 +1 @@
3.13
-18
View File
@@ -23,7 +23,6 @@
- `indicators` — 50+ 技术指标(MACD / KDJ / 布林 / ATR / ADX / 等) - `indicators` — 50+ 技术指标(MACD / KDJ / 布林 / ATR / ADX / 等)
- `data_adapter` — 桥接 qtdb_pro 长表与新模块(rename / long-wide / 复权 / vwap 代理) - `data_adapter` — 桥接 qtdb_pro 长表与新模块(rename / long-wide / 复权 / vwap 代理)
- `backtest` — weight-based 多日仿真(rebalance_table / compute_nav / compare_to_benchmark) - `backtest` — weight-based 多日仿真(rebalance_table / compute_nav / compare_to_benchmark)
- `portfolio_construction` — 多期因子分数 → Top-K → 等权目标权重表
- `metrics` — 绩效(年化收益 / 波动率 / Sharpe / 最大回撤 / Calmar) - `metrics` — 绩效(年化收益 / 波动率 / Sharpe / 最大回撤 / Calmar)
- `factor_library` — 通用方法(turnover / winsorize / IC / OLS / jb_test) - `factor_library` — 通用方法(turnover / winsorize / IC / OLS / jb_test)
- `portfolio_decomp` — 组合分解(risk_parity / mean_variance / 因子归因) - `portfolio_decomp` — 组合分解(risk_parity / mean_variance / 因子归因)
@@ -61,8 +60,6 @@ from quant_engine.alpha_factors import alpha_001, alpha_005, ALPHA158_REGISTRY
from quant_engine.execution import ( from quant_engine.execution import (
ExecutionConfig, simulate_with_daily_data, compute_realized_pnl, ExecutionConfig, simulate_with_daily_data, compute_realized_pnl,
) )
from quant_engine.backtest import run_weight_backtest
from quant_engine.portfolio_construction import scores_to_weight_table
from quant_engine.indicators import macd, bollinger, kdj from quant_engine.indicators import macd, bollinger, kdj
from quant_engine.data_adapter import ( from quant_engine.data_adapter import (
long_to_wide, wide_to_long, rename_tushare_columns, long_to_wide, wide_to_long, rename_tushare_columns,
@@ -75,21 +72,6 @@ from quant_engine.data_adapter import (
df = load_qtdb_daily(["000001.SZ"], "2024-01-01", with_adj=True) df = load_qtdb_daily(["000001.SZ"], "2024-01-01", with_adj=True)
prices, volumes = prepare_execution_inputs(df) prices, volumes = prepare_execution_inputs(df)
result = simulate_with_daily_data(prices, initial_cash=1_000_000.0) result = simulate_with_daily_data(prices, initial_cash=1_000_000.0)
# 多期因子分数 → Top-K 等权组合 → 稳定回测结果
rebalance_weights = scores_to_weight_table(
factor_scores,
top_k=20,
gross_exposure=1.0,
)
backtest = run_weight_backtest(
weights=rebalance_weights,
stock_returns=daily_returns,
initial_capital=1_000_000.0,
benchmark_nav=benchmark_nav,
)
print(backtest.stats())
print(backtest.benchmark_report())
``` ```
## 与 research_results 的关系 ## 与 research_results 的关系
+9
View File
@@ -0,0 +1,9 @@
profile: lite
runtime_contract: v1
language: python
python_version: "3.13"
python_manager: uv
python_root: "."
local_test_command: "python3 tests/governance/test_module_spec.py"
requires_database: false
integration_profile: none
+6 -3
View File
@@ -7,7 +7,7 @@ name = "quant_engine"
version = "0.1.0" version = "0.1.0"
description = "量化研究引擎 —— alpha 因子库 + 执行仿真 + 技术指标 + 数据适配 + 回测工具(v1.2.0 从 research_results 抽出)" description = "量化研究引擎 —— alpha 因子库 + 执行仿真 + 技术指标 + 数据适配 + 回测工具(v1.2.0 从 research_results 抽出)"
readme = "README.md" readme = "README.md"
requires-python = ">=3.11" requires-python = ">=3.13,<3.14"
license = { text = "MIT" } license = { text = "MIT" }
authors = [ authors = [
{ name = "researchhub team" }, { name = "researchhub team" },
@@ -39,7 +39,7 @@ where = ["src"]
[tool.ruff] [tool.ruff]
line-length = 100 line-length = 100
target-version = "py311" target-version = "py313"
[tool.ruff.lint] [tool.ruff.lint]
select = ["E", "F", "W", "I", "N", "UP", "B", "A", "C4", "PT", "RUF"] select = ["E", "F", "W", "I", "N", "UP", "B", "A", "C4", "PT", "RUF"]
@@ -54,10 +54,13 @@ ignore = [
] ]
[tool.mypy] [tool.mypy]
python_version = "3.11" python_version = "3.13"
strict = true strict = true
ignore_missing_imports = true ignore_missing_imports = true
[tool.uv]
index-url = "https://mirrors.cloud.tencent.com/pypi/simple"
[tool.pytest.ini_options] [tool.pytest.ini_options]
testpaths = ["tests"] testpaths = ["tests"]
addopts = "-v --tb=short" addopts = "-v --tb=short"
+10 -57
View File
@@ -13,28 +13,29 @@
```python ```python
from quant_engine.backtest import ( from quant_engine.backtest import (
rebalance_periodic, # 周期性再平衡 compute_nav_from_weights, # 调仓表 → 净值
run_weight_backtest, # 权重 → 统一结果对象 rebalance_table, # 周期性再平衡
compare_to_benchmark, # 策略 vs 基准
weights_to_long_short, # 多空组合 weights_to_long_short, # 多空组合
) )
# 调仓表 → 净值、收益、绩效与基准报告 # 1. 调仓表 → 净值
rebalance_table = rebalance_periodic(target_weights, rebalance_dates, returns.index) nav = compute_nav_from_weights(
result = run_weight_backtest(
weights=rebalance_table, # 每周/每月调仓 weights=rebalance_table, # 每周/每月调仓
stock_returns=returns, # 个股日收益 stock_returns=returns, # 个股日收益
initial_capital=1.0, initial_capital=1.0,
benchmark_nav=benchmark_nav,
) )
print(result.stats())
print(result.benchmark_report()) # 2. 跟基准比
result = compare_to_benchmark(nav, benchmark_nav)
print(result.summary())
``` ```
""" """
from __future__ import annotations from __future__ import annotations
from pathlib import Path
from collections.abc import Mapping, Sequence from collections.abc import Mapping, Sequence
from dataclasses import dataclass
import numpy as np import numpy as np
import pandas as pd import pandas as pd
@@ -45,26 +46,6 @@ from quant_engine.metrics import summary as metrics_summary
logger = get_logger(__name__) logger = get_logger(__name__)
@dataclass(frozen=True, slots=True, eq=False)
class BacktestResult:
"""一次权重回测的稳定结果快照。"""
nav: pd.Series
returns: pd.Series
weights: pd.DataFrame
benchmark_nav: pd.Series | None = None
def stats(self, rf: float = 0.0) -> Mapping[str, float]:
"""返回标准绩效指标。"""
return metrics_summary(self.returns, rf)
def benchmark_report(self, rf: float = 0.0) -> pd.DataFrame:
"""返回策略与基准的对比报告。"""
if self.benchmark_nav is None:
raise ValueError("benchmark_nav is required for benchmark comparison")
return compare_to_benchmark(self.nav, self.benchmark_nav, rf)
# ── 调仓表 → 净值 ────────────────────────────────────── # ── 调仓表 → 净值 ──────────────────────────────────────
@@ -132,29 +113,6 @@ def compute_returns_from_nav(nav: pd.Series) -> pd.Series:
return nav.pct_change().fillna(0.0) return nav.pct_change().fillna(0.0)
def run_weight_backtest(
weights: pd.DataFrame,
stock_returns: pd.DataFrame,
initial_capital: float = 1.0,
tc_rate: float = 0.0,
benchmark_nav: pd.Series | None = None,
) -> BacktestResult:
"""执行权重回测并返回隔离于调用方输入的结果快照。"""
weights_snapshot = weights.copy(deep=True)
nav = compute_nav_from_weights(
weights=weights_snapshot,
stock_returns=stock_returns,
initial_capital=initial_capital,
tc_rate=tc_rate,
)
return BacktestResult(
nav=nav,
returns=compute_returns_from_nav(nav),
weights=weights_snapshot,
benchmark_nav=None if benchmark_nav is None else benchmark_nav.copy(deep=True),
)
# ── 调仓工具 ────────────────────────────────────── # ── 调仓工具 ──────────────────────────────────────
@@ -173,9 +131,6 @@ def rebalance_periodic(
Returns: Returns:
调仓表 DataFrame(all_dates × 股票代码) 调仓表 DataFrame(all_dates × 股票代码)
""" """
if all_dates.empty:
return pd.DataFrame(index=all_dates, columns=target_weights.index, dtype=float)
table = pd.DataFrame(0.0, index=all_dates, columns=target_weights.index) table = pd.DataFrame(0.0, index=all_dates, columns=target_weights.index)
for date in rebalance_dates: for date in rebalance_dates:
if date not in all_dates: if date not in all_dates:
@@ -246,8 +201,6 @@ def compare_to_benchmark(
""" """
# 对齐 index # 对齐 index
common = strategy_nav.index.intersection(benchmark_nav.index) common = strategy_nav.index.intersection(benchmark_nav.index)
if common.empty:
raise ValueError("strategy and benchmark must have overlapping dates")
s = strategy_nav.loc[common] s = strategy_nav.loc[common]
b = benchmark_nav.loc[common] b = benchmark_nav.loc[common]
+2 -6
View File
@@ -312,8 +312,8 @@ def ols_regress(
ss_tot = float(((y_arr - y_arr.mean()) ** 2).sum()) ss_tot = float(((y_arr - y_arr.mean()) ** 2).sum())
r_sq = 1.0 - ss_res / ss_tot if ss_tot > 0 else np.nan r_sq = 1.0 - ss_res / ss_tot if ss_tot > 0 else np.nan
sigma2 = ss_res / max(n - k, 1) sigma2 = ss_res / max(n - k, 1)
# 广义协方差矩阵 = sigma2 * (X'X)^+,伪逆兼容共线因子。 # 协方差矩阵 = sigma2 * (X'X)^-1
xtx_inv = np.linalg.pinv(x_arr.T @ x_arr) if sigma2 > 0 else np.full((k, k), np.nan) xtx_inv = np.linalg.inv(x_arr.T @ x_arr) if sigma2 > 0 else np.full((k, k), np.nan)
se = np.sqrt(np.diag(xtx_inv) * sigma2) se = np.sqrt(np.diag(xtx_inv) * sigma2)
t_vals = coef / se if sigma2 > 0 else np.full_like(coef, np.nan) t_vals = coef / se if sigma2 > 0 else np.full_like(coef, np.nan)
if add_constant: if add_constant:
@@ -513,8 +513,6 @@ def apply_factor_direction(
Returns: Returns:
方向调整后的因子(同向 = 越大越好) 方向调整后的因子(同向 = 越大越好)
""" """
if direction not in {"auto", "forward", "reverse"}:
raise ValueError(f"direction={direction!r} not supported (auto / forward / reverse)")
if factor.empty: if factor.empty:
return factor.copy() return factor.copy()
if direction == "auto": if direction == "auto":
@@ -543,8 +541,6 @@ def cross_sectional_rank_with_direction(
Returns: Returns:
pd.Series(百分位排名 [0, 1],越大越优) pd.Series(百分位排名 [0, 1],越大越优)
""" """
if direction not in {"auto", "forward", "reverse"}:
raise ValueError(f"direction={direction!r} not supported (auto / forward / reverse)")
if df.empty or factor_col not in df.columns: if df.empty or factor_col not in df.columns:
return pd.Series(dtype=float) return pd.Series(dtype=float)
factor = df[factor_col] factor = df[factor_col]
+1 -2
View File
@@ -71,8 +71,7 @@ def max_drawdown(r: pd.Series) -> float:
if len(r) < 2: if len(r) < 2:
return 0.0 return 0.0
nav = (1 + r).cumprod() nav = (1 + r).cumprod()
# 初始资金净值为 1;否则首个观测日的亏损会被误当成新的历史高点。 peak = nav.cummax()
peak = nav.cummax().clip(lower=1.0)
drawdown = (nav - peak) / peak drawdown = (nav - peak) / peak
return float(drawdown.min()) return float(drawdown.min())
-104
View File
@@ -1,104 +0,0 @@
"""因子分数到目标权重的轻量组合构建闭环。"""
from __future__ import annotations
import numpy as np
import pandas as pd
from pandas.api.types import is_numeric_dtype
__all__ = [
"select_top_k",
"equal_weight",
"scores_to_target_weights",
"scores_to_weight_table",
]
def _validate_top_k(top_k: int) -> None:
if isinstance(top_k, bool) or not isinstance(top_k, int) or top_k <= 0:
raise ValueError("top_k must be positive")
def _validate_gross_exposure(gross_exposure: float) -> None:
if not np.isfinite(gross_exposure) or gross_exposure < 0:
raise ValueError("gross_exposure must be finite and non-negative")
def _validate_score_series(scores: pd.Series) -> None:
if not isinstance(scores, pd.Series):
raise TypeError(f"scores must be a pandas Series, got {type(scores).__name__}")
if not scores.index.is_unique:
raise ValueError("scores must contain unique asset labels")
if not is_numeric_dtype(scores.dtype):
raise TypeError("scores must contain numeric values")
def select_top_k(scores: pd.Series, top_k: int, *, largest: bool = True) -> pd.Index:
"""稳定选择最高或最低的 K 个有效因子分数。"""
_validate_top_k(top_k)
_validate_score_series(scores)
valid_scores = scores.dropna()
ordered = valid_scores.sort_values(ascending=not largest, kind="mergesort")
return ordered.iloc[:top_k].index.copy()
def equal_weight(assets: pd.Index, *, gross_exposure: float = 1.0) -> pd.Series:
"""在已选资产间等权分配指定总敞口。"""
_validate_gross_exposure(gross_exposure)
if not assets.is_unique:
raise ValueError("assets must contain unique asset labels")
if assets.empty:
return pd.Series(index=assets.copy(), dtype=float, name="weight")
weight = gross_exposure / len(assets)
return pd.Series(weight, index=assets.copy(), dtype=float, name="weight")
def scores_to_target_weights(
scores: pd.Series,
top_k: int,
*,
gross_exposure: float = 1.0,
largest: bool = True,
) -> pd.Series:
"""把单期因子分数转换为完整股票池目标权重。"""
_validate_score_series(scores)
selected = select_top_k(scores, top_k, largest=largest)
selected_weights = equal_weight(selected, gross_exposure=gross_exposure)
result = pd.Series(0.0, index=scores.index.copy(), dtype=float, name="weight")
result.loc[selected_weights.index] = selected_weights
return result
def scores_to_weight_table(
scores: pd.DataFrame,
top_k: int,
*,
gross_exposure: float = 1.0,
largest: bool = True,
) -> pd.DataFrame:
"""逐调仓日独立构建目标权重表,避免使用未来分数。"""
if not isinstance(scores, pd.DataFrame):
raise TypeError(f"scores must be a pandas DataFrame, got {type(scores).__name__}")
_validate_top_k(top_k)
_validate_gross_exposure(gross_exposure)
if not scores.index.is_unique:
raise ValueError("scores must contain unique rebalance dates")
if not scores.index.is_monotonic_increasing:
raise ValueError("scores rebalance dates must be in chronological order")
if not scores.columns.is_unique:
raise ValueError("scores must contain unique asset labels")
if not all(is_numeric_dtype(dtype) for dtype in scores.dtypes):
raise TypeError("scores must contain numeric values")
if scores.empty:
return pd.DataFrame(index=scores.index.copy(), columns=scores.columns.copy(), dtype=float)
rows = [
scores_to_target_weights(
row,
top_k,
gross_exposure=gross_exposure,
largest=largest,
).to_numpy()
for _, row in scores.iterrows()
]
return pd.DataFrame(rows, index=scores.index.copy(), columns=scores.columns.copy(), dtype=float)
+9 -17
View File
@@ -5,22 +5,9 @@
from __future__ import annotations from __future__ import annotations
from typing import Any
import numpy as np import numpy as np
from numpy.typing import NDArray from numpy.typing import NDArray
from typing import Any
def _validate_inputs(weights: NDArray[Any], cov: NDArray[Any]) -> tuple[NDArray[Any], NDArray[Any]]:
"""Normalize a portfolio vector and its covariance matrix."""
w = np.asarray(weights, dtype=float).ravel()
covariance = np.asarray(cov, dtype=float)
k = w.size
if k == 0:
raise ValueError("weights must contain at least one asset")
if covariance.shape != (k, k):
raise ValueError(f"cov shape {covariance.shape} does not match weights length {k}")
return w, covariance
def risk_contribution(weights: NDArray[Any], cov: NDArray[Any]) -> NDArray[Any]: def risk_contribution(weights: NDArray[Any], cov: NDArray[Any]) -> NDArray[Any]:
@@ -38,8 +25,11 @@ def risk_contribution(weights: NDArray[Any], cov: NDArray[Any]) -> NDArray[Any]:
Returns: Returns:
RC: 风险贡献向量 (k,), Σ=1 RC: 风险贡献向量 (k,), Σ=1
""" """
w, cov = _validate_inputs(weights, cov) w = np.asarray(weights, dtype=float).ravel()
cov = np.asarray(cov, dtype=float)
k = w.size k = w.size
if cov.shape != (k, k):
raise ValueError(f"cov 形状 {cov.shape} 与 weights 长度 {k} 不匹配")
port_var = float(w @ cov @ w) port_var = float(w @ cov @ w)
if port_var <= 0: if port_var <= 0:
@@ -51,11 +41,13 @@ def risk_contribution(weights: NDArray[Any], cov: NDArray[Any]) -> NDArray[Any]:
def marginal_risk_contribution(weights: NDArray[Any], cov: NDArray[Any]) -> NDArray[Any]: def marginal_risk_contribution(weights: NDArray[Any], cov: NDArray[Any]) -> NDArray[Any]:
"""边际风险贡献 (MRC_i): (Σw)_i。""" """边际风险贡献 (MRC_i): (Σw)_i。"""
w, cov = _validate_inputs(weights, cov) w = np.asarray(weights, dtype=float).ravel()
cov = np.asarray(cov, dtype=float)
return cov @ w # type: ignore[no-any-return] return cov @ w # type: ignore[no-any-return]
def component_var(weights: NDArray[Any], cov: NDArray[Any]) -> NDArray[Any]: def component_var(weights: NDArray[Any], cov: NDArray[Any]) -> NDArray[Any]:
"""成分方差: w_i · (Σw)_i; 与 RC 的关系 RC_i = CV_i / w'Σw。""" """成分方差: w_i · (Σw)_i; 与 RC 的关系 RC_i = CV_i / w'Σw。"""
w, cov = _validate_inputs(weights, cov) w = np.asarray(weights, dtype=float).ravel()
cov = np.asarray(cov, dtype=float)
return w * (cov @ w) # type: ignore[no-any-return] return w * (cov @ w) # type: ignore[no-any-return]
+11 -3
View File
@@ -9,14 +9,22 @@ ROOT = Path(__file__).resolve().parents[2]
class CiContractTests(unittest.TestCase): class CiContractTests(unittest.TestCase):
def test_ci_is_one_dependency_free_lite_gate(self) -> None: def test_ci_is_one_locked_shared_runtime_lite_gate(self) -> None:
workflow = (ROOT / ".gitea/workflows/ci.yml").read_text(encoding="utf-8") workflow = (ROOT / ".gitea/workflows/ci.yml").read_text(encoding="utf-8")
jobs = workflow.split("jobs:", 1)[1] jobs = workflow.split("jobs:", 1)[1]
self.assertEqual(re.findall(r"(?m)^ ([a-z][a-z0-9_-]*):\s*$", jobs), ["lite"]) self.assertEqual(re.findall(r"(?m)^ ([a-z][a-z0-9_-]*):\s*$", jobs), ["lite"])
self.assertIn("actions/checkout@524e936cd9e579adf00e308bfdf971aebc7de09e", workflow) self.assertIn("actions/checkout@524e936cd9e579adf00e308bfdf971aebc7de09e", workflow)
self.assertIn("persist-credentials: false", workflow) self.assertIn("persist-credentials: false", workflow)
self.assertIn("python3 tests/governance/test_module_spec.py", workflow) self.assertIn("UV_PYTHON_DOWNLOADS: never", workflow)
for forbidden in ("setup-python", "pip ", "curl ", "wget ", "docker pull"): self.assertIn('test "$(python3 --version)" = "Python 3.13.15"', workflow)
self.assertIn(
'test "$(uv --version | cut -d\' \' -f1-2)" = "uv 0.12.3"',
workflow,
)
self.assertIn("uv sync --locked --extra dev", workflow)
self.assertIn("uv run --locked --no-sync python", workflow)
self.assertIn("tests/governance/test_ci_contract.py", workflow)
for forbidden in ("setup-python", "setup-uv", "pip ", "curl ", "wget ", "docker pull"):
self.assertNotIn(forbidden, workflow) self.assertNotIn(forbidden, workflow)
-209
View File
@@ -1,209 +0,0 @@
"""Backtest contract tests for weights, NAV, rebalancing, and benchmarks."""
from __future__ import annotations
import pandas as pd
import pytest
from quant_engine.backtest import (
BacktestResult,
compare_to_benchmark,
compute_nav_from_weights,
compute_returns_from_nav,
rebalance_periodic,
run_weight_backtest,
weights_to_long_short,
)
def test_compute_nav_from_weights_forward_fills_rebalance_weights() -> None:
dates = pd.date_range("2026-01-05", periods=3, freq="B")
weights = pd.DataFrame({"A": [0.5], "B": [0.5]}, index=dates[:1])
returns = pd.DataFrame({"A": [0.10, 0.00, -0.10], "B": [0.00, 0.10, 0.00]}, index=dates)
nav = compute_nav_from_weights(weights, returns, initial_capital=100.0)
expected = pd.Series([105.0, 110.25, 104.7375], index=dates)
pd.testing.assert_series_equal(nav, expected)
def test_compute_nav_stays_in_cash_before_first_rebalance() -> None:
dates = pd.date_range("2026-01-05", periods=3, freq="B")
weights = pd.DataFrame({"A": [1.0]}, index=dates[1:2])
returns = pd.DataFrame({"A": [0.50, 0.10, 0.10]}, index=dates)
nav = compute_nav_from_weights(weights, returns)
pd.testing.assert_series_equal(nav, pd.Series([1.0, 1.1, 1.21], index=dates))
def test_compute_nav_ignores_weight_columns_without_returns() -> None:
dates = pd.date_range("2026-01-05", periods=2, freq="B")
weights = pd.DataFrame({"A": [0.5], "MISSING": [0.5]}, index=dates[:1])
returns = pd.DataFrame({"A": [0.10, 0.10]}, index=dates)
nav = compute_nav_from_weights(weights, returns)
pd.testing.assert_series_equal(nav, pd.Series([1.05, 1.1025], index=dates))
def test_compute_nav_charges_configured_turnover_cost() -> None:
dates = pd.date_range("2026-01-05", periods=2, freq="B")
weights = pd.DataFrame({"A": [1.0]}, index=dates[:1])
returns = pd.DataFrame({"A": [0.0, 0.0]}, index=dates)
nav = compute_nav_from_weights(weights, returns, tc_rate=0.01)
pd.testing.assert_series_equal(nav, pd.Series([0.995, 0.995], index=dates))
def test_compute_returns_from_nav_preserves_index_and_sets_initial_zero() -> None:
nav = pd.Series([100.0, 110.0, 99.0], index=pd.date_range("2026-01-05", periods=3))
result = compute_returns_from_nav(nav)
pd.testing.assert_series_equal(result, pd.Series([0.0, 0.1, -0.1], index=nav.index))
def test_rebalance_periodic_maps_weekend_to_previous_trading_day() -> None:
dates = pd.date_range("2026-01-05", periods=5, freq="B")
target = pd.Series({"A": 0.6, "B": 0.4})
result = rebalance_periodic(target, [pd.Timestamp("2026-01-10")], dates)
assert result.loc[pd.Timestamp("2026-01-08")].sum() == 0.0
pd.testing.assert_series_equal(
result.loc[pd.Timestamp("2026-01-09")], target, check_names=False
)
def test_rebalance_periodic_accepts_empty_trading_calendar() -> None:
target = pd.Series({"A": 1.0})
result = rebalance_periodic(
target,
[pd.Timestamp("2026-01-05")],
pd.DatetimeIndex([]),
)
assert result.empty
assert result.columns.tolist() == ["A"]
def test_weights_to_long_short_allocates_each_leg() -> None:
result = weights_to_long_short(["A", "B"], ["C"], long_weight=0.6, short_weight=0.4)
assert result["A"] == pytest.approx(0.3)
assert result["B"] == pytest.approx(0.3)
assert result["C"] == pytest.approx(-0.4)
assert result.sum() == pytest.approx(0.2)
def test_weights_to_long_short_keeps_explicit_universe() -> None:
result = weights_to_long_short(["A"], [], all_tickers=["A", "B"])
pd.testing.assert_series_equal(result, pd.Series({"A": 0.5, "B": 0.0}))
def test_compare_to_benchmark_returns_report_table() -> None:
dates = pd.date_range("2026-01-05", periods=4, freq="B")
strategy = pd.Series([1.0, 1.1, 1.0, 1.2], index=dates)
benchmark = pd.Series([1.0, 1.0, 1.05, 1.1], index=dates)
result = compare_to_benchmark(strategy, benchmark)
assert result.columns.tolist() == ["策略", "基准"]
assert result.loc["n_days", "策略"] == 4
assert result.loc["累计收益", "策略"] == pytest.approx(0.2)
assert result.loc["累计收益", "基准"] == pytest.approx(0.1)
def test_compare_to_benchmark_rejects_non_overlapping_dates() -> None:
strategy = pd.Series([1.0], index=[pd.Timestamp("2026-01-05")])
benchmark = pd.Series([1.0], index=[pd.Timestamp("2026-02-05")])
with pytest.raises(ValueError, match="overlapping dates"):
compare_to_benchmark(strategy, benchmark)
# ── 统一回测结果门面 ──────────────────────────────────────
def test_run_weight_backtest_returns_nav_returns_and_input_snapshot() -> None:
dates = pd.date_range("2026-01-05", periods=3, freq="B")
weights = pd.DataFrame({"A": [1.0]}, index=dates[:1])
stock_returns = pd.DataFrame({"A": [0.10, -0.10, 0.20]}, index=dates)
result = run_weight_backtest(weights, stock_returns, initial_capital=100.0)
assert isinstance(result, BacktestResult)
pd.testing.assert_series_equal(
result.nav,
pd.Series([110.0, 99.0, 118.8], index=dates),
)
pd.testing.assert_series_equal(
result.returns,
pd.Series([0.0, -0.1, 0.2], index=dates),
)
pd.testing.assert_frame_equal(result.weights, weights)
def test_backtest_result_stats_reuses_standard_metrics_contract() -> None:
dates = pd.date_range("2026-01-05", periods=3, freq="B")
result = run_weight_backtest(
pd.DataFrame({"A": [1.0]}, index=dates[:1]),
pd.DataFrame({"A": [0.10, -0.10, 0.20]}, index=dates),
)
stats = result.stats(rf=0.02)
assert stats["n_days"] == 3
assert stats["ann_return"] == pytest.approx(
(1.0 * 0.9 * 1.2) ** (252 / 3) - 1.0
)
assert "sharpe" in stats
assert stats["drawback"] == stats["max_drawdown"]
def test_backtest_result_builds_benchmark_report() -> None:
dates = pd.date_range("2026-01-05", periods=3, freq="B")
benchmark = pd.Series([1.0, 1.05, 1.10], index=dates, name="benchmark")
result = run_weight_backtest(
pd.DataFrame({"A": [1.0]}, index=dates[:1]),
pd.DataFrame({"A": [0.10, -0.10, 0.20]}, index=dates),
benchmark_nav=benchmark,
)
report = result.benchmark_report()
assert report.columns.tolist() == ["策略", "基准"]
assert report.loc["累计收益", "基准"] == pytest.approx(0.10)
def test_backtest_result_requires_benchmark_for_comparison() -> None:
dates = pd.date_range("2026-01-05", periods=2, freq="B")
result = run_weight_backtest(
pd.DataFrame({"A": [1.0]}, index=dates[:1]),
pd.DataFrame({"A": [0.0, 0.0]}, index=dates),
)
with pytest.raises(ValueError, match="benchmark_nav"):
result.benchmark_report()
def test_backtest_result_isolated_from_mutated_caller_inputs() -> None:
dates = pd.date_range("2026-01-05", periods=2, freq="B")
weights = pd.DataFrame({"A": [1.0]}, index=dates[:1])
benchmark = pd.Series([1.0, 1.1], index=dates)
result = run_weight_backtest(
weights,
pd.DataFrame({"A": [0.0, 0.0]}, index=dates),
benchmark_nav=benchmark,
)
weights.iloc[0, 0] = 0.0
benchmark.iloc[1] = 99.0
assert result.weights.iloc[0, 0] == 1.0
assert result.benchmark_nav is not None
assert result.benchmark_nav.iloc[1] == 1.1
-173
View File
@@ -1,173 +0,0 @@
"""Contracts for reusable factor diagnostics and transformations."""
from __future__ import annotations
import numpy as np
import pandas as pd
import pytest
from quant_engine.factor_library import (
annualized_sharpe,
apply_factor_direction,
cross_sectional_momentum,
cross_sectional_pct_rank,
cross_sectional_rank_with_direction,
ic_summary,
jb_test,
kurtosis,
ols_regress,
rolling_annual_vol,
rolling_zscore,
skewness,
spearman_ic,
time_series_momentum,
turnover,
winsorize,
)
def test_turnover_supports_one_way_and_round_trip_conventions() -> None:
weights = pd.DataFrame({"A": [1.0, 0.0], "B": [0.0, 1.0]})
pd.testing.assert_series_equal(turnover(weights), pd.Series([1.0], index=[1]))
pd.testing.assert_series_equal(
turnover(weights, divide_by_two=False), pd.Series([2.0], index=[1])
)
assert turnover(weights.iloc[:1]).empty
def test_ic_functions_measure_monotonic_relationship() -> None:
factor = pd.Series([1.0, 2.0, 3.0, 4.0])
forward = pd.Series([10.0, 20.0, 30.0, 40.0])
assert spearman_ic(factor, forward) == pytest.approx(1.0)
result = ic_summary(factor, forward, periods=(1,), method="pearson")
assert result.loc[1, "ic_mean"] == pytest.approx(1.0)
assert result.loc[1, "n"] == 4
def test_ic_summary_rejects_unknown_method() -> None:
with pytest.raises(ValueError, match="not supported"):
ic_summary(pd.Series([1, 2, 3]), pd.Series([1, 2, 3]), method="kendall")
def test_winsorize_clips_tails_and_preserves_nan() -> None:
values = pd.Series([0.0, 1.0, 2.0, 100.0, np.nan])
result = winsorize(values, lower=0.25, upper=0.75)
assert result.iloc[0] == pytest.approx(0.75)
assert result.iloc[3] == pytest.approx(26.5)
assert pd.isna(result.iloc[4])
def test_distribution_diagnostics_handle_short_samples() -> None:
assert np.isnan(skewness(pd.Series([1.0, 2.0])))
assert np.isnan(kurtosis(pd.Series([1.0, 2.0, 3.0])))
jb, p_value = jb_test(pd.Series(range(7), dtype=float))
assert np.isnan(jb)
assert np.isnan(p_value)
def test_distribution_diagnostics_return_finite_values() -> None:
values = pd.Series([-2.0, -1.0, -0.5, 0.0, 0.25, 0.75, 1.0, 3.0])
assert np.isfinite(skewness(values))
assert np.isfinite(kurtosis(values))
jb, p_value = jb_test(values)
assert jb >= 0
assert 0 <= p_value <= 1
def test_ols_recovers_linear_coefficients_and_residual_index() -> None:
index = pd.date_range("2026-01-01", periods=8)
factor = pd.Series(np.arange(8, dtype=float), index=index, name="factor")
target = 1.5 + 2.0 * factor
result = ols_regress(target, factor)
assert result.alpha == pytest.approx(1.5)
assert result.beta["factor"] == pytest.approx(2.0)
assert result.r_squared == pytest.approx(1.0)
assert result.n == 8
assert result.resid.index.equals(index)
def test_ols_handles_collinear_factors_without_crashing() -> None:
x = pd.DataFrame({"a": np.arange(8, dtype=float), "b": np.arange(8, dtype=float)})
y = pd.Series(1.0 + x["a"])
result = ols_regress(y, x)
assert result.n == 8
assert np.isfinite(result.beta).all()
np.testing.assert_allclose(result.resid, 0.0, atol=1e-12)
def test_ols_short_sample_returns_empty_estimate() -> None:
result = ols_regress(pd.Series([1.0, 2.0]), pd.Series([1.0, 2.0], name="x"))
assert np.isnan(result.alpha)
assert result.beta.empty
assert result.n == 2
def test_momentum_and_rolling_transforms_match_manual_values() -> None:
prices = pd.DataFrame({"A": [100.0, 110.0, 121.0, 133.1]})
momentum = cross_sectional_momentum(prices, lookback=2, skip=0)
assert momentum.iloc[2, 0] == pytest.approx(0.21)
returns = pd.Series([0.1, 0.1, -0.5, -0.5])
pd.testing.assert_series_equal(
time_series_momentum(returns, lookback=2),
pd.Series([0, 1, -1, -1]),
)
values = pd.Series([1.0, 2.0, 3.0])
zscore = rolling_zscore(values, window=3)
assert zscore.iloc[-1] == pytest.approx(1.0)
annual_vol = rolling_annual_vol(returns, window=2, min_periods=2, trading_days=4)
assert annual_vol.iloc[1] == pytest.approx(0.0)
def test_rank_helpers_support_global_and_grouped_ranking() -> None:
frame = pd.DataFrame(
{"factor": [3.0, 1.0, 2.0, 4.0], "industry": ["x", "x", "y", "y"]}
)
global_rank = cross_sectional_pct_rank(frame, "factor", ascending=True)
grouped_rank = cross_sectional_pct_rank(
frame, "factor", group_col="industry", ascending=True
)
assert global_rank.tolist() == [0.75, 0.25, 0.5, 1.0]
assert grouped_rank.tolist() == [1.0, 0.5, 0.5, 1.0]
assert cross_sectional_pct_rank(frame, "missing").empty
def test_factor_direction_and_directional_rank() -> None:
pe = pd.Series([10.0, 20.0], name="pe_ttm")
pd.testing.assert_series_equal(apply_factor_direction(pe), -pe)
frame = pd.DataFrame({"pe_ttm": [10.0, 20.0], "roe": [0.1, 0.2]})
assert cross_sectional_rank_with_direction(frame, "pe_ttm").tolist() == [1.0, 0.5]
assert cross_sectional_rank_with_direction(frame, "roe").tolist() == [0.5, 1.0]
@pytest.mark.parametrize("direction", ["sideways", "", "REVERSE"])
def test_factor_direction_rejects_unknown_values(direction: str) -> None:
factor = pd.Series([1.0, 2.0], name="roe")
with pytest.raises(ValueError, match="direction"):
apply_factor_direction(factor, direction=direction)
with pytest.raises(ValueError, match="direction"):
cross_sectional_rank_with_direction(
pd.DataFrame({"roe": factor}), "roe", direction=direction
)
def test_annualized_sharpe_handles_empty_and_nonzero_returns() -> None:
assert annualized_sharpe(pd.Series(dtype=float)) == 0.0
returns = pd.Series([0.01, -0.01, 0.02, 0.0])
expected = returns.mean() * 252 / (returns.std() * np.sqrt(252))
assert annualized_sharpe(returns) == pytest.approx(expected)
-93
View File
@@ -1,93 +0,0 @@
"""Mathematical contracts for the standard performance metrics."""
from __future__ import annotations
import numpy as np
import pandas as pd
import pytest
from quant_engine.metrics import (
TRADING_DAYS_PER_YEAR,
annualized_return,
annualized_volatility,
calmar_ratio,
max_drawdown,
sharpe_ratio,
summary,
win_rate,
)
def test_annualized_return_uses_compounded_simple_returns() -> None:
returns = pd.Series([0.10, -0.10])
expected = 0.99 ** (TRADING_DAYS_PER_YEAR / 2) - 1.0
assert annualized_return(returns) == pytest.approx(expected)
def test_annualized_volatility_uses_sample_standard_deviation() -> None:
returns = pd.Series([0.01, 0.03, 0.02])
assert annualized_volatility(returns) == pytest.approx(
returns.std() * np.sqrt(TRADING_DAYS_PER_YEAR)
)
def test_sharpe_ratio_subtracts_annual_risk_free_rate() -> None:
returns = pd.Series([0.01, -0.005, 0.02, 0.0])
result = sharpe_ratio(returns, rf=0.02)
assert result == pytest.approx(
(annualized_return(returns) - 0.02) / annualized_volatility(returns)
)
def test_zero_volatility_metrics_return_zero() -> None:
returns = pd.Series([0.0, 0.0, 0.0])
assert sharpe_ratio(returns) == 0.0
assert calmar_ratio(returns) == 0.0
def test_max_drawdown_includes_loss_from_initial_capital() -> None:
returns = pd.Series([-0.20, 0.0])
assert max_drawdown(returns) == pytest.approx(-0.20)
def test_max_drawdown_tracks_peak_to_trough_loss() -> None:
returns = pd.Series([0.10, -0.20, 0.05])
assert max_drawdown(returns) == pytest.approx(-0.20)
def test_metrics_clean_nan_and_infinite_values() -> None:
returns = pd.Series([0.10, np.nan, np.inf, -0.05, -np.inf])
assert win_rate(returns) == 0.5
assert summary(returns)["n_days"] == 2
def test_summary_aliases_match_canonical_fields() -> None:
result = summary(pd.Series([0.01, -0.02, 0.03]))
assert result["annual_yield"] == result["ann_return"]
assert result["annual_sd"] == result["ann_volatility"]
assert result["drawback"] == result["max_drawdown"]
@pytest.mark.parametrize(
"metric",
[annualized_return, annualized_volatility, sharpe_ratio, max_drawdown, calmar_ratio, win_rate],
)
def test_metrics_reject_non_series_input(metric) -> None:
with pytest.raises(TypeError, match=r"expected pd\.Series"):
metric([0.01, 0.02])
def test_short_and_empty_series_return_zero() -> None:
assert annualized_return(pd.Series(dtype=float)) == 0.0
assert annualized_volatility(pd.Series([0.01])) == 0.0
assert max_drawdown(pd.Series([0.01])) == 0.0
assert win_rate(pd.Series(dtype=float)) == 0.0
-158
View File
@@ -1,158 +0,0 @@
"""Factor-score portfolio construction and backtest integration contracts."""
from __future__ import annotations
import numpy as np
import pandas as pd
import pytest
from quant_engine.backtest import run_weight_backtest
from quant_engine.portfolio_construction import (
equal_weight,
scores_to_target_weights,
scores_to_weight_table,
select_top_k,
)
def test_select_top_k_ignores_nan_and_breaks_ties_by_input_order() -> None:
scores = pd.Series([1.0, 1.0, np.nan, 0.5], index=["B", "A", "C", "D"])
selected = select_top_k(scores, top_k=2)
assert selected.tolist() == ["B", "A"]
def test_select_top_k_can_select_lowest_scores() -> None:
scores = pd.Series([3.0, 1.0, 2.0], index=["A", "B", "C"])
selected = select_top_k(scores, top_k=2, largest=False)
assert selected.tolist() == ["B", "C"]
def test_equal_weight_allocates_requested_gross_exposure() -> None:
result = equal_weight(pd.Index(["A", "B", "C"]), gross_exposure=0.9)
pd.testing.assert_series_equal(
result,
pd.Series([0.3, 0.3, 0.3], index=["A", "B", "C"], name="weight"),
)
def test_equal_weight_returns_empty_float_series_for_no_assets() -> None:
result = equal_weight(pd.Index([], dtype=object))
assert result.empty
assert result.dtype == float
assert result.name == "weight"
def test_scores_to_target_weights_keeps_full_universe_with_zero_for_unselected() -> None:
scores = pd.Series([0.2, 0.8, 0.5], index=["A", "B", "C"])
result = scores_to_target_weights(scores, top_k=2)
pd.testing.assert_series_equal(
result,
pd.Series([0.0, 0.5, 0.5], index=scores.index, name="weight"),
)
def test_scores_to_target_weights_divides_exposure_over_available_scores() -> None:
scores = pd.Series([1.0, np.nan, 0.5], index=["A", "B", "C"])
result = scores_to_target_weights(scores, top_k=5, gross_exposure=0.8)
pd.testing.assert_series_equal(
result,
pd.Series([0.4, 0.0, 0.4], index=scores.index, name="weight"),
)
def test_scores_to_weight_table_constructs_each_rebalance_independently() -> None:
dates = pd.to_datetime(["2026-01-05", "2026-01-07"])
scores = pd.DataFrame(
{"A": [3.0, 1.0], "B": [2.0, 3.0], "C": [1.0, 2.0]},
index=dates,
)
result = scores_to_weight_table(scores, top_k=2)
expected = pd.DataFrame(
{"A": [0.5, 0.0], "B": [0.5, 0.5], "C": [0.0, 0.5]},
index=dates,
)
pd.testing.assert_frame_equal(result, expected)
changed_future = scores.copy()
changed_future.iloc[1] = [100.0, -100.0, 0.0]
changed_result = scores_to_weight_table(changed_future, top_k=2)
pd.testing.assert_series_equal(result.iloc[0], changed_result.iloc[0])
def test_factor_scores_flow_directly_into_weight_backtest() -> None:
dates = pd.date_range("2026-01-05", periods=3, freq="B")
scores = pd.DataFrame(
{"A": [2.0, 0.0], "B": [1.0, 3.0]},
index=dates[[0, 2]],
)
stock_returns = pd.DataFrame(
{"A": [0.10, 0.0, 0.0], "B": [0.0, 0.0, 0.20]},
index=dates,
)
weights = scores_to_weight_table(scores, top_k=1)
result = run_weight_backtest(weights, stock_returns)
pd.testing.assert_series_equal(result.nav, pd.Series([1.1, 1.1, 1.32], index=dates))
pd.testing.assert_frame_equal(result.weights, weights)
@pytest.mark.parametrize("top_k", [0, -1])
def test_portfolio_construction_rejects_non_positive_top_k(top_k: int) -> None:
scores = pd.Series([1.0], index=["A"])
with pytest.raises(ValueError, match="top_k must be positive"):
select_top_k(scores, top_k=top_k)
@pytest.mark.parametrize("gross_exposure", [-0.1, np.inf, np.nan])
def test_equal_weight_rejects_invalid_gross_exposure(gross_exposure: float) -> None:
with pytest.raises(ValueError, match="gross_exposure"):
equal_weight(pd.Index(["A"]), gross_exposure=gross_exposure)
def test_portfolio_construction_rejects_duplicate_assets() -> None:
duplicate_scores = pd.Series([1.0, 2.0], index=["A", "A"])
with pytest.raises(ValueError, match="unique asset labels"):
scores_to_target_weights(duplicate_scores, top_k=1)
def test_weight_table_rejects_duplicate_rebalance_dates() -> None:
duplicate_date = pd.Timestamp("2026-01-05")
scores = pd.DataFrame(
{"A": [1.0, 2.0]},
index=[duplicate_date, duplicate_date],
)
with pytest.raises(ValueError, match="unique rebalance dates"):
scores_to_weight_table(scores, top_k=1)
def test_weight_table_rejects_unsorted_rebalance_dates() -> None:
scores = pd.DataFrame(
{"A": [1.0, 2.0]},
index=pd.to_datetime(["2026-01-07", "2026-01-05"]),
)
with pytest.raises(ValueError, match="chronological order"):
scores_to_weight_table(scores, top_k=1)
def test_weight_table_rejects_non_numeric_scores() -> None:
scores = pd.DataFrame({"A": ["high"], "B": ["low"]})
with pytest.raises(TypeError, match="numeric"):
scores_to_weight_table(scores, top_k=1)
-54
View File
@@ -1,54 +0,0 @@
"""Risk contribution contracts and validation tests."""
from __future__ import annotations
import numpy as np
import pytest
from quant_engine.risk import component_var, marginal_risk_contribution, risk_contribution
def test_risk_contribution_sums_to_one_for_positive_portfolio_variance() -> None:
weights = np.array([0.5, 0.5])
covariance = np.diag([1.0, 4.0])
result = risk_contribution(weights, covariance)
np.testing.assert_allclose(result, [0.2, 0.8])
assert result.sum() == pytest.approx(1.0)
def test_zero_variance_portfolio_falls_back_to_equal_contribution() -> None:
result = risk_contribution(np.array([0.2, 0.3, 0.5]), np.zeros((3, 3)))
np.testing.assert_allclose(result, np.full(3, 1 / 3))
def test_marginal_and_component_risk_follow_matrix_identities() -> None:
weights = np.array([0.25, 0.75])
covariance = np.array([[0.04, 0.01], [0.01, 0.09]])
marginal = marginal_risk_contribution(weights, covariance)
component = component_var(weights, covariance)
np.testing.assert_allclose(marginal, covariance @ weights)
np.testing.assert_allclose(component, weights * marginal)
assert component.sum() == pytest.approx(weights @ covariance @ weights)
@pytest.mark.parametrize(
"function",
[risk_contribution, marginal_risk_contribution, component_var],
)
def test_risk_functions_reject_covariance_shape_mismatch(function) -> None:
with pytest.raises(ValueError, match="does not match weights length"):
function(np.array([0.5, 0.5]), np.eye(3))
@pytest.mark.parametrize(
"function",
[risk_contribution, marginal_risk_contribution, component_var],
)
def test_risk_functions_reject_empty_portfolio(function) -> None:
with pytest.raises(ValueError, match="at least one asset"):
function(np.array([]), np.empty((0, 0)))
Generated
+408
View File
@@ -0,0 +1,408 @@
version = 1
revision = 3
requires-python = "==3.13.*"
resolution-markers = [
"sys_platform == 'win32'",
"sys_platform == 'emscripten'",
"sys_platform != 'emscripten' and sys_platform != 'win32'",
]
[[package]]
name = "ast-serialize"
version = "0.8.0"
source = { registry = "https://mirrors.cloud.tencent.com/pypi/simple" }
sdist = { url = "https://mirrors.cloud.tencent.com/pypi/packages/e1/a9/11851c3e02a3fea2ddc9932d1fdc7d2edaeecc0d2e11bc5f2a7fde2b0934/ast_serialize-0.8.0.tar.gz", hash = "sha256:6c37c43e4004dfb42d321ddedc569dc17ff4259296f3af577c9ea46a809bc010" }
wheels = [
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/4c/11/911210c3c78923273a9211a2b6cfc4c8aa723b30dab3e1c8d19afb983b40/ast_serialize-0.8.0-cp315-abi3.abi3t-macosx_10_12_x86_64.whl", hash = "sha256:86b8a1e6d90467345356098b040150e82fbc26d24a7a202224b13dc1f6264ca0" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/77/89/6282881c8587606638db153cbe21e1e0c4d1f3970dee1aa0610a1c62a026/ast_serialize-0.8.0-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:39e92ff8e8cb45947fe9007174b2950e1fb098e6abd00266a13cd3bcf6675068" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/97/78/a9f846a03a340ff3728c915f23338ca742742f3292700559cdb3ad999b1e/ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c85d8d18db5b2dfcb3b7e38a4d600ca35504c0ed8a6f75cd1c811e4ffe248a15" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/c0/15/aba6ef8a988a6eceb6f0359589aac509e29ae2dba67fd9bfd5af0c3f13e7/ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9830ff7e764f74d9eefb01170c61a9f0fd2c027dac5fcb72e064decd57d56371" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/94/29/3f63d696ea7c5b8abadcecc3505be51bd900daaccc522ed8322fa5b05a93/ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6479d9722a4cd21b578f5478074c41e6169f04811996ec881655560f703a5bba" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/e2/5d/0aac338604ff59df5774d4304307898982252f325ff7cafe31d52fedcb65/ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a63bed264e818cd83eec11feed0f50aa162542b91132ef58afebc857182763a5" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/23/ca/9f1ef795bb724719532bd86dbec11e5b66857d3fbe9b6772baec0191a6ed/ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d187197d234aa45d6cfa2b096be5f666e8cc2e7eb3722d0ab8926293cf5720c" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/dc/25/5e061372d2ed953b9ba3b9c4f73de3b8e9234cda3f6c088db4686801d0e1/ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_31_riscv64.whl", hash = "sha256:2d39a56282cfcc0d8eeea37267c754be59c98d48505c23b1dae5c6011f3813dd" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/a8/c1/ae7da218053120635a4ca802366c69f707203641af95372eeb83f70dfd52/ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f7cc5f10386994c0f4844f1e6d6a97127e9b478660eb6dec2b257644f0acab64" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/2e/89/271d1f49c5269fcddcc789ea3f25be401f6723fc1138aeda539f4d05516d/ast_serialize-0.8.0-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:6102f2f985c2e542be85cd857678ec9356fefa792b93cadfadd31139f5696f27" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/55/be/4e7d77fcf571ac7cb5cf7115a20c36642bd7d29473b45dfaaefeb9618f90/ast_serialize-0.8.0-cp315-abi3.abi3t-musllinux_1_2_armv7l.whl", hash = "sha256:3a8660fe66667b76a6e9dccd1d33e66b229fde3b308db991c041609226c005b6" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/8b/ae/ed1de2db7e019d4236fbc164ffa5ef9a6022a300a342bbf142d21b7c141e/ast_serialize-0.8.0-cp315-abi3.abi3t-musllinux_1_2_i686.whl", hash = "sha256:e7266307e5fba39836edb79def8608887af48820508bff3c5f2941e1e04d1534" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/92/89/5fea507fae5c5f18b7dc7f95e5c00956574b8c717b8fd2049c504fab0b18/ast_serialize-0.8.0-cp315-abi3.abi3t-musllinux_1_2_ppc64le.whl", hash = "sha256:4ca7e6fd1ad845d1cc649dc2ecd499db2f8f46af5bf8da7b70dd858774cc038b" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/42/71/478d69df21b64e064554a68134c94be304270316ca676a94e63c389a636a/ast_serialize-0.8.0-cp315-abi3.abi3t-musllinux_1_2_riscv64.whl", hash = "sha256:2880350b13d3eae69a0d70bc1fb6c9bfaca4dbd0e20ba8cd1aa483080b56ff06" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/5e/2d/8962dc8d5b3a9dc27b36f9db199afa25264c741505469d9ec10ffbfd2ba7/ast_serialize-0.8.0-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:ab0f9a59f7d63d0d441b56b9a818b273705264352d5115cfee12e940e816d958" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/4f/22/14d2ad4fd1d1bcd0dc687ca268e0630069f45162496260c0efb70ee0ea72/ast_serialize-0.8.0-cp315-abi3.abi3t-win32.whl", hash = "sha256:0485a25ef519c62e749ee3c1ad8070e591b380d67226349eb5a70b228dc1ac4a" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/18/1d/84a327c0202a41aa5fdba3ade33904d6d8f3b9e6806fa83568d835395850/ast_serialize-0.8.0-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:bd84d60bca7079e741be4ac5dbe237751a59d7f6f9f0126b11880d63822cbe16" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/8c/92/74556dec52fde85a2ad84ed159991b916241043788609c15d8b77e14570b/ast_serialize-0.8.0-cp315-abi3.abi3t-win_arm64.whl", hash = "sha256:057769b5921336eb2d9124f2a731b42ed05ffdac559b840dbdf6f3937cf153dc" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/d9/e3/6142e920fec6ef7bccabd8c24ed8ed99f8bdc6cb8b065e1df7c6a3b2d667/ast_serialize-0.8.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:e1bd223df0f6c96b396975fa604cb33bce53d9b4a0185490be4c4a289f7c9c87" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/a6/e9/6e8be8df02b35d85e2b8809f7f1cfa290bdf5882b55127a539d049482db0/ast_serialize-0.8.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ddd3b61f45c132da66c5476b281891e08c1fd87fbdabe8a6973e1622efc85f06" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/8c/80/7e0fd2e2e2aba257820db4a8657c4c356844d36b914b20a4af294bcfb902/ast_serialize-0.8.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f9caa63fad8241257ae401b5ff0a64026c6adb36b8e86cbe8782d9ea505daf6" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/b0/6a/3bae0af06f9b1bae3001c44d64215f5b567877e7aae9ffd45db11c3a7647/ast_serialize-0.8.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3926fa117b5e65019853a2969966d11c7175af377a3425991f3fe73784412405" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/6f/c4/ce2d41a1bc22508e82618901f7e10f2a5e2f9556553fea90624daf9875e2/ast_serialize-0.8.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:485f1113af805e9e170b95ef993ca3fbd4f89c04bab25c58b4fc632d854801ab" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/1a/90/f5058f209756dd70e958b7538aaa82d25d24944baf9ec8ae6f27b06fcacc/ast_serialize-0.8.0-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ccebbed24f1281062d5852353c72c47502955926cfcb8345ffb3a44d87ff3d3" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/bf/32/7f77ea87fa0836daab706ed5cb7f903bb25fa26a77439011aee626af11d8/ast_serialize-0.8.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:252f883290d1cdb728eb7fe1d9a7221b88af5a329aae0bc91ddee4dafb820331" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/eb/5a/75b82ad2725b5e8e8c742732f9e76c6738a292d0709e1f60d10a973730b4/ast_serialize-0.8.0-cp39-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:96abc072ad29db8d02194afd47d68987322622787daceae82398d7b69f3ba2e6" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/4e/54/8c20ed4eea805516a3fd23dd4a721ce28c64f50f0e4b359969f60a8c97a6/ast_serialize-0.8.0-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9118ad3e369727060b2696fc4078f250ecffca4248ba87f537f55cea9f9dce06" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/cb/5b/9f14430f12fe830b656fb38f8e2e05ee13b02a88967660bef46af0ab22a8/ast_serialize-0.8.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f359df4bd921918af8bebd142a376c77511d7151cc8ba852760b587b5a4a54f3" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/2d/3d/084882eca93c842bd4262591a071ec7f825340644035e51501208cc5a8d4/ast_serialize-0.8.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:e94f9121d13fa36cbf21314783c77d05ae3a0868decd18cf5233fdcc6de49ac8" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/ce/73/ea84852096c2036c61cc0b2f97b90242207419f534dc671060ee1c8e05cb/ast_serialize-0.8.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:54f95b486018d262bcb387a9afd96f0da74508b442762b80c769454a6fbb3ee3" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/cb/88/287b9a5300c1f2f651d259f670931b63110adc265b7613c885b44c5bc53d/ast_serialize-0.8.0-cp39-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:4c38b915511e32bc718c49dbce98ff9af36bac0ad6a604f58000cd5e3aecdba7" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/ee/f3/1bc3a79afcf0c2a8d2c37182d0d659d1545a9d7f7f6dc9cf3e63d6c17135/ast_serialize-0.8.0-cp39-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:9a2ef9cf12f2de4f1028c42c1dd7d775255e0fb3e5bb48896c97e35ef52366fe" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/5c/cd/440c798957e14e31776bfeb024d8fafe0bb1d5b89c51c2f067e69938f7b0/ast_serialize-0.8.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6f18048fe9f6dd266bd577cdec48bdcecb74faaa01fe941324435483b013ed2a" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/4f/4a/587eb36dcc240a54c8660f599464516b469ecad96f0dbdb6bccbedb50745/ast_serialize-0.8.0-cp39-abi3-win32.whl", hash = "sha256:31883542dd6c94d178f5db3d32fbd69c5eb88b3a7c018e7ac8cc0c45195ddbed" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/5f/a4/3e887bbd92164e183cb6e412c6a3e9198ddd446d7fe405958293ef5ef49c/ast_serialize-0.8.0-cp39-abi3-win_amd64.whl", hash = "sha256:861794565b06337005c1447ef23103a3d5a627d08bdc827870d00d0b28ef5f51" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/25/6c/b400476d3ceba681ab929787edc9554f6d88fcc69435eb681b00fc0457a5/ast_serialize-0.8.0-cp39-abi3-win_arm64.whl", hash = "sha256:b2a5978662fd4db463dfb4b974d2b10ac6430b98f5333aabc7051909df3561d0" },
]
[[package]]
name = "colorama"
version = "0.4.6"
source = { registry = "https://mirrors.cloud.tencent.com/pypi/simple" }
sdist = { url = "https://mirrors.cloud.tencent.com/pypi/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44" }
wheels = [
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6" },
]
[[package]]
name = "coverage"
version = "7.15.4"
source = { registry = "https://mirrors.cloud.tencent.com/pypi/simple" }
sdist = { url = "https://mirrors.cloud.tencent.com/pypi/packages/be/c3/4f2195f512fb172aa425a8803a874b2baa9ba7f80ff7b6080998761fc701/coverage-7.15.4.tar.gz", hash = "sha256:0548198fff07ccf4faf469520bce1c2eceb1ce3e62891921138dec10907f9d00" }
wheels = [
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/f1/84/651a9310859673aaa3b3203f1aa1641ca60fcf2494683e1c9474c7172780/coverage-7.15.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c705b28feb2775dc82a25f1d473a370bc37ff93f5177f4e29ce2425f560f6921" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/82/f9/4dcf700137e8af550670f4d74d1b63828ce93e1e2b05e5f10710eb2ea987/coverage-7.15.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3ff205ab5e3ecc670f6a4dd19d9cbf12ede53dd41cfc1e15716ec961ea6d314e" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/07/4a/612ff1e780b3fbfd637486f542f84adc5503873d8b5d279dec1ffeef9414/coverage-7.15.4-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5172326e861a38b48b48befca15e0f477a26b283337a33a739c8fed229934e36" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/b0/04/d1cff1c2ead4708a6a79c01d3736b6a25bd38a36678398f72a8dd33dfad9/coverage-7.15.4-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:12b59c90084e3234fb11184886bf4a40f4f16a8c8f867be2e087b81f8e8868d4" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/b9/80/d34e13fb4b293cbdb9665838cf5522077b8ad14ef947550631a4bced36a5/coverage-7.15.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:349062d66f00b40fa2c1c222438bad25fabf755631b5d82937fe985c8008615c" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/0f/e7/2c5fe7636fdb0732fe0f09f308a5b066864078b7fc61f6678e8478554f2e/coverage-7.15.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4256ced708e598e05209bc1a8ab4074e04a51dba4c62fb45926a229af675ace7" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/92/28/9689f0858dfff59c2ea688938ab9fa2925631235df67126a42b6c5c70ae1/coverage-7.15.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d80f974b20782d9612c8b4c9beeca867074c7cf4079d1419843fa25a26428b25" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/f9/e2/785077c230c157243eb5aa9a26c3be260ecd02001bead54a3cada3df8e03/coverage-7.15.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2e179f19bfe1d31f8eeeaa12990194d761c4f62f0759661000bca6cd8729f40b" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/d4/90/e20371b17b40f912f21305c2db2f30efa3de306f7320fc916804872c85a4/coverage-7.15.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8bc16bb47b7679670eceff71d78bfb7d6e5b143f6c2cd117487ec7c75e0d4b78" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/05/49/25371987ee459a5f67c0427fb75c74f9358e65f2c71fe75bf41c1b6c5fcb/coverage-7.15.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1cd685005cd2c4200adfc14cf39a603b9320efab3f18a8f7f156d20c9cc3345f" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/30/6e/32e67467f6154bf4f1c4f63b05acc5097cba4237d45bbeeea446b52e8ac1/coverage-7.15.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:337399ad2c93b3acd2a937627dae8b3e86b66707cd3d3e856347999aadf1ef8d" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/03/c1/8b24192e89286399765155251f99ee9f070a9d637109018ac23d99b99f6f/coverage-7.15.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:96e257121228ec5cd2bb919276e94ac11074471bc37d68dbae0e8308cce15fff" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/16/6f/8b41ebdf67c87854e17c035336a90f1cfbad0c14c2a584301be6ff148718/coverage-7.15.4-cp313-cp313-win32.whl", hash = "sha256:c65a9e0dfc6143491879da4e13b5e30f8be192055de508d737fb14601edbd22c" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/e0/e2/2946c7f0b42b152ecb21ff1bdad72e3d301e790c0c487e4a86e8c9f69347/coverage-7.15.4-cp313-cp313-win_amd64.whl", hash = "sha256:2ff8f5e9b8f7a94f0c11c45631eee103dbcb7d63274edd12c56efe1be690b3b4" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/9e/83/3f4a69957f48ae7a0aba76c34743f88963d607b19e03f3f8e66f91cae0f9/coverage-7.15.4-cp313-cp313-win_arm64.whl", hash = "sha256:6e0a8a5083b096487d6cfced94cdd514d8f5db6f113610fb36c0620edb1028cf" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/b4/d9/e70c286c979378f061d8266e279b686ab0b0b688e1fe0af864684f23a77d/coverage-7.15.4-py3-none-any.whl", hash = "sha256:964730a1e9de9c0cf11be6a1a3c79ce419c34882842abd256086ba4698705e84" },
]
[[package]]
name = "iniconfig"
version = "2.3.0"
source = { registry = "https://mirrors.cloud.tencent.com/pypi/simple" }
sdist = { url = "https://mirrors.cloud.tencent.com/pypi/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730" }
wheels = [
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12" },
]
[[package]]
name = "librt"
version = "0.15.0"
source = { registry = "https://mirrors.cloud.tencent.com/pypi/simple" }
sdist = { url = "https://mirrors.cloud.tencent.com/pypi/packages/36/9b/356320fbae2ac8467e21c5e73e1389c80468e4998c62cc7d3536cc51b614/librt-0.15.0.tar.gz", hash = "sha256:4e66cbe84437497d951b799d3e1551291b6fb3d643820a7014b3655d57a59162" }
wheels = [
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/e7/42/467b53a601b406ccd7b97c1fd54b59cb34f9185ad5ce7e9d5c3c4e8961c8/librt-0.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:db13ca398005abcbe538deda87b686d9bd08b7001cf40c4c06b444960ae10a26" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/3e/e6/36c2299b7a94b84fdd01220d8a777a71be5be0925bb0dbdf71c0a06a34d9/librt-0.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:aa1f1995789dca3698bc550aaceb09a51bd5df0a057ff84ff15296cd1975b801" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/c9/b6/ed5071f9325845e670bd36012757419767fbf56af77ed483077b9e4db541/librt-0.15.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55456ea87d8df21808446d03817be2f65e20391c1c615d9187440dff28cd08dc" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/7f/81/6450c67c3615d87704bcbc21323fafc69c799b06a044c447529f725d4b01/librt-0.15.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:5a86a5a08c2235316bdb359d5dbb6ce0abfca7fac06363103e2c5af571d92f95" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/e1/d6/5f52b722bc75076954b3bfd49be15ea362df4d580c6fb315d0f617100d30/librt-0.15.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e56b6a368529bed262da40ce13f8fef590db0479819cca84f16a1f01ac356d0b" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/8d/e2/c08fd1d36ce63ea5a12b85c5d37f4550b5f86a692167e41e5a74222607ae/librt-0.15.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:234d8d394721fa0d786af15ebf1f3fb7f3ed82fd1cd0cde45c2f247b5d4281d2" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/3f/d8/d9482fcbeb177b9eb87bb3899eeb3b42be690313c652f9e146b1d0681fb2/librt-0.15.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d8363d7accb0286ac3a0e633f396e93800dafb8150494505daf9515bbda591f3" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/10/cc/075171517b41f861753034fbb151b42cfc83bcc853849f24f5e66fd60ccf/librt-0.15.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0f0ee3644d951f31055ad07d77d92520e84505dd7a432cc4cd501dd70ee06785" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/b0/03/42c2330f37eeb475b6affeedd06518f60035f323af3a839335e3fc9fef2d/librt-0.15.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2cfd1a81a648806e6a7717be4cc4d1bb392fa229752bf8444ba365e381e984d6" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/57/1e/1ad4c5638f7e64d8560328bd25c54b409a661bdb6ff254b38ff90744288d/librt-0.15.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:a6cd22c9da0d866558e46a041f1cc0c2bbb26b61b137b2347fa834c332e1d101" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/49/41/39fa7d15db1204cd1cbe6514680fbdc243adf754a0885061308f43afc013/librt-0.15.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:6d5225ef8801e4ea5e482fa9b5dfb891dd9ef6f6d870f1f25d449ca2c70ac218" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/1e/88/c6dcf0dd8e26dc0c9a499a2abab8646c86dcaf9ecea9524cb46d3686331a/librt-0.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d28a05796b99f749bf8794f17ba9ba1612d0076b802e9cfc62c554634e9ce3b" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/1b/9b/ab54c71a7918a7c34fa5327fb61390a77446a07a146fbfb1165250a61035/librt-0.15.0-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:2067ff438048cead9d223ca5675bae2a25e520a7c3e6c1498bf9c6892d22caab" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/8d/b2/4f9a243bb892395f3becb80789ade13771701091f9f07ab8230247953ba8/librt-0.15.0-cp313-cp313-win32.whl", hash = "sha256:1cd3b721f24c206398b9e26da3c3a9c011e6e89d06f318ba8ebefc30f1003890" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/bf/af/64aff4885a40b93132382f2c314647d722574605416504379184ef3045ea/librt-0.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:f395a4a9a03ac062dbe9a9f82e0c720502e590a38feee6a757bc82e9c63afbd8" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/27/83/335bccf6c7cb9028cb0b54aead27d9ece3f01f83bc6baa2abace5da655c1/librt-0.15.0-cp313-cp313-win_arm64.whl", hash = "sha256:0a15cb554761247d84a3ec0cbdf4078d70725384f0e4662c0fa3b26266eb60ad" },
]
[[package]]
name = "loguru"
version = "0.7.3"
source = { registry = "https://mirrors.cloud.tencent.com/pypi/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
{ name = "win32-setctime", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://mirrors.cloud.tencent.com/pypi/packages/3a/05/a1dae3dffd1116099471c643b8924f5aa6524411dc6c63fdae648c4f1aca/loguru-0.7.3.tar.gz", hash = "sha256:19480589e77d47b8d85b2c827ad95d49bf31b0dcde16593892eb51dd18706eb6" }
wheels = [
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/0c/29/0348de65b8cc732daa3e33e67806420b2ae89bdce2b04af740289c5c6c8c/loguru-0.7.3-py3-none-any.whl", hash = "sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c" },
]
[[package]]
name = "mypy"
version = "2.3.1"
source = { registry = "https://mirrors.cloud.tencent.com/pypi/simple" }
dependencies = [
{ name = "ast-serialize" },
{ name = "librt", marker = "platform_python_implementation != 'PyPy'" },
{ name = "mypy-extensions" },
{ name = "pathspec" },
{ name = "typing-extensions" },
]
sdist = { url = "https://mirrors.cloud.tencent.com/pypi/packages/82/6a/878cc1097d4035f82bd516658d0c528d2a9955bc7b363afcbd0b07fea11b/mypy-2.3.1.tar.gz", hash = "sha256:47c1b1207258513a9d93495f69c8be9de73916186f0e52703e8c461b7a623419" }
wheels = [
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/de/cf/862010ee800ca9c2bd0c4c0dacf0f092e5411824a09b8f97ad4be8fe250e/mypy-2.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:114dff494000f18bd10d5d95d84b8567b26da60279ecbe838131841df20e635d" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/75/5a/3f3a2107b41e3e92e617e25daaee121413b91e9784bea733131ed4fecc5d/mypy-2.3.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8637731bb5eee3671eb2c3200827aa3564ed8a9309ecee4d1afe77e6d031bdb" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/8b/41/04dc4fe7e63d7820fa4eff272e95157d30cbea921388f3ab3fe77794cd0b/mypy-2.3.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c80fbc405ed8020f5ff3802dc18cf060197bcdd3fbdd6a26ef2fd34dfdd5226" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/96/fc/c3053b26b9054949285aa868cb6af8c10e7591541cacd79c5dcc06a1fcf9/mypy-2.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:84081f538ce27375045c02e3d7f81bd11d853400621ae245d87ce7b6c420ec74" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/70/4e/d77daab008bbc4e5001374d7928f4a260d28f0e6747af444fc4763f7a310/mypy-2.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:e9144ac16fde007096f9563eb2041b4433c2d705c4218edeb79e7e9d01035ee6" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/f0/f8/7eb68c136e4abd30569fe31ef2bfcb7eceae9952cab80017c04cd09f5d0c/mypy-2.3.1-cp313-cp313-win_arm64.whl", hash = "sha256:77ad9529e67dca28e511f5cd5671436584ce91f6d3bac159a353158187b986ac" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/8e/41/9675c7a1e78edecfba0b79e587a52594c56e189368261dc7b3a7fffb9527/mypy-2.3.1-py3-none-any.whl", hash = "sha256:6ed5c7e3419083268e5c9258bd1c1ef91af44a9e89374dbcaf37b775716e72eb" },
]
[[package]]
name = "mypy-extensions"
version = "1.1.0"
source = { registry = "https://mirrors.cloud.tencent.com/pypi/simple" }
sdist = { url = "https://mirrors.cloud.tencent.com/pypi/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558" }
wheels = [
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505" },
]
[[package]]
name = "numpy"
version = "2.5.2"
source = { registry = "https://mirrors.cloud.tencent.com/pypi/simple" }
sdist = { url = "https://mirrors.cloud.tencent.com/pypi/packages/9a/80/db0b4559e57ec36362bedbb05530a87fafbcb6067708c946967a41d449e7/numpy-2.5.2.tar.gz", hash = "sha256:d482d171c406ae88c5b19cad3b6a1c4c5209f886ab74bc44c2c865c23f52d860" }
wheels = [
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/f5/d2/6b24738a0ef4557d189b150046cd07823c50e4273e8aebd651222e24306f/numpy-2.5.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8e4cb9a754c8a0c62eaa88273a5fba3391f4a610d1dee893c0755da31c083f15" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/65/60/f2d208d366f263f39c6e69ed309290717aab41078b6d04c9be2a84fa2a07/numpy-2.5.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:52c808f96484f5571a5cc863775ce50247c17dfb3b0361f8ed6b4b0456f80080" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/3c/79/81e0bf24f4d020a2b1d5cd297a9f60c3f24eeb116f9bba5870443f7b6a4a/numpy-2.5.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:29d81e97f668489cba8ebfd796b9bdd453525d35dd9e162e2daec94bf3fc7740" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/ba/cc/e3141cf06d1a8a2c7e107543fe1269c1d1af760d4d683c0794a4ee1127c2/numpy-2.5.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:afb3f0632d6b2e3ba04dbce8d1e48d321b369138b73830b5ca371a0e8d479d56" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/29/f1/2a64a307d92c5d98f5255a4014eb43bb6103ee477087b61ecae44a3aa9b9/numpy-2.5.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0aadf13b60048d501e05fa699efaf7734e2494f3498a4c2a5521d822640324f3" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/7b/44/59a1eb68e773c4098d107ef34a0dbdeca501d72ffcfbff9a7707343921ce/numpy-2.5.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29b86ff8a6cc556b47ec6b64b194815cc80e6bf5eedcc6cddfd65318cb0b4eee" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/8a/4c/3e54d4ddbc359a1295f8b633e8106bcd4d7d4a206e82df051bdfb3058755/numpy-2.5.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6950c4b7dd562453090548ba7f5da7e59f57f85663f15d5dcc60e249192f7e59" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/f2/9f/02e371638ebf19b66d46231e4be52999e87f32d1961b113bc45656608b22/numpy-2.5.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b9727f472d2f3888053b8a75ab0cb94745a9de224bb5846dbadc0092101bc71d" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/eb/ae/ad6645abc7a3510fe48e8ea1ab4598166f500057ef4ebf38bfad4f1577de/numpy-2.5.2-cp313-cp313-win32.whl", hash = "sha256:4f9744f9fbdcea0bc552e8f19e1f141f811a3f9bc2be2cc6e86d982cab23e3f4" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/15/20/f3489f86d81ea460b2bcdceaed094142ca6579f6be0ec527b781d39afe68/numpy-2.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:85aaccb24182c25df891ad0ec333585967e115269d5f1b17f2c9ae005bc96657" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/d5/21/35b31dde1b283b79de828b80f876afd8c94e28fe1e9c375f89e261cc4c0d/numpy-2.5.2-cp313-cp313-win_arm64.whl", hash = "sha256:bd68ece1553d2023c09a4226d9e41c586ad2d20594d1a456186c33513d2cb3f2" },
]
[[package]]
name = "packaging"
version = "26.3"
source = { registry = "https://mirrors.cloud.tencent.com/pypi/simple" }
sdist = { url = "https://mirrors.cloud.tencent.com/pypi/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79" }
wheels = [
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c" },
]
[[package]]
name = "pandas"
version = "3.0.5"
source = { registry = "https://mirrors.cloud.tencent.com/pypi/simple" }
dependencies = [
{ name = "numpy" },
{ name = "python-dateutil" },
{ name = "tzdata", marker = "sys_platform == 'emscripten' or sys_platform == 'win32'" },
]
sdist = { url = "https://mirrors.cloud.tencent.com/pypi/packages/be/4f/5f3422a2afec5ffc46308b79e53291365a93748b498ac2e58bead0197916/pandas-3.0.5.tar.gz", hash = "sha256:dca3734d6ab7c906e6730f0788b0a1dbb9f2467731f9711f77995c8e9d62d712" }
wheels = [
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/bf/09/7b95c4a0025227d6f118c4039b423412ac6a982db02864166185d812fbc7/pandas-3.0.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c1c05a767fe8e5b4fe9e1c29806829c582052eaedb9120a3da83ba3f69e24a5b" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/8d/0c/dc78fd8c4da477b4b5e8ad37295af352190d21ef63a9ee1bc071753074cc/pandas-3.0.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b86765f268b56f7e665b93bce9d5df69dee7f99e595cf8fb839483ab315942a3" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/3e/71/3592c055cf44df9808550f9368ceda80ff2b224d355ef73fe251dcda1802/pandas-3.0.5-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c597ecf5616b5c420372c1d4d4c00dbbfba7398bea857dcc984347e1ea48417b" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/e3/70/4363150359f95b4cb4bcbb34ca23572bb5495749a621a8f3d5a1ddfd293c/pandas-3.0.5-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4b11c36e218331d0387cbe3a0a5f75162357a1d92d57b2b08a336ff94b19b2be" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/f7/d0/317e7a0c67c0e69fa905a0161409397a7dc2d46ff611f6ca4803352c042b/pandas-3.0.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cf52e1f61d229496da17dc7ab54acdee627357e7008fd4fecba3d0ba2937fa58" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/f1/8d/36dade89b49e4f9d5cbdbe863772581f98c0c6d78fc39ad4c557f6f2e17e/pandas-3.0.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:db172144bb56422bd157812f3b021eacc255451470b31e2c633c349490a1cfee" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/9c/ba/18c4ec8a746e177da05a9e7a7963781d8ea195780724f854601b6ebd6b78/pandas-3.0.5-cp313-cp313-win_amd64.whl", hash = "sha256:0d298e951f23016ce4699951d044ae6418dbc91bf68cefca0f77666fcbb4e5c6" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/de/ec/28a57266b753799a87b8bc79e7887ac6fd981b8c6d2978a0b7e7b6bd708c/pandas-3.0.5-cp313-cp313-win_arm64.whl", hash = "sha256:66266d3442a5e8b3c90274c2b8b230bee42dd1c286bc822cc2f9f2c7e12b883e" },
]
[[package]]
name = "pathspec"
version = "1.1.1"
source = { registry = "https://mirrors.cloud.tencent.com/pypi/simple" }
sdist = { url = "https://mirrors.cloud.tencent.com/pypi/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a" }
wheels = [
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189" },
]
[[package]]
name = "pluggy"
version = "1.6.0"
source = { registry = "https://mirrors.cloud.tencent.com/pypi/simple" }
sdist = { url = "https://mirrors.cloud.tencent.com/pypi/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3" }
wheels = [
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746" },
]
[[package]]
name = "pygments"
version = "2.21.0"
source = { registry = "https://mirrors.cloud.tencent.com/pypi/simple" }
sdist = { url = "https://mirrors.cloud.tencent.com/pypi/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c" }
wheels = [
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9" },
]
[[package]]
name = "pytest"
version = "9.1.1"
source = { registry = "https://mirrors.cloud.tencent.com/pypi/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
{ name = "iniconfig" },
{ name = "packaging" },
{ name = "pluggy" },
{ name = "pygments" },
]
sdist = { url = "https://mirrors.cloud.tencent.com/pypi/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313" }
wheels = [
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c" },
]
[[package]]
name = "pytest-asyncio"
version = "1.4.0"
source = { registry = "https://mirrors.cloud.tencent.com/pypi/simple" }
dependencies = [
{ name = "pytest" },
]
sdist = { url = "https://mirrors.cloud.tencent.com/pypi/packages/43/7c/d36d04db312ecf4298932ef77e6e4a9e8ad017906e24e34f0b0c361a2473/pytest_asyncio-1.4.0.tar.gz", hash = "sha256:c6c0d2259945122819f171a32ecea2c349ead889ee28176caaf492143424be42" }
wheels = [
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/03/e2/08a497ef684b88559c9cc5f4ad53a37e7b99e727094a86d6ea32536d5d3c/pytest_asyncio-1.4.0-py3-none-any.whl", hash = "sha256:933ca923a23075a87fb7070c0ec272a6848489824d887c85c812670932835aa1" },
]
[[package]]
name = "pytest-cov"
version = "7.1.0"
source = { registry = "https://mirrors.cloud.tencent.com/pypi/simple" }
dependencies = [
{ name = "coverage" },
{ name = "pluggy" },
{ name = "pytest" },
]
sdist = { url = "https://mirrors.cloud.tencent.com/pypi/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2" }
wheels = [
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678" },
]
[[package]]
name = "python-dateutil"
version = "2.9.0.post0"
source = { registry = "https://mirrors.cloud.tencent.com/pypi/simple" }
dependencies = [
{ name = "six" },
]
sdist = { url = "https://mirrors.cloud.tencent.com/pypi/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3" }
wheels = [
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427" },
]
[[package]]
name = "quant-engine"
version = "0.1.0"
source = { editable = "." }
dependencies = [
{ name = "loguru" },
{ name = "numpy" },
{ name = "pandas" },
{ name = "scipy" },
]
[package.optional-dependencies]
dev = [
{ name = "mypy" },
{ name = "pytest" },
{ name = "pytest-asyncio" },
{ name = "pytest-cov" },
{ name = "ruff" },
]
[package.metadata]
requires-dist = [
{ name = "loguru", specifier = ">=0.7" },
{ name = "mypy", marker = "extra == 'dev'", specifier = ">=1.10" },
{ name = "numpy", specifier = ">=1.24" },
{ name = "pandas", specifier = ">=2.0" },
{ name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0" },
{ name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.23" },
{ name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=4.1" },
{ name = "ruff", marker = "extra == 'dev'", specifier = ">=0.4" },
{ name = "scipy", specifier = ">=1.10" },
]
provides-extras = ["dev"]
[[package]]
name = "ruff"
version = "0.16.4"
source = { registry = "https://mirrors.cloud.tencent.com/pypi/simple" }
sdist = { url = "https://mirrors.cloud.tencent.com/pypi/packages/00/8f/d8074b1f25e003164087a8bfe79a0f1a3945135764dbb6aaab04103dcaf9/ruff-0.16.4.tar.gz", hash = "sha256:13171aa9d9af2240ee3504e639de73122c67e74036de5ba2e1d01422cd17e3dc" }
wheels = [
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/ff/80/779895ef584e089d22f2c6df0d0e99a65ec2df0805f1fffd439415b8c1f0/ruff-0.16.4-py3-none-linux_armv6l.whl", hash = "sha256:df4075f71ddac40b9934af60c3ec8a53047dd5a5fdc43224e6e4e8e9a27cb6f7" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/a9/e6/f553199b5e8927a05cb5c422d921fd0656b29ab976e91c44802107c6b0da/ruff-0.16.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:0c95538517af68004306b0fb3214ff2f2af67a65092aee77cd9eb86db6656604" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/1c/70/4a6dc4bb34da4dee35e30f09bbd1bfbdd26f33b62fb9b8df31f08a199cd2/ruff-0.16.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:963f83df8e69e575b64d67dd447ebbc917db41a14bf38d4593a4183e7aaa8255" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/24/12/c6e22d686372c15bcb7af99831f1a1be96df696491babf4f24e4f942c527/ruff-0.16.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32a5057c7ff3f6e6480a48fccfb3a412a690f48a3d03ac5cf08177d6c2da3ade" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/46/49/72b10ec912f5ab5854992eaf7aa7cd36729b6937d9dc4e0fb41b3bf428ec/ruff-0.16.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b3dce8d9b0c57c265b91885a66a567d8ea1372e8eb4e250fa8e5e3f579e99cff" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/fa/80/0f30e32e7f6ee26edc39075502db9d368d788a44a79b55f763eb4ab03796/ruff-0.16.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7dc651db49283c69f8e72c834eec4fe5573e4c646856aebece0ce385dceb2a80" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/52/3d/86e8ad3542169e56cac3859a343afdb9df2ad54d35a59ce1e67baee83421/ruff-0.16.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3817b87dbcabc92f13b05019257c5b89b5b4d51b5fb20f56fb5235ceb723cd07" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/d0/16/481c29b380c20a0054a8261066665e1b3488e23636c49d0a43e75975b9bb/ruff-0.16.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e9fce1499134b2c8c68e5166f95705a5812062bb93aacc5f9873bb1a27084bc7" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/5e/b6/56bc0b8cf45b54b28b3a5e6381c8945d51b5b18adf659454c32295209a31/ruff-0.16.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2d812e482f5a7e02eee26cd73d2a37ebbdf47d795ea63ba1b89110ae93e9fb3" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/e8/8b/b345b4fb110f2fbe2bd31eabd271e5e8b3b7e4ee6c0e02f2dc6be78db000/ruff-0.16.4-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:6baaf984aa7976edf93d3b627fe2d1d22ee94bbca05fa6f90fc76d73924e3454" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/29/e5/827b34041c35f58774a9681a4213994c164fc987800f4dddabcf451da0bf/ruff-0.16.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:bdfcf0b28662eb890372d50f92c283bb94e67e7635ed93c7fd533970acff7b2b" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/0f/10/d0bffcdd6729b87afc82ba0ef377173356a7dc8e972f5179968cf2fdf98c/ruff-0.16.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b66b02cb9b04f537643cadf5768e5f98dc461890d530cb67113d71c8c76e605d" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/f5/32/0db2a863b796ca62d83e92a07a3ccf00921b14db02059347576a2fda3d4b/ruff-0.16.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:8528bf9a4b291a60bf02ea453511e8ce6215bd2b982ee80405b66b008b6c30a0" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/b2/a0/fbdeb59e48c6261f523e56c8f12e9c08fbe693786595cc7e3959207a9232/ruff-0.16.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:fbd85d2875fdd67e833213a651f613bbf25303abf6aa822a5121f4531195678d" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/aa/28/0c6dd865859c6d17bc8ccc34cb72b0e02d6c7eb25e8a1e22b5bea681e2c0/ruff-0.16.4-py3-none-win32.whl", hash = "sha256:312769988007aaeb8e189b443ccdd03c0e6374489e053467be6d96518ebff76e" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/a3/03/e724450f621698117f9aa6dd241c94d0274ae96781378dc86745ae29f0e7/ruff-0.16.4-py3-none-win_amd64.whl", hash = "sha256:05d9d27a18c4bcbefada602480ec9e01e0bc949d432e0ced5df77edac195919c" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/0e/fe/da8b9e1347696bb22120b77280ec5ce25d500ca5cb39d5ad6e5c18de19c1/ruff-0.16.4-py3-none-win_arm64.whl", hash = "sha256:a3a61621c9b6f6a89573e938a080e648f1695baa3f58570a3a707bc51ff65a21" },
]
[[package]]
name = "scipy"
version = "1.18.1"
source = { registry = "https://mirrors.cloud.tencent.com/pypi/simple" }
dependencies = [
{ name = "numpy" },
]
sdist = { url = "https://mirrors.cloud.tencent.com/pypi/packages/7e/74/66de6258867beb2ef08f35f9f2ac017a52cacd5081714d239ff1a442d458/scipy-1.18.1.tar.gz", hash = "sha256:52c4b7422442aba924d03ad4019852b08a92e64ea187b933135687bfe2747307" }
wheels = [
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/b6/55/4540ee0f9c42a9ad7109d0d1a8cc70de54c3572b01c6693a2b1c70e90ceb/scipy-1.18.1-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:3ab3523da44749156e1f68b464dc56af11ae4cbc5c739a49d05f32b982eca9f3" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/2a/f5/769f36d14922b8071a43e95d24d18b6bdafad10d7f5cf647867e1ac052bc/scipy-1.18.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:e6fb6a55cc0ba97b59a1f288fb86dc6fce8bdfc0fffcbfd015e3a954bf2a2d93" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/9a/d7/21d890274f75ea37a8209d5519e72da3da90302e3b9fb8397a0918386a62/scipy-1.18.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:ea324d9dd34c38bfb9bec8ca4d1b407db97dbb74029f566b8e322b1b6fe56fe6" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/ec/01/798430ecea2e78ec7c02663d5f71c007bb6abeca931080debd40d7fa55ea/scipy-1.18.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:75b00eb8fb802090aa903f4ea1c7f5a584779f967361e68b7e98e531cc2d7174" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/e6/5f/4634e9d35c68496e4e34cb6946eafab044458e6cedab42b40b6588e475b6/scipy-1.18.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d416b16cccfd70fbf62400e84d0bb2f4e6af519a45557f1692c749b37f14b315" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/41/48/6450ed9243315322bbc19ac57b9b70d66a20bf1d38d124c96bc4bf6af9ea/scipy-1.18.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fdaf5ea890a6183d0565f51a61799d67081bd5b1cf03c5f4b3fd3732108625c9" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/00/bd/bf5a4be6a3525676499f6dff307991739ff6fdcad1481b1aeb6745339f58/scipy-1.18.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c825cef2f49e46753726a7181a8e199804a912b29519ada542c6ebc654951899" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/bd/4e/3c45c33e00a77996c4b1cb707929f833ba7b1d522ee29f882512c330676d/scipy-1.18.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e3b417bf8c2c7c16e8f58ad91db17783ec911ac16e7b50eb6eab6e809b4f5b07" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/93/0e/e0348fbc0dbab65c114cf78957e7dfeb49f8e8b556b4d930cc12ff195e18/scipy-1.18.1-cp313-cp313-win_amd64.whl", hash = "sha256:559ed65f60c1af5a03f3912605a1b5114f522c7c32fb23c3376ae8f03219fe28" },
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/50/a8/6a77f5f267c555108f0a864b6db714363dab567a8266422a79a385f9232b/scipy-1.18.1-cp313-cp313-win_arm64.whl", hash = "sha256:cd479fc04dd9401e3b4f49e76518768ef99c4f517a98c284eb091fd725719adf" },
]
[[package]]
name = "six"
version = "1.17.0"
source = { registry = "https://mirrors.cloud.tencent.com/pypi/simple" }
sdist = { url = "https://mirrors.cloud.tencent.com/pypi/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81" }
wheels = [
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274" },
]
[[package]]
name = "typing-extensions"
version = "4.16.0"
source = { registry = "https://mirrors.cloud.tencent.com/pypi/simple" }
sdist = { url = "https://mirrors.cloud.tencent.com/pypi/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5" }
wheels = [
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8" },
]
[[package]]
name = "tzdata"
version = "2026.3"
source = { registry = "https://mirrors.cloud.tencent.com/pypi/simple" }
sdist = { url = "https://mirrors.cloud.tencent.com/pypi/packages/92/ff/5a28bdfd8c3ebec42564ac7d0e54ca3db65044a9314a97f9564fa7a1e926/tzdata-2026.3.tar.gz", hash = "sha256:4a1518b8993086a7982523e071643f3c0e5f213e75b21318e78bcabfff9d1415" }
wheels = [
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/e5/6d/b53b99a9f2766d095985947a5782f1702cabb129a34f7a802d7197af832f/tzdata-2026.3-py2.py3-none-any.whl", hash = "sha256:dc096730c87af6cab1b171c9d532be840741ff5d459015e7f6947bd7d7e54931" },
]
[[package]]
name = "win32-setctime"
version = "1.2.0"
source = { registry = "https://mirrors.cloud.tencent.com/pypi/simple" }
sdist = { url = "https://mirrors.cloud.tencent.com/pypi/packages/b3/8f/705086c9d734d3b663af0e9bb3d4de6578d08f46b1b101c2442fd9aecaa2/win32_setctime-1.2.0.tar.gz", hash = "sha256:ae1fdf948f5640aae05c511ade119313fb6a30d7eabe25fef9764dca5873c4c0" }
wheels = [
{ url = "https://mirrors.cloud.tencent.com/pypi/packages/e1/07/c6fe3ad3e685340704d314d765b7912993bcb8dc198f0e7a89382d37974b/win32_setctime-1.2.0-py3-none-any.whl", hash = "sha256:95d644c4e708aba81dc3704a116d8cbc974d70b3bdb8be1d150e36be6e9d1390" },
]