feat(selection): 新增个股技术图表接口

This commit is contained in:
yuxuanhui
2026-09-01 13:37:11 +08:00
parent c8b08add76
commit 9145fe4b2c
4 changed files with 438 additions and 12 deletions
@@ -9,6 +9,11 @@ from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, Query, s
from pydantic import BaseModel, Field
from zhixing_server.bootstrap.config import Settings, get_settings
from zhixing_server.modules.selection.application.chart import (
GetSelectionChart,
SelectionChart,
SelectionChartNotFound,
)
from zhixing_server.modules.selection.application.run import (
RunZhixingB1,
)
@@ -17,6 +22,7 @@ from zhixing_server.modules.selection.domain.pattern_scoring import (
PatternScore,
ZhixingB1PatternScorer,
)
from zhixing_server.modules.selection.domain.ports import MarketDataReaderError
from zhixing_server.modules.selection.domain.runs import (
SelectionRerunRequired,
SelectionResultQuery,
@@ -140,6 +146,30 @@ class SelectionStockResponse(BaseModel):
signals: list[SelectionSignalResponse] = Field(default_factory=_empty_signals)
class SelectionChartPointResponse(BaseModel):
"""One date-aligned qfq OHLCV and KDJ point."""
trade_date: date
open: float | None
high: float | None
low: float | None
close: float | None
volume: float | None
k: float | None
d: float | None
j: float | None
class SelectionChartResponse(BaseModel):
"""A bounded ascending chart series for one selected stock."""
ts_code: str
name: str
target_trade_date: date
source_adj: Literal["qfq"]
points: list[SelectionChartPointResponse]
def _empty_stocks() -> list[SelectionStockResponse]:
"""Create a typed default stock-result list."""
@@ -192,6 +222,15 @@ def get_selection_service(
)
def get_selection_chart_service(
settings: Annotated[Settings, Depends(get_settings)],
) -> GetSelectionChart:
"""Build the read-only chart use case on the shared selection pool."""
pool = get_selection_postgres_pool(settings)
return GetSelectionChart(PostgresMarketDataReader(settings, pool=pool))
def get_selection_postgres_pool(settings: Settings) -> SelectionPostgresPool:
"""Return the cached bounded pool shared by selection adapters."""
@@ -220,6 +259,26 @@ def _close_cached_selection_pools() -> None:
atexit.register(_close_cached_selection_pools)
@selection_router.get(
"/stocks/{ts_code}/chart",
response_model=SelectionChartResponse,
)
def get_selection_chart(
ts_code: str,
target_trade_date: date,
service: Annotated[GetSelectionChart, Depends(get_selection_chart_service)],
) -> SelectionChartResponse:
"""Return one stock's bounded qfq OHLCV and KDJ history."""
try:
chart = service.execute(ts_code, target_trade_date)
except SelectionChartNotFound as exc:
raise _http_error(404, "chart_data_not_found", str(exc)) from exc
except MarketDataReaderError as exc:
raise _http_error(503, "selection_storage_unavailable", str(exc)) from exc
return _chart_response(chart)
@selection_router.post(
"/runs",
response_model=SelectionRunAcceptedResponse,
@@ -387,13 +446,6 @@ def _pattern_score_response(score: PatternScore) -> SelectionPatternScoreRespons
return None
if score.status == "failed":
return SelectionPatternScoreResponse(status="failed", reason=score.reason)
if score.status == "below_threshold":
return SelectionPatternScoreResponse(
status="below_threshold",
threshold=score.threshold,
version=score.version,
reason="未匹配到评分阈值以上案例",
)
if score.case is None or score.breakdown is None:
return SelectionPatternScoreResponse(status="failed", reason="评分结果不完整")
return SelectionPatternScoreResponse(
@@ -411,6 +463,31 @@ def _pattern_score_response(score: PatternScore) -> SelectionPatternScoreRespons
)
def _chart_response(chart: SelectionChart) -> SelectionChartResponse:
"""Translate an application chart without leaking domain dataclasses."""
return SelectionChartResponse(
ts_code=chart.ts_code,
name=chart.name,
target_trade_date=chart.target_trade_date,
source_adj=chart.source_adj,
points=[
SelectionChartPointResponse(
trade_date=point.trade_date,
open=point.open,
high=point.high,
low=point.low,
close=point.close,
volume=point.volume,
k=point.k,
d=point.d,
j=point.j,
)
for point in chart.points
],
)
def _result_query(
page: int,
page_size: int,
@@ -440,10 +517,12 @@ def _http_error(code: int, error_type: str, message: str) -> HTTPException:
__all__ = [
"SelectionChartResponse",
"SelectionResultsResponse",
"SelectionRunAcceptedResponse",
"SelectionRunRequest",
"SelectionStockResponse",
"get_selection_chart_service",
"get_selection_service",
"get_selection_postgres_pool",
"selection_router",