fix(selection): render gold brick chart as TDX brick blocks with strong-red flag

Match the ZXB1 sub-chart formula: each brick spans between yesterday's
and today's brick value via a stacked range bar — rising bricks are
hollow red (solid on strong_red), falling bricks solid green. The chart
API now also returns brick_strong_red per point.
This commit is contained in:
yuxuanhui
2026-09-05 14:58:02 +08:00
parent 3b50e75ef8
commit 50cac3575a
8 changed files with 124 additions and 33 deletions
@@ -37,6 +37,7 @@ class SelectionChartPoint:
trend_white: float | None
trend_yellow: float | None
brick_chart: float | None = None
brick_strong_red: bool | None = None
@dataclass(frozen=True, slots=True)
@@ -95,7 +96,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
brick = self._compute_brick_indicators(ts_code, bars) if include_brick_chart else None
start = max(0, len(bars) - SELECTION_CHART_LIMIT)
points = tuple(
SelectionChartPoint(
@@ -111,7 +112,10 @@ class GetSelectionChart:
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])
None if brick is None else _finite_or_none(brick["brick_chart"].iloc[index])
),
brick_strong_red=(
None if brick is None else _strong_red_or_none(brick["strong_red"].iloc[index])
),
)
for index, bar in enumerate(bars)
@@ -125,11 +129,11 @@ class GetSelectionChart:
points=points,
)
def _compute_brick_chart(
def _compute_brick_indicators(
self,
ts_code: str,
bars: tuple[SelectionBar, ...],
) -> pd.Series:
) -> pd.DataFrame:
"""Run the gold-brick formula on full history for stable warmup values."""
frame = pd.DataFrame(
@@ -142,7 +146,16 @@ class GetSelectionChart:
"volume": [bar.volume for bar in bars],
}
)
return prepare_gold_brick_indicators(frame, ts_code)["brick_chart"]
prepared = prepare_gold_brick_indicators(frame, ts_code)
return prepared[["brick_chart", "strong_red"]]
def _strong_red_or_none(value: object) -> bool | None:
"""Convert one Pandas scalar to a JSON-safe brick flag or ``None``."""
if value is None or pd.isna(value):
return None
return bool(value)
def _finite_or_none(value: object) -> float | None:
@@ -167,6 +167,7 @@ class SelectionChartPointResponse(BaseModel):
trend_white: float | None
trend_yellow: float | None
brick_chart: float | None = None
brick_strong_red: bool | None = None
class SelectionChartResponse(BaseModel):
@@ -510,6 +511,7 @@ def _chart_response(chart: SelectionChart) -> SelectionChartResponse:
trend_white=point.trend_white,
trend_yellow=point.trend_yellow,
brick_chart=point.brick_chart,
brick_strong_red=point.brick_strong_red,
)
for point in chart.points
],
@@ -141,6 +141,7 @@ class FakeChartService:
trend_white=10.2,
trend_yellow=10.4,
brick_chart=5.0 if include_brick_chart else None,
brick_strong_red=True if include_brick_chart else None,
),
),
)
@@ -462,6 +463,7 @@ def test_chart_returns_bounded_qfq_contract() -> None:
"trend_white": 10.2,
"trend_yellow": 10.4,
"brick_chart": None,
"brick_strong_red": None,
}
],
}
@@ -478,6 +480,7 @@ def test_chart_requests_brick_series_for_gold_brick_strategy() -> None:
assert response.status_code == 200
assert chart_service.request == ("000001.SZ", TARGET, True)
assert response.json()["points"][0]["brick_chart"] == 5.0
assert response.json()["points"][0]["brick_strong_red"] is True
@pytest.mark.parametrize(
@@ -127,10 +127,15 @@ def test_chart_skips_brick_series_by_default_and_computes_on_request() -> None:
"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]))
prepared = prepare_gold_brick_indicators(frame, history.ts_code)
assert brick_chart.points[-1].brick_chart == pytest.approx(
float(prepared["brick_chart"].iloc[-1])
)
assert brick_chart.points[-1].brick_strong_red is bool(prepared["strong_red"].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]))
assert brick_chart.points[0].brick_chart == pytest.approx(
float(prepared["brick_chart"].iloc[10])
)
def test_chart_rejects_empty_history() -> None: