New module agent/src/factors/ providing the foundation for the Alpha
Zoo:
- base.py: 19 cross-sectional + time-series operators on wide
pd.DataFrame panels (rank/scale/ts_*/delta/decay_linear/signed_power/
safe_div + market-aware vwap). NaN propagates strictly; delta(d>=1)
enforced at the operator layer; no negative shifts allowed.
- registry.py: AST-extracts __alpha_meta__ from each zoo .py module
without importing it; pydantic AlphaMeta (extra="forbid",
frozen=True). Lazy importlib.import_module on compute(); filesystem-
loader fallback for plugin/test trees. Sanity gates reject +/-inf,
NaN ratio > 95%, and shape mismatches.
- factor_analysis_core.py: IC/IR + layered backtest math extracted
from agent/src/tools/factor_analysis_tool.py so it's reusable from
CLI, bench tool, and API.
- bench_runner.py: per-alpha compute → IC vs forward return →
alive/reversed/dead categorisation (mean IC > 0.02, pos_ratio >=
0.55, |t-stat| > 2). Shared by CLI driver and API worker.
Multi-factor skill gains a panel-style ZooSignalEngine.from_zoo()
factory (cross-sectional standardisation + weighting + optional
top-N / bottom-N long-short conversion). The legacy per-symbol
example_signal_engine.py is left untouched. SKILL.md edits in
multi-factor and factor-research cross-reference the new zoo.
Safety floor:
- test_alpha_purity.py: AST allowlist scan of every zoo/**/*.py
module. Allowed imports: pandas, numpy, scipy.*, src.factors.base,
__future__, typing, math, dataclasses. Banned names: os/sys/
subprocess/socket/urllib/requests/httpx/pathlib/Path/open/eval/exec/
compile/__import__, plus dunder-access via getattr(_, "__*"),
breakpoint, input, help, memoryview, globals, locals, vars, and
the __class__/__bases__/__mro__/__globals__/__builtins__ chain.
Module-level statements limited to docstring + imports + functions
+ ALPHA_ID/__alpha_meta__ assignments with literal RHS.
- test_lookahead.py: 300-row synthetic panel; corrupt rows >= 270;
assert factor at probe row 260 unchanged within 1e-9.
- test_base.py + test_registry.py + conftest.py with opt-in
pytest-socket integration that hard-fails outbound network during
the factors test suite.
- tools/ci_grep_gates.sh: rejects yaml.load( without safe_load,
any trademarked-name leak in shipped artifacts, and any per-stock-
code data leak in wiki/**/*.{json,csv,html}.
Governance:
- CONTRIBUTING.md with the Developer Certificate of Origin trailer
for community PRs and the alpha PR reviewer checklist.
- NOTICE at the repo root with Apache-2.0 attribution for the
upstream qlib NOTICE and the formulas-as-mathematical-content
stance for Kakushadze / GTJA / Fama-French sources.
35 lines
1.4 KiB
Python
35 lines
1.4 KiB
Python
"""Test isolation for ``agent/tests/factors/``.
|
|
|
|
Two responsibilities:
|
|
|
|
1. **Network kill-switch** — alphas must be pure functions of the input panel.
|
|
We disable all sockets during factor tests via ``pytest-socket``. If the
|
|
package is not installed we skip silently (CI installs it from the
|
|
``dev`` extra in ``pyproject.toml``).
|
|
2. **Import root** — the bundled zoo modules live at ``agent/src/factors/zoo``
|
|
and import each other via the ``src.factors.*`` package path. The pytest
|
|
config in ``pyproject.toml`` already sets ``pythonpath = ["agent"]`` so no
|
|
action is needed here, but we keep this file in the test directory so the
|
|
socket hook is scoped to factors-only.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
try:
|
|
from pytest_socket import disable_socket, enable_socket
|
|
except ImportError: # pragma: no cover - exercised only in stripped envs
|
|
disable_socket = None # type: ignore[assignment]
|
|
enable_socket = None # type: ignore[assignment]
|
|
|
|
|
|
def pytest_runtest_setup(item) -> None: # noqa: D401 - pytest hook
|
|
"""Disable sockets before every factors test (scoped, not global)."""
|
|
if disable_socket is not None:
|
|
disable_socket()
|
|
|
|
|
|
def pytest_runtest_teardown(item) -> None: # noqa: D401 - pytest hook
|
|
"""Re-enable sockets after each test so non-factors tests stay untouched."""
|
|
if enable_socket is not None:
|
|
enable_socket()
|