feat(selection): show gold brick chart below volume in stock detail panel

Add an optional brick_chart series to the selection chart API, computed
from the shared gold-brick formula when strategy=gold_brick, and render
a fourth grid with red/green brick bars between the volume and J grids.
This commit is contained in:
yuxuanhui
2026-09-05 13:41:25 +08:00
parent 79476252b1
commit 3b50e75ef8
13 changed files with 249 additions and 36 deletions
+24 -4
View File
@@ -109,10 +109,15 @@ class FakeChartService:
def __init__(self) -> None:
self.mode = "ok"
self.request: tuple[str, date] | None = None
self.request: tuple[str, date, bool] | None = None
def execute(self, ts_code: str, target_trade_date: date) -> SelectionChart:
self.request = (ts_code, target_trade_date)
def execute(
self,
ts_code: str,
target_trade_date: date,
include_brick_chart: bool = False,
) -> SelectionChart:
self.request = (ts_code, target_trade_date, include_brick_chart)
if self.mode == "missing":
raise SelectionChartNotFound("chart unavailable")
if self.mode == "storage_error":
@@ -135,6 +140,7 @@ class FakeChartService:
j=60.0,
trend_white=10.2,
trend_yellow=10.4,
brick_chart=5.0 if include_brick_chart else None,
),
),
)
@@ -436,7 +442,7 @@ def test_chart_returns_bounded_qfq_contract() -> None:
)
assert response.status_code == 200
assert chart_service.request == ("000001.SZ", TARGET)
assert chart_service.request == ("000001.SZ", TARGET, False)
assert response.json() == {
"ts_code": "000001.SZ",
"name": "平安银行",
@@ -455,11 +461,25 @@ def test_chart_returns_bounded_qfq_contract() -> None:
"j": 60.0,
"trend_white": 10.2,
"trend_yellow": 10.4,
"brick_chart": None,
}
],
}
def test_chart_requests_brick_series_for_gold_brick_strategy() -> None:
chart_service = FakeChartService()
response = _client(FakeSelectionService(), chart_service).get(
"/api/v1/selection/stocks/000001.SZ/chart",
params={"target_trade_date": "2026-08-08", "strategy": "gold_brick"},
)
assert response.status_code == 200
assert chart_service.request == ("000001.SZ", TARGET, True)
assert response.json()["points"][0]["brick_chart"] == 5.0
@pytest.mark.parametrize(
("mode", "status_code", "error_code"),
[
@@ -9,6 +9,9 @@ from zhixing_server.modules.selection.application.chart import (
GetSelectionChart,
SelectionChartNotFound,
)
from zhixing_server.modules.selection.domain.gold_brick import (
prepare_gold_brick_indicators,
)
from zhixing_server.modules.selection.domain.indicators import (
compute_kdj,
compute_zhixing_lines,
@@ -100,6 +103,36 @@ def test_chart_filters_future_rows_and_preserves_nullable_points() -> None:
assert chart.points[3].j is None
def test_chart_skips_brick_series_by_default_and_computes_on_request() -> None:
history = _history(260)
reader = FakeReader(history)
target = history.bars[-1].trade_date
default_chart = GetSelectionChart(reader).execute(history.ts_code, target)
assert all(point.brick_chart is None for point in default_chart.points)
brick_chart = GetSelectionChart(reader).execute(
history.ts_code,
target,
include_brick_chart=True,
)
frame = pd.DataFrame(
{
"trade_date": [bar.trade_date for bar in history.bars],
"open": [bar.open for bar in history.bars],
"high": [bar.high for bar in history.bars],
"low": [bar.low for bar in history.bars],
"close": [bar.close for bar in history.bars],
"volume": [bar.volume for bar in history.bars],
}
)
expected = prepare_gold_brick_indicators(frame, history.ts_code)["brick_chart"]
assert brick_chart.points[-1].brick_chart == pytest.approx(float(expected.iloc[-1]))
assert brick_chart.points[0].trade_date == history.bars[10].trade_date
assert brick_chart.points[0].brick_chart == pytest.approx(float(expected.iloc[10]))
def test_chart_rejects_empty_history() -> None:
reader = FakeReader(StockHistory(ts_code="000001.SZ", name="平安银行"))