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 通过
This commit is contained in:
@@ -12,6 +12,7 @@ from datetime import date, datetime
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.domain.entities.chart import SelectionHit
|
||||
from app.domain.entities.selection import (
|
||||
SelectionCandidate,
|
||||
SelectionMeta,
|
||||
@@ -84,6 +85,27 @@ class SqlAlchemySelectionRepository:
|
||||
config_snapshot=json.loads(snap.query_json),
|
||||
)
|
||||
|
||||
def list_by_symbol(self, symbol: str, limit: int = 50) -> list[SelectionHit]:
|
||||
rows = self._session.execute(
|
||||
select(SelectionResultModel, SelectionSnapshotModel.as_of, SelectionSnapshotModel.method)
|
||||
.join(SelectionSnapshotModel, SelectionSnapshotModel.id == SelectionResultModel.selection_id)
|
||||
.where(SelectionResultModel.symbol == symbol)
|
||||
.order_by(SelectionSnapshotModel.as_of.desc(), SelectionResultModel.rank)
|
||||
.limit(limit)
|
||||
).all()
|
||||
return [
|
||||
SelectionHit(
|
||||
selection_id=r[0].selection_id,
|
||||
as_of=r[1],
|
||||
method=r[2],
|
||||
symbol=r[0].symbol,
|
||||
rank=r[0].rank,
|
||||
score=float(r[0].score),
|
||||
selection_reason=json.loads(r[0].reason_json or "[]"),
|
||||
)
|
||||
for r in rows
|
||||
]
|
||||
|
||||
def list_recent(
|
||||
self,
|
||||
as_of: date | None = None,
|
||||
|
||||
@@ -10,6 +10,7 @@ from sqlalchemy.orm import Session
|
||||
|
||||
from app.domain.entities.signal import (
|
||||
SignalEvent,
|
||||
SignalHit,
|
||||
SignalMeta,
|
||||
SignalResult,
|
||||
SignalRules,
|
||||
@@ -87,6 +88,25 @@ class SqlAlchemySignalRepository:
|
||||
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:
|
||||
|
||||
Reference in New Issue
Block a user