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:
@@ -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",
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -9,6 +9,11 @@ import {
|
||||
radarViews,
|
||||
sectorTypes,
|
||||
type RadarDatesResponse,
|
||||
type RadarSectorQuery,
|
||||
type RadarHistoryResponse,
|
||||
type RadarHistoryMetric,
|
||||
type RadarDetailResponse,
|
||||
type RadarMember,
|
||||
type RadarMetricDefinition,
|
||||
type RadarPublication,
|
||||
type RadarRankingRow,
|
||||
@@ -176,7 +181,11 @@ export function parseStockSectorMembershipResponse(
|
||||
): StockSectorMembershipResponse {
|
||||
const record = readRecord(value, "membership")
|
||||
return {
|
||||
status: readEnum(record.status, ["success", "no_data"], "membership.status"),
|
||||
status: readEnum(
|
||||
record.status,
|
||||
["success", "no_data"],
|
||||
"membership.status",
|
||||
),
|
||||
ts_code: readNonEmptyString(record.ts_code, "membership.ts_code"),
|
||||
requested_trade_date: readDate(
|
||||
record.requested_trade_date,
|
||||
@@ -309,6 +318,33 @@ function readRankingRow(value: unknown, index: number): RadarRankingRow {
|
||||
record.metric_value,
|
||||
`${path}.metric_value`,
|
||||
),
|
||||
pct_change: readNullableFiniteNumber(
|
||||
record.pct_change ?? null,
|
||||
`${path}.pct_change`,
|
||||
),
|
||||
daily_net_amount_yuan: readNullableFiniteNumber(
|
||||
record.daily_net_amount_yuan ?? null,
|
||||
`${path}.daily_net_amount_yuan`,
|
||||
),
|
||||
daily_ratio: readNullableFiniteNumber(
|
||||
record.daily_ratio ?? null,
|
||||
`${path}.daily_ratio`,
|
||||
),
|
||||
on_list_count:
|
||||
record.on_list_count == null
|
||||
? null
|
||||
: readIntegerInRange(
|
||||
record.on_list_count,
|
||||
0,
|
||||
30,
|
||||
`${path}.on_list_count`,
|
||||
),
|
||||
history_available_days: readIntegerInRange(
|
||||
record.history_available_days ?? 0,
|
||||
0,
|
||||
30,
|
||||
`${path}.history_available_days`,
|
||||
),
|
||||
quality: readEnum(record.quality, radarMetricQualities, `${path}.quality`),
|
||||
member_count: readIntegerInRange(
|
||||
record.member_count,
|
||||
@@ -427,7 +463,8 @@ function readFiniteNumber(value: unknown, path: string): number {
|
||||
const number =
|
||||
typeof value === "number"
|
||||
? value
|
||||
: typeof value === "string" && value.trim().length > 0
|
||||
: typeof value === "string" &&
|
||||
/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/.test(value.trim())
|
||||
? Number(value)
|
||||
: Number.NaN
|
||||
if (!Number.isFinite(number)) throw contractError(path, "must be finite")
|
||||
@@ -483,3 +520,240 @@ function readNullablePositiveNumber(
|
||||
function contractError(path: string, reason: string): Error {
|
||||
return new Error(`Invalid sector radar response: ${path} ${reason}`)
|
||||
}
|
||||
|
||||
/** Fetch persisted history for a sector/date; forward cancellation and reject malformed decimals. */
|
||||
export async function getSectorRadarHistory(
|
||||
query: RadarSectorQuery,
|
||||
signal?: AbortSignal,
|
||||
) {
|
||||
const payload = await requestJson<unknown>(
|
||||
sectorResourceUrl(query, "history"),
|
||||
{ signal },
|
||||
)
|
||||
return parseRadarHistoryResponse(payload)
|
||||
}
|
||||
|
||||
/** Fetch persisted detail on demand; no supplier calls originate from the browser. */
|
||||
export async function getSectorRadarDetail(
|
||||
query: RadarSectorQuery,
|
||||
signal?: AbortSignal,
|
||||
) {
|
||||
const payload = await requestJson<unknown>(
|
||||
sectorResourceUrl(query, "detail"),
|
||||
{ signal },
|
||||
)
|
||||
return parseRadarDetailResponse(payload)
|
||||
}
|
||||
|
||||
function sectorResourceUrl(
|
||||
query: RadarSectorQuery,
|
||||
resource: "history" | "detail",
|
||||
) {
|
||||
const params = new URLSearchParams({ trade_date: query.tradeDate })
|
||||
return `/api/v1/sector-radar/sectors/${query.sectorType}/${encodeURIComponent(query.sectorCode)}/${resource}?${params}`
|
||||
}
|
||||
|
||||
function readSectorIdentity(record: JsonRecord, path: string) {
|
||||
return {
|
||||
status: readEnum(record.status, ["success", "no_data"], `${path}.status`),
|
||||
requested_trade_date: readNullableDate(
|
||||
record.requested_trade_date,
|
||||
`${path}.requested_trade_date`,
|
||||
),
|
||||
trade_date: readNullableDate(record.trade_date, `${path}.trade_date`),
|
||||
publication: readNullablePublication(
|
||||
record.publication,
|
||||
`${path}.publication`,
|
||||
),
|
||||
sector_type: readEnum(
|
||||
record.sector_type,
|
||||
sectorTypes,
|
||||
`${path}.sector_type`,
|
||||
),
|
||||
sector_code: readNonEmptyString(record.sector_code, `${path}.sector_code`),
|
||||
sector_name: readNullableString(record.sector_name, `${path}.sector_name`),
|
||||
}
|
||||
}
|
||||
|
||||
/** Normalize the finite Decimal fields while preserving missing ranks as null. */
|
||||
export function parseRadarHistoryResponse(
|
||||
value: unknown,
|
||||
): RadarHistoryResponse {
|
||||
const record = readRecord(value, "history")
|
||||
return {
|
||||
...readSectorIdentity(record, "history"),
|
||||
window_size: readIntegerInRange(
|
||||
record.window_size,
|
||||
1,
|
||||
30,
|
||||
"history.window_size",
|
||||
),
|
||||
available_days: readIntegerInRange(
|
||||
record.available_days,
|
||||
0,
|
||||
30,
|
||||
"history.available_days",
|
||||
),
|
||||
points: readArray(record.points, "history.points").map((value, index) => {
|
||||
const path = `history.points[${index}]`
|
||||
const point = readRecord(value, path)
|
||||
return {
|
||||
trade_date: readDate(point.trade_date, `${path}.trade_date`),
|
||||
publication_id: readNullableString(
|
||||
point.publication_id,
|
||||
`${path}.publication_id`,
|
||||
),
|
||||
...readMetricSummary(point, path),
|
||||
}
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
function readMetricSummary(record: JsonRecord, path: string) {
|
||||
return {
|
||||
amount: readHistoryMetric(record.amount, `${path}.amount`),
|
||||
ratio: readHistoryMetric(record.ratio, `${path}.ratio`),
|
||||
swing: readHistoryMetric(record.swing, `${path}.swing`),
|
||||
}
|
||||
}
|
||||
|
||||
function readHistoryMetric(value: unknown, path: string): RadarHistoryMetric {
|
||||
const record = readRecord(value, path)
|
||||
return {
|
||||
rank_position: readNullableInteger(
|
||||
record.rank_position,
|
||||
1,
|
||||
`${path}.rank_position`,
|
||||
),
|
||||
rank_percentile: readNullablePositiveNumber(
|
||||
record.rank_percentile,
|
||||
100,
|
||||
`${path}.rank_percentile`,
|
||||
),
|
||||
pool_size: readIntegerInRange(
|
||||
record.pool_size,
|
||||
0,
|
||||
Number.MAX_SAFE_INTEGER,
|
||||
`${path}.pool_size`,
|
||||
),
|
||||
metric_value: readNullableFiniteNumber(
|
||||
record.metric_value,
|
||||
`${path}.metric_value`,
|
||||
),
|
||||
missing: readBoolean(record.missing, `${path}.missing`),
|
||||
in_top: readBoolean(record.in_top, `${path}.in_top`),
|
||||
in_bottom: readBoolean(record.in_bottom, `${path}.in_bottom`),
|
||||
}
|
||||
}
|
||||
|
||||
function readBoolean(value: unknown, path: string): boolean {
|
||||
if (typeof value !== "boolean") throw contractError(path, "must be a boolean")
|
||||
return value
|
||||
}
|
||||
|
||||
function readMembers(value: unknown, path: string): RadarMember[] {
|
||||
return readArray(value, path).map((value, index) => {
|
||||
const itemPath = `${path}[${index}]`
|
||||
const record = readRecord(value, itemPath)
|
||||
return {
|
||||
ts_code: readNonEmptyString(record.ts_code, `${itemPath}.ts_code`),
|
||||
name: readNonEmptyString(record.name, `${itemPath}.name`),
|
||||
pct_change: readNullableFiniteNumber(
|
||||
record.pct_change,
|
||||
`${itemPath}.pct_change`,
|
||||
),
|
||||
net_amount_yuan: readNullableFiniteNumber(
|
||||
record.net_amount_yuan,
|
||||
`${itemPath}.net_amount_yuan`,
|
||||
),
|
||||
active_buy_net_amount_yuan: readNullableFiniteNumber(
|
||||
record.active_buy_net_amount_yuan,
|
||||
`${itemPath}.active_buy_net_amount_yuan`,
|
||||
),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/** Validate the full detail payload, including independent member metrics and overlaps. */
|
||||
export function parseRadarDetailResponse(value: unknown): RadarDetailResponse {
|
||||
const record = readRecord(value, "detail")
|
||||
const leading =
|
||||
record.leading_stock === null
|
||||
? null
|
||||
: readRecord(record.leading_stock, "detail.leading_stock")
|
||||
const leaders = readRecord(record.leaders, "detail.leaders")
|
||||
function readLeaders(key: string) {
|
||||
const path = `detail.leaders.${key}`
|
||||
const group = readRecord(leaders[key], path)
|
||||
return {
|
||||
top: readMembers(group.top, `${path}.top`),
|
||||
bottom: readMembers(group.bottom, `${path}.bottom`),
|
||||
}
|
||||
}
|
||||
return {
|
||||
...readSectorIdentity(record, "detail"),
|
||||
pct_change: readNullableFiniteNumber(
|
||||
record.pct_change,
|
||||
"detail.pct_change",
|
||||
),
|
||||
leading_stock:
|
||||
leading === null
|
||||
? null
|
||||
: {
|
||||
ts_code: readNonEmptyString(
|
||||
leading.ts_code,
|
||||
"detail.leading_stock.ts_code",
|
||||
),
|
||||
name: readNullableString(leading.name, "detail.leading_stock.name"),
|
||||
},
|
||||
summary: readMetricSummary(
|
||||
readRecord(record.summary, "detail.summary"),
|
||||
"detail.summary",
|
||||
),
|
||||
history: parseRadarHistoryResponse(record.history),
|
||||
members: readMembers(record.members, "detail.members"),
|
||||
leaders: {
|
||||
pct_change: readLeaders("pct_change"),
|
||||
net_amount_yuan: readLeaders("net_amount_yuan"),
|
||||
active_buy_net_amount_yuan: readLeaders("active_buy_net_amount_yuan"),
|
||||
},
|
||||
similar_sectors: readArray(
|
||||
record.similar_sectors,
|
||||
"detail.similar_sectors",
|
||||
).map((value, index) => {
|
||||
const path = `detail.similar_sectors[${index}]`
|
||||
const sector = readRecord(value, path)
|
||||
return {
|
||||
sector_type: readEnum(
|
||||
sector.sector_type,
|
||||
sectorTypes,
|
||||
`${path}.sector_type`,
|
||||
),
|
||||
sector_code: readNonEmptyString(
|
||||
sector.sector_code,
|
||||
`${path}.sector_code`,
|
||||
),
|
||||
sector_name: readNonEmptyString(
|
||||
sector.sector_name,
|
||||
`${path}.sector_name`,
|
||||
),
|
||||
overlap_ratio: readFraction(
|
||||
sector.overlap_ratio,
|
||||
`${path}.overlap_ratio`,
|
||||
),
|
||||
intersection_count: readIntegerInRange(
|
||||
sector.intersection_count,
|
||||
0,
|
||||
Number.MAX_SAFE_INTEGER,
|
||||
`${path}.intersection_count`,
|
||||
),
|
||||
union_count: readIntegerInRange(
|
||||
sector.union_count,
|
||||
1,
|
||||
Number.MAX_SAFE_INTEGER,
|
||||
`${path}.union_count`,
|
||||
),
|
||||
}
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ import { beforeEach, describe, expect, it, vi } from "vitest"
|
||||
const useQuery = vi.hoisted(() => vi.fn())
|
||||
const useInfiniteQuery = vi.hoisted(() => vi.fn())
|
||||
const api = vi.hoisted(() => ({
|
||||
getSectorRadarHistory: vi.fn(),
|
||||
getSectorRadarDetail: vi.fn(),
|
||||
getSectorRadarDates: vi.fn(),
|
||||
getSectorRadarRankings: vi.fn(),
|
||||
}))
|
||||
@@ -11,6 +13,9 @@ vi.mock("@tanstack/react-query", () => ({ useInfiniteQuery, useQuery }))
|
||||
vi.mock("./sector-radar.api", () => api)
|
||||
|
||||
import {
|
||||
sectorRadarSectorQueryKey,
|
||||
useSectorRadarDetail,
|
||||
useSectorRadarHistory,
|
||||
sectorRadarDatesQueryKey,
|
||||
sectorRadarRankingsQueryKey,
|
||||
useSectorRadarDates,
|
||||
@@ -36,6 +41,42 @@ describe("sector radar query hooks", () => {
|
||||
useInfiniteQuery.mockImplementation((options) => options)
|
||||
})
|
||||
|
||||
it("isolates sector resources by type, code and date and forwards cancellation", () => {
|
||||
const sector = {
|
||||
sectorType: "concept" as const,
|
||||
sectorCode: "BK01",
|
||||
tradeDate: "2026-08-28",
|
||||
}
|
||||
const signal = new AbortController().signal
|
||||
useSectorRadarDetail(sector)
|
||||
const options = useQuery.mock.calls.at(-1)?.[0]
|
||||
expect(options.queryKey).toEqual([
|
||||
"sectorRadar",
|
||||
"detail",
|
||||
"concept",
|
||||
"BK01",
|
||||
"2026-08-28",
|
||||
])
|
||||
expect(options.placeholderData).toBeUndefined()
|
||||
options.queryFn({ signal })
|
||||
expect(api.getSectorRadarDetail).toHaveBeenCalledWith(sector, signal)
|
||||
expect(
|
||||
sectorRadarSectorQueryKey("detail", {
|
||||
...sector,
|
||||
tradeDate: "2026-08-31",
|
||||
}),
|
||||
).not.toEqual(options.queryKey)
|
||||
expect(
|
||||
sectorRadarSectorQueryKey("detail", {
|
||||
...sector,
|
||||
sectorType: "industry",
|
||||
}),
|
||||
).not.toEqual(options.queryKey)
|
||||
useSectorRadarHistory(sector)
|
||||
useQuery.mock.calls.at(-1)?.[0].queryFn({ signal })
|
||||
expect(api.getSectorRadarHistory).toHaveBeenCalledWith(sector, signal)
|
||||
})
|
||||
|
||||
it("keeps filters in one ranking key without splitting cached pages", () => {
|
||||
expect(sectorRadarDatesQueryKey).toEqual(["sectorRadar", "dates"])
|
||||
expect(sectorRadarRankingsQueryKey(query)).toEqual([
|
||||
|
||||
@@ -4,10 +4,17 @@ import {
|
||||
type InfiniteData,
|
||||
} from "@tanstack/react-query"
|
||||
|
||||
import { getSectorRadarDates, getSectorRadarRankings, getStockSectorMembership } from "./sector-radar.api"
|
||||
import {
|
||||
getSectorRadarDates,
|
||||
getSectorRadarRankings,
|
||||
getStockSectorMembership,
|
||||
getSectorRadarHistory,
|
||||
getSectorRadarDetail,
|
||||
} from "./sector-radar.api"
|
||||
import type {
|
||||
RadarRankingsQuery,
|
||||
RadarRankingsResponse,
|
||||
RadarSectorQuery,
|
||||
} from "./sector-radar.types"
|
||||
|
||||
export const sectorRadarDatesQueryKey = ["sectorRadar", "dates"] as const
|
||||
@@ -92,3 +99,31 @@ export function useSectorRadarRankings(query: RadarRankingsQuery) {
|
||||
queryKey: sectorRadarRankingsQueryKey(query),
|
||||
})
|
||||
}
|
||||
|
||||
export const sectorRadarSectorQueryKey = (
|
||||
resource: "history" | "detail",
|
||||
query: RadarSectorQuery,
|
||||
) =>
|
||||
[
|
||||
"sectorRadar",
|
||||
resource,
|
||||
query.sectorType,
|
||||
query.sectorCode,
|
||||
query.tradeDate,
|
||||
] as const
|
||||
|
||||
/** Keep on-demand history isolated by sector type, code and requested date. */
|
||||
export function useSectorRadarHistory(query: RadarSectorQuery) {
|
||||
return useQuery({
|
||||
queryKey: sectorRadarSectorQueryKey("history", query),
|
||||
queryFn: ({ signal }) => getSectorRadarHistory(query, signal),
|
||||
})
|
||||
}
|
||||
|
||||
/** Mount only while the detail dialog is open; never reuse another date as placeholder data. */
|
||||
export function useSectorRadarDetail(query: RadarSectorQuery) {
|
||||
return useQuery({
|
||||
queryKey: sectorRadarSectorQueryKey("detail", query),
|
||||
queryFn: ({ signal }) => getSectorRadarDetail(query, signal),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -77,6 +77,11 @@ export interface RadarRankingRow {
|
||||
rank_percentile: number | null
|
||||
rank_change_days: number
|
||||
rank_change: number | null
|
||||
pct_change: number | null
|
||||
daily_net_amount_yuan: number | null
|
||||
daily_ratio: number | null
|
||||
on_list_count: number | null
|
||||
history_available_days: number
|
||||
}
|
||||
|
||||
export interface RadarRankingsResponse {
|
||||
@@ -127,3 +132,68 @@ export interface StockSectorMembershipResponse {
|
||||
concept_total: number
|
||||
concept_limit: number
|
||||
}
|
||||
|
||||
export interface RadarSectorQuery {
|
||||
sectorType: SectorType
|
||||
sectorCode: string
|
||||
tradeDate: string
|
||||
}
|
||||
|
||||
export interface RadarHistoryMetric {
|
||||
rank_position: number | null
|
||||
rank_percentile: number | null
|
||||
pool_size: number
|
||||
metric_value: number | null
|
||||
missing: boolean
|
||||
in_top: boolean
|
||||
in_bottom: boolean
|
||||
}
|
||||
|
||||
export type RadarMetricSummary = Record<RadarMetricKind, RadarHistoryMetric>
|
||||
export interface RadarHistoryPoint extends RadarMetricSummary {
|
||||
trade_date: string
|
||||
publication_id: string | null
|
||||
}
|
||||
export interface RadarHistoryResponse {
|
||||
status: "success" | "no_data"
|
||||
requested_trade_date: string | null
|
||||
trade_date: string | null
|
||||
publication: RadarPublication | null
|
||||
sector_type: SectorType
|
||||
sector_code: string
|
||||
sector_name: string | null
|
||||
points: RadarHistoryPoint[]
|
||||
window_size: number
|
||||
available_days: number
|
||||
}
|
||||
export interface RadarMember {
|
||||
ts_code: string
|
||||
name: string
|
||||
pct_change: number | null
|
||||
net_amount_yuan: number | null
|
||||
active_buy_net_amount_yuan: number | null
|
||||
}
|
||||
export type RadarMemberMetric =
|
||||
"pct_change" | "net_amount_yuan" | "active_buy_net_amount_yuan"
|
||||
export interface RadarDetailResponse extends Omit<
|
||||
RadarHistoryResponse,
|
||||
"points" | "window_size" | "available_days"
|
||||
> {
|
||||
pct_change: number | null
|
||||
leading_stock: { ts_code: string; name: string | null } | null
|
||||
summary: RadarMetricSummary
|
||||
history: RadarHistoryResponse
|
||||
members: RadarMember[]
|
||||
leaders: Record<
|
||||
RadarMemberMetric,
|
||||
{ top: RadarMember[]; bottom: RadarMember[] }
|
||||
>
|
||||
similar_sectors: {
|
||||
sector_type: SectorType
|
||||
sector_code: string
|
||||
sector_name: string
|
||||
overlap_ratio: number
|
||||
intersection_count: number
|
||||
union_count: number
|
||||
}[]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user