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:
@@ -0,0 +1,88 @@
|
||||
"""统一量化可视化 DTO(v3 §20 Chart Service)。
|
||||
|
||||
原则:前端只展示本结构,不得自行重算选股/信号/成交(v3 §20.1)。
|
||||
价格口径:bars 已按请求 adjust 折算(显示层);成交/信号标记与 K 线同坐标系;
|
||||
metadata 记录 adjust_mode 与回测执行价 basis,杜绝图表与回测口径混用(v3 §20.5)。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import date
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class OHLC(BaseModel):
|
||||
time: date
|
||||
open: float | None = None
|
||||
high: float | None = None
|
||||
low: float | None = None
|
||||
close: float | None = None
|
||||
|
||||
|
||||
class VolumePoint(BaseModel):
|
||||
time: date
|
||||
value: float | None = None
|
||||
|
||||
|
||||
class SeriesPoint(BaseModel):
|
||||
"""指标/分数等 (时间, 值) 序列点。"""
|
||||
|
||||
time: date
|
||||
value: float | None = None
|
||||
|
||||
|
||||
class EventMarker(BaseModel):
|
||||
"""K 线上可点击的事件标记(选股/信号/实际成交)。"""
|
||||
|
||||
time: date
|
||||
kind: str = Field(
|
||||
description="selection | signal_buy | signal_sell | signal_watch | fill_buy | fill_sell"
|
||||
)
|
||||
symbol: str = ""
|
||||
price: float | None = None
|
||||
score: float | None = None
|
||||
text: list[str] = Field(default_factory=list, description="原因/说明(tooltip)")
|
||||
ref_id: str | None = Field(default=None, description="关联 selection/signal 记录 id")
|
||||
|
||||
|
||||
class ChartMetadata(BaseModel):
|
||||
symbol: str
|
||||
name: str = ""
|
||||
adjust_mode: str = Field(default="none", description="显示口径:none | qfq | hfq")
|
||||
price_basis: str = Field(default="chart_display", description="显示价基准(显示层折算)")
|
||||
execution_price_basis: str | None = Field(
|
||||
default=None, description="回测执行价口径(如 none),与显示口径不同时用于解释"
|
||||
)
|
||||
start: date | None = None
|
||||
end: date | None = None
|
||||
bar_count: int = 0
|
||||
indicator_windows: list[int] = Field(default_factory=list)
|
||||
|
||||
|
||||
class ChartResult(BaseModel):
|
||||
"""单只股票 / 回测个股的统一图表数据(v3 §20.2)。"""
|
||||
|
||||
metadata: ChartMetadata
|
||||
bars: list[OHLC] = Field(default_factory=list)
|
||||
volume: list[VolumePoint] = Field(default_factory=list)
|
||||
indicators: dict[str, list[SeriesPoint]] = Field(default_factory=dict)
|
||||
selections: list[EventMarker] = Field(default_factory=list)
|
||||
signals: list[EventMarker] = Field(default_factory=list)
|
||||
fills: list[EventMarker] = Field(default_factory=list)
|
||||
holding_periods: list[dict] = Field(default_factory=list)
|
||||
factor_values: dict[str, list[SeriesPoint]] = Field(default_factory=dict)
|
||||
strategy_scores: dict[str, list[SeriesPoint]] = Field(default_factory=dict)
|
||||
unimplemented: list[str] = Field(default_factory=list)
|
||||
|
||||
|
||||
class SelectionHit(BaseModel):
|
||||
"""个股在选股历史中的命中(by-symbol 查询)。"""
|
||||
|
||||
selection_id: str
|
||||
as_of: date
|
||||
method: str
|
||||
symbol: str
|
||||
rank: int
|
||||
score: float
|
||||
selection_reason: list[str] = Field(default_factory=list)
|
||||
@@ -55,3 +55,14 @@ class SignalMeta(BaseModel):
|
||||
watch: int = 0
|
||||
sell: int = 0
|
||||
created_at: datetime | None = None
|
||||
|
||||
|
||||
class SignalHit(BaseModel):
|
||||
"""个股在信号历史中的命中(by-symbol 查询,供 Chart 标记)。"""
|
||||
|
||||
signal_id: str
|
||||
signal_date: date
|
||||
signal_type: str
|
||||
score: float | None = None
|
||||
price: float | None = None
|
||||
trigger_reason: list[str] = Field(default_factory=list)
|
||||
|
||||
@@ -10,6 +10,7 @@ from __future__ import annotations
|
||||
from datetime import date
|
||||
from typing import Protocol
|
||||
|
||||
from app.domain.entities.chart import SelectionHit
|
||||
from app.domain.entities.selection import SelectionMeta, SelectionResult
|
||||
|
||||
|
||||
@@ -27,3 +28,6 @@ class SelectionRepository(Protocol):
|
||||
limit: int = 20,
|
||||
) -> list[SelectionMeta]:
|
||||
"""历史选股元数据列表(按 created_at 倒序;可选 as_of/method 过滤)。"""
|
||||
|
||||
def list_by_symbol(self, symbol: str, limit: int = 50) -> list[SelectionHit]:
|
||||
"""该股在历史选股中的命中(Chart 标记用,含 as_of/rank/reason)。"""
|
||||
|
||||
@@ -5,7 +5,7 @@ from __future__ import annotations
|
||||
from datetime import date
|
||||
from typing import Protocol
|
||||
|
||||
from app.domain.entities.signal import SignalMeta, SignalResult
|
||||
from app.domain.entities.signal import SignalHit, SignalMeta, SignalResult
|
||||
|
||||
|
||||
class SignalRepository(Protocol):
|
||||
@@ -17,3 +17,6 @@ class SignalRepository(Protocol):
|
||||
def list_recent(
|
||||
self, as_of: date | None = None, limit: int = 20
|
||||
) -> list[SignalMeta]: ...
|
||||
|
||||
def list_by_symbol(self, symbol: str, limit: int = 50) -> list[SignalHit]:
|
||||
"""该股在历史信号中的命中(Chart 标记用,含 signal_id 溯源)。"""
|
||||
|
||||
Reference in New Issue
Block a user