feat(sector_radar): enhance sector radar functionality with active moneyflow and detailed metrics

- Introduced ActiveMoneyflowSource to fetch optional active-order flow, enhancing the sector radar's data capabilities.
- Updated StockFactRecord and DailyAggregateRecord to include pct_change and active_buy_net_amount_yuan for improved financial insights.
- Modified the build process to incorporate active moneyflow data without invalidating main rankings on failure.
- Enhanced the HTTP API to return detailed sector history and metrics, including pct_change and active buy metrics for members.
- Updated tests to validate the new functionality and ensure data integrity across various scenarios.
This commit is contained in:
yuxuanhui
2026-09-06 16:06:17 +08:00
parent 68282f5d46
commit 7f93d6b0f5
48 changed files with 4883 additions and 95 deletions
@@ -5,6 +5,9 @@ const requestJson = vi.hoisted(() => vi.fn())
vi.mock("@/shared/api/request-json", () => ({ requestJson }))
import {
getSectorRadarHistory,
getSectorRadarDetail,
parseRadarDetailResponse,
getSectorRadarDates,
getSectorRadarRankings,
getStockSectorMembership,
@@ -69,11 +72,106 @@ const rankingPayload = {
],
}
const historyMetric = {
rank_position: 1,
rank_percentile: "100",
pool_size: 10,
metric_value: "0.125",
missing: false,
in_top: true,
in_bottom: false,
}
const historyPayload = {
status: "success",
requested_trade_date: "2026-08-28",
trade_date: "2026-08-28",
sector_type: "concept",
sector_code: "BK01",
sector_name: "机器人",
publication,
window_size: 30,
available_days: 1,
points: [
{
trade_date: "2026-08-28",
publication_id: "publication-1",
swing: historyMetric,
ratio: historyMetric,
amount: historyMetric,
},
],
}
const detailPayload = {
...historyPayload,
pct_change: "-1.25",
leading_stock: { ts_code: "000001.SZ", name: null },
summary: {
swing: historyMetric,
ratio: historyMetric,
amount: historyMetric,
},
history: historyPayload,
members: [
{
ts_code: "000001.SZ",
name: "测试成员",
pct_change: "-1.5",
net_amount_yuan: "120000000",
active_buy_net_amount_yuan: null,
},
],
leaders: {
pct_change: { top: [], bottom: [] },
net_amount_yuan: { top: [], bottom: [] },
active_buy_net_amount_yuan: { top: [], bottom: [] },
},
similar_sectors: [],
}
describe("sector radar API adapters", () => {
beforeEach(() => {
requestJson.mockReset()
})
it("reads persisted sector resources and preserves nullable independent metrics", async () => {
const signal = new AbortController().signal
const query = {
sectorType: "concept" as const,
sectorCode: "BK01",
tradeDate: "2026-08-28",
}
requestJson.mockResolvedValue(historyPayload)
expect(
(await getSectorRadarHistory(query, signal)).points[0]?.ratio
.metric_value,
).toBe(0.125)
expect(requestJson).toHaveBeenLastCalledWith(
"/api/v1/sector-radar/sectors/concept/BK01/history?trade_date=2026-08-28",
{ signal },
)
requestJson.mockResolvedValue(detailPayload)
const detail = await getSectorRadarDetail(query, signal)
expect(requestJson).toHaveBeenLastCalledWith(
"/api/v1/sector-radar/sectors/concept/BK01/detail?trade_date=2026-08-28",
{ signal },
)
expect(detail.pct_change).toBe(-1.25)
expect(detail.leading_stock?.name).toBeNull()
expect(detail.members[0]).toMatchObject({
net_amount_yuan: 120000000,
active_buy_net_amount_yuan: null,
})
})
it.each(["NaN", "Infinity", "0x10", "", true])(
"rejects malformed detail Decimal %s",
(value) => {
expect(() =>
parseRadarDetailResponse({ ...detailPayload, pct_change: value }),
).toThrow("detail.pct_change")
},
)
it("forwards AbortSignal and normalizes decimal publication fields", async () => {
const signal = new AbortController().signal
requestJson.mockResolvedValue({
@@ -237,6 +335,8 @@ describe("stock sector membership adapter", () => {
await expect(
getStockSectorMembership("000001.SZ", "2026-08-28"),
).rejects.toThrow("membership.industries[0].name must be a non-empty string")
).rejects.toThrow(
"membership.industries[0].name must be a non-empty string",
)
})
})