Files
qlib/backend/app/infrastructure/persistence/sqlalchemy/repositories/signal_impl.py
T
Simon 995ed08548 feat(chart): M9-1 Chart DTO + Chart Service + Chart API(v3 §20)
- domain/entities/chart.py:ChartResult/OHLC/Volume/Series/EventMarker/ChartMetadata
  (adjust_mode + execution_price_basis 口径元数据)+ SelectionHit
- application/services/chart_service.py:个股 K线/量/MA 指标;显示层 qfq/hfq 折算
  (基于主口径 none 行情 × adjust_factor,绝回写研究数据);回测个股视图把实际成交
  转 fills 标记并在显示口径不同时做坐标换算(v3 §20.3/§20.5)
- by-symbol 历史查询:SignalRepository/SelectionRepository.list_by_symbol(含溯源 id)
- api/charts.py:/stocks/{symbol}/chart|signals|selections、/backtests/{id}/stocks/{symbol}/chart
  |trades|positions
- tests/test_charts.py(指标/qfq-hfq 折算断言/回测 fills/API 集成+404);全量 pytest 通过
2026-09-09 07:09:52 +08:00

129 lines
4.6 KiB
Python

"""Signal Repository 的 SQLAlchemy 实现(M8.1)。"""
from __future__ import annotations
import json
from datetime import date, datetime
from sqlalchemy import select
from sqlalchemy.orm import Session
from app.domain.entities.signal import (
SignalEvent,
SignalHit,
SignalMeta,
SignalResult,
SignalRules,
SignalStatistics,
)
from app.infrastructure.persistence.sqlalchemy.models.signal import (
SignalEventModel,
SignalSnapshotModel,
)
class SqlAlchemySignalRepository:
def __init__(self, session: Session) -> None:
self._session = session
def save(self, signal_id: str, result: SignalResult) -> None:
self._session.add(
SignalSnapshotModel(
id=signal_id,
as_of=result.as_of_date,
query_json=json.dumps(
{
"as_of": result.as_of_date.isoformat(),
**{k: v for k, v in result.config_snapshot.items() if k != "as_of"},
},
ensure_ascii=False,
),
rules_json=json.dumps(result.rules.model_dump(mode="json"), ensure_ascii=False),
statistics_json=json.dumps(result.statistics.model_dump(mode="json")),
created_at=datetime.now(),
)
)
now = datetime.now()
for e in result.events:
self._session.add(
SignalEventModel(
signal_id=signal_id,
symbol=e.symbol,
signal_date=e.signal_date,
signal_type=e.signal_type,
score=e.score,
price=e.price,
reason_json=json.dumps(e.trigger_reason, ensure_ascii=False),
created_at=now,
)
)
self._session.flush()
def get(self, signal_id: str) -> SignalResult | None:
snap = self._session.get(SignalSnapshotModel, signal_id)
if snap is None:
return None
rows = self._session.scalars(
select(SignalEventModel)
.where(SignalEventModel.signal_id == signal_id)
.order_by(SignalEventModel.signal_type, SignalEventModel.symbol)
).all()
rules = SignalRules.model_validate_json(snap.rules_json)
stats = SignalStatistics.model_validate_json(snap.statistics_json)
return SignalResult(
as_of_date=snap.as_of,
rules=rules,
statistics=stats,
events=[
SignalEvent(
symbol=r.symbol,
signal_date=r.signal_date,
signal_type=r.signal_type,
score=float(r.score) if r.score is not None else None,
price=float(r.price) if r.price is not None else None,
trigger_reason=json.loads(r.reason_json or "[]"),
)
for r in rows
],
config_snapshot={"as_of": snap.as_of.isoformat()},
)
def list_by_symbol(self, symbol: str, limit: int = 50) -> list[SignalHit]:
rows = self._session.scalars(
select(SignalEventModel)
.where(SignalEventModel.symbol == symbol)
.order_by(SignalEventModel.signal_date.desc(), SignalEventModel.id.desc())
.limit(limit)
).all()
return [
SignalHit(
signal_id=r.signal_id,
signal_date=r.signal_date,
signal_type=r.signal_type,
score=float(r.score) if r.score is not None else None,
price=float(r.price) if r.price is not None else None,
trigger_reason=json.loads(r.reason_json or "[]"),
)
for r in rows
]
def list_recent(self, as_of: date | None = None, limit: int = 20) -> list[SignalMeta]:
stmt = select(SignalSnapshotModel).order_by(SignalSnapshotModel.created_at.desc())
if as_of is not None:
stmt = stmt.where(SignalSnapshotModel.as_of == as_of)
stmt = stmt.limit(limit)
metas: list[SignalMeta] = []
for snap in self._session.scalars(stmt).all():
stats = SignalStatistics.model_validate_json(snap.statistics_json)
metas.append(
SignalMeta(
id=snap.id,
as_of=snap.as_of,
buy=stats.buy,
watch=stats.watch,
sell=stats.sell,
created_at=snap.created_at,
)
)
return metas