feat(factor): M7.1 因子定义入库 + /api/factors 读库(目录契约源)
- factor_definition 表(migration b7f2a5e81c33,MySQL 已应用):name 主键 + 元数据 (formula/brief/frequency/lookback/direction/requires JSON/version)+ FactorDefinition entity(from_registry_def 由代码注册表构造) - FactorRepository Protocol + SQLAlchemy 实现(幂等 upsert/list/get) - /api/factors 改读 DB;目录为空自动 seed 注册表(幂等)—— 保留自定义因子登记能力 (计算仍须代码注册,引用未注册因子照常 FactorError,防伪因子) - tests/test_factor_catalog.py(repo 幂等/roundtrip/registry seed、API seed+字段齐全); test_api 的 client fixture 补 tmp sqlite session(factors 读库);全量 pytest 通过
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
"""因子目录 Repository 的 SQLAlchemy 实现(M7.1)。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.domain.entities.factor import FactorDefinition
|
||||
from app.infrastructure.persistence.sqlalchemy.models.factor import FactorDefinitionModel
|
||||
|
||||
|
||||
def _to_entity(row: FactorDefinitionModel) -> FactorDefinition:
|
||||
return FactorDefinition(
|
||||
name=row.name,
|
||||
description=row.description,
|
||||
formula=row.formula,
|
||||
brief=row.brief,
|
||||
frequency=row.frequency,
|
||||
lookback=row.lookback,
|
||||
direction=row.direction,
|
||||
requires=json.loads(row.requires_json or "[]"),
|
||||
version=row.version,
|
||||
created_at=row.created_at,
|
||||
)
|
||||
|
||||
|
||||
class SqlAlchemyFactorRepository:
|
||||
def __init__(self, session: Session) -> None:
|
||||
self._session = session
|
||||
|
||||
def upsert_many(self, definitions: list[FactorDefinition]) -> int:
|
||||
if not definitions:
|
||||
return 0
|
||||
existing = {
|
||||
r.name: r
|
||||
for r in self._session.scalars(
|
||||
select(FactorDefinitionModel).where(
|
||||
FactorDefinitionModel.name.in_([d.name for d in definitions])
|
||||
)
|
||||
)
|
||||
}
|
||||
now = datetime.now()
|
||||
for d in definitions:
|
||||
row = existing.get(d.name)
|
||||
if row is None:
|
||||
self._session.add(
|
||||
FactorDefinitionModel(
|
||||
name=d.name,
|
||||
description=d.description,
|
||||
formula=d.formula,
|
||||
brief=d.brief,
|
||||
frequency=d.frequency,
|
||||
lookback=d.lookback,
|
||||
direction=d.direction,
|
||||
requires_json=json.dumps(d.requires),
|
||||
version=d.version,
|
||||
created_at=now,
|
||||
)
|
||||
)
|
||||
else:
|
||||
for k, v in d.model_dump(exclude={"created_at"}).items():
|
||||
if k == "requires":
|
||||
v = json.dumps(v)
|
||||
setattr(row, k, v)
|
||||
return len(definitions)
|
||||
|
||||
def list(self) -> list[FactorDefinition]:
|
||||
rows = self._session.scalars(
|
||||
select(FactorDefinitionModel).order_by(FactorDefinitionModel.name)
|
||||
).all()
|
||||
return [_to_entity(r) for r in rows]
|
||||
|
||||
def get(self, name: str) -> FactorDefinition | None:
|
||||
row = self._session.get(FactorDefinitionModel, name)
|
||||
return _to_entity(row) if row else None
|
||||
Reference in New Issue
Block a user