feat(selection): show stock industry and concept boards in detail panel
Add point-in-time stock membership lookup to the sector radar module
(GET /sector-radar/stocks/{ts_code}/membership) reading the existing
dc_index/dc_member snapshots, and surface industries plus concept chips
in the selection signal detail panel with graceful no-data hiding.
This commit is contained in:
@@ -14,6 +14,8 @@ import {
|
||||
type RadarRankingRow,
|
||||
type RadarRankingsQuery,
|
||||
type RadarRankingsResponse,
|
||||
type StockSectorMembershipResponse,
|
||||
type StockSectorRef,
|
||||
} from "./sector-radar.types"
|
||||
|
||||
type JsonRecord = Record<string, unknown>
|
||||
@@ -145,6 +147,69 @@ export function parseRadarRankingsResponse(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch one stock's industries and concepts from the last-good build snapshot.
|
||||
*
|
||||
* @param tsCode - Stock code such as `000001.SZ`.
|
||||
* @param tradeDate - Requested trade date; the backend answers with the
|
||||
* newest snapshot at or before it and reports that exact date.
|
||||
* @param signal - React Query cancellation signal forwarded to fetch.
|
||||
* @returns A validated membership response with grouped sector references.
|
||||
* @throws Error when the server response violates the stable HTTP contract.
|
||||
*/
|
||||
export async function getStockSectorMembership(
|
||||
tsCode: string,
|
||||
tradeDate: string,
|
||||
signal?: AbortSignal,
|
||||
) {
|
||||
const params = new URLSearchParams({ trade_date: tradeDate })
|
||||
const payload = await requestJson<unknown>(
|
||||
`/api/v1/sector-radar/stocks/${encodeURIComponent(tsCode)}/membership?${params.toString()}`,
|
||||
{ signal },
|
||||
)
|
||||
return parseStockSectorMembershipResponse(payload)
|
||||
}
|
||||
|
||||
/** Validate one stock-membership response at the feature boundary. */
|
||||
export function parseStockSectorMembershipResponse(
|
||||
value: unknown,
|
||||
): StockSectorMembershipResponse {
|
||||
const record = readRecord(value, "membership")
|
||||
return {
|
||||
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,
|
||||
"membership.requested_trade_date",
|
||||
),
|
||||
trade_date: readNullableDate(record.trade_date, "membership.trade_date"),
|
||||
industries: readSectorRefs(record.industries, "membership.industries"),
|
||||
concepts: readSectorRefs(record.concepts, "membership.concepts"),
|
||||
concept_total: readIntegerInRange(
|
||||
record.concept_total,
|
||||
0,
|
||||
Number.MAX_SAFE_INTEGER,
|
||||
"membership.concept_total",
|
||||
),
|
||||
concept_limit: readIntegerInRange(
|
||||
record.concept_limit,
|
||||
1,
|
||||
100,
|
||||
"membership.concept_limit",
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
function readSectorRefs(value: unknown, path: string): StockSectorRef[] {
|
||||
return readArray(value, path).map((item, index) => {
|
||||
const ref = readRecord(item, `${path}[${index}]`)
|
||||
return {
|
||||
code: readNonEmptyString(ref.code, `${path}[${index}].code`),
|
||||
name: readNonEmptyString(ref.name, `${path}[${index}].name`),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function readNullablePublication(
|
||||
value: unknown,
|
||||
path: string,
|
||||
|
||||
Reference in New Issue
Block a user