feat(selection): add Zhixing line indicators to selection chart and update related components
This commit is contained in:
@@ -9,7 +9,7 @@ from typing import Literal, cast
|
||||
|
||||
import pandas as pd
|
||||
|
||||
from ..domain.indicators import compute_kdj
|
||||
from ..domain.indicators import compute_kdj, compute_zhixing_lines
|
||||
from ..domain.ports import MarketDataReader
|
||||
|
||||
SELECTION_CHART_LIMIT = 250
|
||||
@@ -21,7 +21,7 @@ class SelectionChartNotFound(LookupError):
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class SelectionChartPoint:
|
||||
"""One date-aligned OHLCV and KDJ point in the public chart vocabulary."""
|
||||
"""One date-aligned OHLCV, KDJ, and Zhixing-line point."""
|
||||
|
||||
trade_date: date
|
||||
open: float | None
|
||||
@@ -32,6 +32,8 @@ class SelectionChartPoint:
|
||||
k: float | None
|
||||
d: float | None
|
||||
j: float | None
|
||||
trend_white: float | None
|
||||
trend_yellow: float | None
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
@@ -46,7 +48,7 @@ class SelectionChart:
|
||||
|
||||
|
||||
class GetSelectionChart:
|
||||
"""Load one qfq history, compute KDJ, then bound the response series."""
|
||||
"""Load one qfq history, compute KDJ and Zhixing lines, then bound the series."""
|
||||
|
||||
def __init__(self, reader: MarketDataReader) -> None:
|
||||
"""Inject the market-data reader owned by the selection context."""
|
||||
@@ -54,7 +56,7 @@ class GetSelectionChart:
|
||||
self.reader = reader
|
||||
|
||||
def execute(self, ts_code: str, target_trade_date: date) -> SelectionChart:
|
||||
"""Return at most 250 points without changing KDJ's full-history state.
|
||||
"""Return at most 250 points without changing full-history indicator state.
|
||||
|
||||
Args:
|
||||
ts_code: Tushare stock identifier selected by the user.
|
||||
@@ -83,6 +85,7 @@ class GetSelectionChart:
|
||||
}
|
||||
)
|
||||
kdj = compute_kdj(frame)
|
||||
white, yellow = compute_zhixing_lines(frame["close"])
|
||||
start = max(0, len(bars) - SELECTION_CHART_LIMIT)
|
||||
points = tuple(
|
||||
SelectionChartPoint(
|
||||
@@ -95,6 +98,8 @@ class GetSelectionChart:
|
||||
k=_finite_or_none(kdj.iloc[index]["K"]),
|
||||
d=_finite_or_none(kdj.iloc[index]["D"]),
|
||||
j=_finite_or_none(kdj.iloc[index]["J"]),
|
||||
trend_white=_finite_or_none(white.iloc[index]),
|
||||
trend_yellow=_finite_or_none(yellow.iloc[index]),
|
||||
)
|
||||
for index, bar in enumerate(bars)
|
||||
if index >= start
|
||||
|
||||
@@ -147,7 +147,7 @@ class SelectionStockResponse(BaseModel):
|
||||
|
||||
|
||||
class SelectionChartPointResponse(BaseModel):
|
||||
"""One date-aligned qfq OHLCV and KDJ point."""
|
||||
"""One date-aligned qfq OHLCV, KDJ, and Zhixing-line point."""
|
||||
|
||||
trade_date: date
|
||||
open: float | None
|
||||
@@ -158,6 +158,8 @@ class SelectionChartPointResponse(BaseModel):
|
||||
k: float | None
|
||||
d: float | None
|
||||
j: float | None
|
||||
trend_white: float | None
|
||||
trend_yellow: float | None
|
||||
|
||||
|
||||
class SelectionChartResponse(BaseModel):
|
||||
@@ -268,7 +270,7 @@ def get_selection_chart(
|
||||
target_trade_date: date,
|
||||
service: Annotated[GetSelectionChart, Depends(get_selection_chart_service)],
|
||||
) -> SelectionChartResponse:
|
||||
"""Return one stock's bounded qfq OHLCV and KDJ history."""
|
||||
"""Return one stock's bounded qfq OHLCV, KDJ, and Zhixing-line history."""
|
||||
|
||||
try:
|
||||
chart = service.execute(ts_code, target_trade_date)
|
||||
@@ -482,6 +484,8 @@ def _chart_response(chart: SelectionChart) -> SelectionChartResponse:
|
||||
k=point.k,
|
||||
d=point.d,
|
||||
j=point.j,
|
||||
trend_white=point.trend_white,
|
||||
trend_yellow=point.trend_yellow,
|
||||
)
|
||||
for point in chart.points
|
||||
],
|
||||
|
||||
@@ -133,6 +133,8 @@ class FakeChartService:
|
||||
k=52.0,
|
||||
d=48.0,
|
||||
j=60.0,
|
||||
trend_white=10.2,
|
||||
trend_yellow=10.4,
|
||||
),
|
||||
),
|
||||
)
|
||||
@@ -451,6 +453,8 @@ def test_chart_returns_bounded_qfq_contract() -> None:
|
||||
"k": 52.0,
|
||||
"d": 48.0,
|
||||
"j": 60.0,
|
||||
"trend_white": 10.2,
|
||||
"trend_yellow": 10.4,
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
@@ -9,7 +9,10 @@ from zhixing_server.modules.selection.application.chart import (
|
||||
GetSelectionChart,
|
||||
SelectionChartNotFound,
|
||||
)
|
||||
from zhixing_server.modules.selection.domain.indicators import compute_kdj
|
||||
from zhixing_server.modules.selection.domain.indicators import (
|
||||
compute_kdj,
|
||||
compute_zhixing_lines,
|
||||
)
|
||||
from zhixing_server.modules.selection.domain.models import SelectionBar, StockHistory
|
||||
|
||||
|
||||
@@ -61,9 +64,12 @@ def test_chart_computes_on_full_history_before_returning_last_250_points() -> No
|
||||
}
|
||||
)
|
||||
expected = compute_kdj(frame).iloc[-1]
|
||||
expected_white, expected_yellow = compute_zhixing_lines(frame["close"])
|
||||
assert chart.points[-1].k == pytest.approx(float(expected["K"]))
|
||||
assert chart.points[-1].d == pytest.approx(float(expected["D"]))
|
||||
assert chart.points[-1].j == pytest.approx(float(expected["J"]))
|
||||
assert chart.points[-1].trend_white == pytest.approx(float(expected_white.iloc[-1]))
|
||||
assert chart.points[-1].trend_yellow == pytest.approx(float(expected_yellow.iloc[-1]))
|
||||
|
||||
|
||||
def test_chart_filters_future_rows_and_preserves_nullable_points() -> None:
|
||||
|
||||
Reference in New Issue
Block a user