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
@@ -9,7 +9,9 @@ from typing import Literal, cast
import pandas as pd
from ..domain.gold_brick import prepare_gold_brick_indicators
from ..domain.indicators import compute_kdj, compute_zhixing_lines
from ..domain.models import SelectionBar
from ..domain.ports import MarketDataReader
SELECTION_CHART_LIMIT = 250
@@ -21,7 +23,7 @@ class SelectionChartNotFound(LookupError):
@dataclass(frozen=True, slots=True)
class SelectionChartPoint:
"""One date-aligned OHLCV, KDJ, and Zhixing-line point."""
"""One date-aligned OHLCV, KDJ, Zhixing-line, and gold-brick point."""
trade_date: date
open: float | None
@@ -34,6 +36,7 @@ class SelectionChartPoint:
j: float | None
trend_white: float | None
trend_yellow: float | None
brick_chart: float | None = None
@dataclass(frozen=True, slots=True)
@@ -55,12 +58,18 @@ class GetSelectionChart:
self.reader = reader
def execute(self, ts_code: str, target_trade_date: date) -> SelectionChart:
def execute(
self,
ts_code: str,
target_trade_date: date,
include_brick_chart: bool = False,
) -> SelectionChart:
"""Return at most 250 points without changing full-history indicator state.
Args:
ts_code: Tushare stock identifier selected by the user.
target_trade_date: Inclusive historical boundary for the chart.
include_brick_chart: Compute the gold-brick series for the request.
Returns:
An ascending qfq chart series aligned by trade date.
@@ -86,6 +95,7 @@ class GetSelectionChart:
)
kdj = compute_kdj(frame)
white, yellow = compute_zhixing_lines(frame["close"])
brick_chart = self._compute_brick_chart(ts_code, bars) if include_brick_chart else None
start = max(0, len(bars) - SELECTION_CHART_LIMIT)
points = tuple(
SelectionChartPoint(
@@ -100,6 +110,9 @@ class GetSelectionChart:
j=_finite_or_none(kdj.iloc[index]["J"]),
trend_white=_finite_or_none(white.iloc[index]),
trend_yellow=_finite_or_none(yellow.iloc[index]),
brick_chart=(
None if brick_chart is None else _finite_or_none(brick_chart.iloc[index])
),
)
for index, bar in enumerate(bars)
if index >= start
@@ -112,6 +125,25 @@ class GetSelectionChart:
points=points,
)
def _compute_brick_chart(
self,
ts_code: str,
bars: tuple[SelectionBar, ...],
) -> pd.Series:
"""Run the gold-brick formula on full history for stable warmup values."""
frame = pd.DataFrame(
{
"trade_date": [bar.trade_date for bar in bars],
"open": [bar.open for bar in bars],
"high": [bar.high for bar in bars],
"low": [bar.low for bar in bars],
"close": [bar.close for bar in bars],
"volume": [bar.volume for bar in bars],
}
)
return prepare_gold_brick_indicators(frame, ts_code)["brick_chart"]
def _finite_or_none(value: object) -> float | None:
"""Convert one Pandas scalar to a finite JSON-safe float or ``None``."""
@@ -153,7 +153,7 @@ class SelectionStockResponse(BaseModel):
class SelectionChartPointResponse(BaseModel):
"""One date-aligned qfq OHLCV, KDJ, and Zhixing-line point."""
"""One date-aligned qfq OHLCV, KDJ, Zhixing-line, and gold-brick point."""
trade_date: date
open: float | None
@@ -166,6 +166,7 @@ class SelectionChartPointResponse(BaseModel):
j: float | None
trend_white: float | None
trend_yellow: float | None
brick_chart: float | None = None
class SelectionChartResponse(BaseModel):
@@ -276,11 +277,16 @@ def get_selection_chart(
ts_code: str,
target_trade_date: date,
service: Annotated[GetSelectionChart, Depends(get_selection_chart_service)],
strategy: StrategyValue = "zhixing_b1",
) -> SelectionChartResponse:
"""Return one stock's bounded qfq OHLCV, KDJ, and Zhixing-line history."""
try:
chart = service.execute(ts_code, target_trade_date)
chart = service.execute(
ts_code,
target_trade_date,
include_brick_chart=strategy == "gold_brick",
)
except SelectionChartNotFound as exc:
raise _http_error(404, "chart_data_not_found", str(exc)) from exc
except MarketDataReaderError as exc:
@@ -503,6 +509,7 @@ def _chart_response(chart: SelectionChart) -> SelectionChartResponse:
j=point.j,
trend_white=point.trend_white,
trend_yellow=point.trend_yellow,
brick_chart=point.brick_chart,
)
for point in chart.points
],