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
+124 -5
View File
@@ -10,6 +10,11 @@ from fastapi.testclient import TestClient
import zhixing_server.modules.selection.presentation.http as selection_http
from zhixing_server.bootstrap.app import create_app
from zhixing_server.bootstrap.config import Settings
from zhixing_server.modules.selection.application.chart import (
SelectionChart,
SelectionChartNotFound,
SelectionChartPoint,
)
from zhixing_server.modules.selection.application.run import PreparedSelectionRun
from zhixing_server.modules.selection.domain.models import SelectionSignal, ZhixingB1Category
from zhixing_server.modules.selection.domain.pattern_scoring import (
@@ -18,6 +23,7 @@ from zhixing_server.modules.selection.domain.pattern_scoring import (
PatternScore,
PatternScoreBreakdown,
)
from zhixing_server.modules.selection.domain.ports import MarketDataReaderError
from zhixing_server.modules.selection.domain.runs import (
SelectionExecutionSource,
SelectionRerunRequired,
@@ -30,6 +36,7 @@ from zhixing_server.modules.selection.infrastructure.postgres_reader import (
SelectionMarketDataNotReady,
)
from zhixing_server.modules.selection.presentation.http import (
get_selection_chart_service,
get_selection_postgres_pool,
get_selection_service,
)
@@ -97,6 +104,40 @@ class FakeSelectionService:
return self.run
class FakeChartService:
"""Return or fail one deterministic chart response."""
def __init__(self) -> None:
self.mode = "ok"
self.request: tuple[str, date] | None = None
def execute(self, ts_code: str, target_trade_date: date) -> SelectionChart:
self.request = (ts_code, target_trade_date)
if self.mode == "missing":
raise SelectionChartNotFound("chart unavailable")
if self.mode == "storage_error":
raise MarketDataReaderError("chart storage unavailable")
return SelectionChart(
ts_code=ts_code,
name="平安银行",
target_trade_date=target_trade_date,
source_adj="qfq",
points=(
SelectionChartPoint(
trade_date=target_trade_date,
open=10.0,
high=11.0,
low=9.5,
close=10.5,
volume=1000.0,
k=52.0,
d=48.0,
j=60.0,
),
),
)
def _run(run_id: str, status: str) -> SelectionRun:
original_signal = SelectionSignal(
ts_code="000001.SZ",
@@ -153,9 +194,14 @@ def _run(run_id: str, status: str) -> SelectionRun:
)
def _client(service: FakeSelectionService) -> TestClient:
def _client(
service: FakeSelectionService,
chart_service: FakeChartService | None = None,
) -> TestClient:
app = create_app()
app.dependency_overrides[get_selection_service] = lambda: service
if chart_service is not None:
app.dependency_overrides[get_selection_chart_service] = lambda: chart_service
return TestClient(app)
@@ -295,12 +341,21 @@ def test_query_returns_persisted_signal_details() -> None:
),
{
"status": "below_threshold",
"value": None,
"value": 42.5,
"threshold": 60.0,
"version": PATTERN_SCORING_VERSION,
"case": None,
"breakdown": None,
"reason": "未匹配到评分阈值以上案例",
"case": {
"id": "case_001",
"name": "华纳药厂",
"breakout_date": "2025-05-12",
},
"breakdown": {
"trend_structure": 40.0,
"kdj_state": 42.0,
"volume_pattern": 43.0,
"price_shape": 44.0,
},
"reason": None,
},
),
(
@@ -370,6 +425,70 @@ def test_query_rejects_invalid_page_size() -> None:
assert response.status_code == 422
def test_chart_returns_bounded_qfq_contract() -> None:
chart_service = FakeChartService()
response = _client(FakeSelectionService(), chart_service).get(
"/api/v1/selection/stocks/000001.SZ/chart",
params={"target_trade_date": "2026-08-08"},
)
assert response.status_code == 200
assert chart_service.request == ("000001.SZ", TARGET)
assert response.json() == {
"ts_code": "000001.SZ",
"name": "平安银行",
"target_trade_date": "2026-08-08",
"source_adj": "qfq",
"points": [
{
"trade_date": "2026-08-08",
"open": 10.0,
"high": 11.0,
"low": 9.5,
"close": 10.5,
"volume": 1000.0,
"k": 52.0,
"d": 48.0,
"j": 60.0,
}
],
}
@pytest.mark.parametrize(
("mode", "status_code", "error_code"),
[
("missing", 404, "chart_data_not_found"),
("storage_error", 503, "selection_storage_unavailable"),
],
)
def test_chart_maps_application_errors(
mode: str,
status_code: int,
error_code: str,
) -> None:
chart_service = FakeChartService()
chart_service.mode = mode
response = _client(FakeSelectionService(), chart_service).get(
"/api/v1/selection/stocks/000001.SZ/chart",
params={"target_trade_date": "2026-08-08"},
)
assert response.status_code == status_code
assert response.json()["detail"]["code"] == error_code
def test_chart_requires_a_valid_target_date() -> None:
response = _client(FakeSelectionService(), FakeChartService()).get(
"/api/v1/selection/stocks/000001.SZ/chart",
params={"target_trade_date": "not-a-date"},
)
assert response.status_code == 422
def test_query_forwards_score_ascending_sort() -> None:
service = FakeSelectionService(_run("run-http", "success"))