xtraderClient/src/api/event.ts
2026-02-28 00:03:37 +08:00

378 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { get } from './request'
/** 分页结果 */
export interface PageResult<T> {
list: T[]
page: number
pageSize: number
total: number
}
/**
* Event 单项结构(与 doc.json definitions["polymarket.PmEvent"] 对齐)
* 用于 /PmEvent/getPmEventPublic 列表项 与 /PmEvent/findPmEvent 的 data
*/
export interface PmEventListItem {
ID: number
createdAt?: string
updatedAt?: string
slug?: string
ticker?: string
title: string
description?: string
resolutionSource?: string
startDate?: string
endDate?: string
creationDate?: string
closedTime?: string
startTime?: string
image?: string
icon?: string
active?: boolean
archived?: boolean
closed?: boolean
featured?: boolean
new?: boolean
restricted?: boolean
enableOrderBook?: boolean
competitive?: number
liquidity?: number
liquidityAmm?: number
liquidityClob?: number
openInterest?: number
volume?: number
commentCount?: number
seriesSlug?: string
markets?: PmEventMarketItem[]
series?: PmEventSeriesItem[]
tags?: PmEventTagItem[]
[key: string]: unknown
}
/**
* 对应 definitions polymarket.PmMarket 常用字段
* - outcomes: 选项展示文案,如 ["Yes", "No"] 或 ["Up", "Down"],与 outcomePrices 一一对应
* - outcomePrices: 各选项价格(首项为第一选项概率,如 Yes/Up
*/
export interface PmEventMarketItem {
/** 市场 ID部分接口返回大写 ID */
ID?: number
/** 市场 ID部分接口或 JSON 序列化为小写 id */
id?: number
question?: string
slug?: string
/** 选项展示文案,与 outcomePrices 顺序一致 */
outcomes?: string[]
/** 各选项价格outcomes[0] 对应 outcomePrices[0] */
outcomePrices?: string[] | number[]
/** 市场对应的 clob token id 与 outcomePrices、outcomes 顺序一致outcomes[0] 对应 clobTokenIds[0] */
clobTokenIds?: string[]
endDate?: string
volume?: number
[key: string]: unknown
}
/** 从市场项取 marketId兼容 ID / id */
export function getMarketId(m: PmEventMarketItem | null | undefined): string | undefined {
if (!m) return undefined
const raw = m.ID ?? m.id
return raw != null ? String(raw) : undefined
}
/** 从市场项取 clobTokenIdoutcomeIndex 0=Yes/第一选项1=No/第二选项 */
export function getClobTokenId(
m: PmEventMarketItem | null | undefined,
outcomeIndex: 0 | 1 = 0,
): string | undefined {
if (!m?.clobTokenIds?.length) return undefined
const id = m.clobTokenIds[outcomeIndex]
return id != null ? String(id) : undefined
}
/** 对应 definitions polymarket.PmSeries 常用字段 */
export interface PmEventSeriesItem {
ID?: number
ticker?: string
title?: string
slug?: string
[key: string]: unknown
}
/** 对应 definitions polymarket.PmTag 常用字段 */
export interface PmEventTagItem {
label?: string
slug?: string
ID?: number
[key: string]: unknown
}
/** 接口统一响应 */
export interface PmEventListResponse {
code: number
data: PageResult<PmEventListItem>
msg: string
}
/**
* GET /PmEvent/getPmEventPublic 请求参数(与 doc.json 对齐)
*/
export interface GetPmEventListParams {
page?: number
pageSize?: number
keyword?: string
/** 创建时间范围,如 ['2025-01-01', '2025-12-31'] */
createdAtRange?: string[]
/** 标签 ID 列表,按分类筛选,传统数组方式传递 */
tagIds?: number[]
}
/**
* 分页获取 Event 列表(公开接口,不需要鉴权)
* GET /PmEvent/getPmEventPublic
*
* Query: page, pageSize, keyword, createdAtRange, tagIds
* doc.json: paths["/PmEvent/getPmEventPublic"].get.parameters
*/
export async function getPmEventPublic(
params: GetPmEventListParams = {},
): Promise<PmEventListResponse> {
const { page = 1, pageSize = 10, keyword, createdAtRange, tagIds } = params
const query: Record<string, string | number | number[] | string[] | undefined> = {
page,
pageSize,
}
if (keyword != null && keyword !== '') query.keyword = keyword
if (createdAtRange != null && createdAtRange.length) query.createdAtRange = createdAtRange
if (tagIds != null && tagIds.length > 0) {
query.tagIds = tagIds
}
return get<PmEventListResponse>('/PmEvent/getPmEventPublic', query)
}
/**
* GET /PmEvent/findPmEvent 响应体200
* doc.json: responses["200"].schema = allOf [ response.Response, { data: polymarket.PmEvent, msg } ]
*/
export interface PmEventDetailResponse {
code: number
data: PmEventListItem
msg: string
}
/**
* findPmEvent 请求参数Query
* doc.json: ID 与 slug 支持同时传入,至少传一个
*/
export interface FindPmEventParams {
/** Event 主键(数字 ID */
id?: number
/** Event 的 slug 标识 */
slug?: string
}
/**
* 用 id 和/或 slug 查询 Event 详情
* GET /PmEvent/findPmEvent
*
* 请求参数Query
* - ID: number可选
* - slug: string可选
* - ID 与 slug 至少传一个,可同时传
* 鉴权:需在 headers 中传 x-token、x-user-id
*
* 响应200PmEventDetailResponse { code, data: PmEventListItem, msg }
*/
export async function findPmEvent(
params: FindPmEventParams,
config?: { headers?: Record<string, string> },
): Promise<PmEventDetailResponse> {
const query: Record<string, string | number> = {}
if (params.id != null) query.ID = params.id
if (params.slug != null && params.slug !== '') query.slug = params.slug
if (Object.keys(query).length === 0) {
throw new Error('findPmEvent: 至少需要传 id 或 slug')
}
return get<PmEventDetailResponse>('/PmEvent/findPmEvent', query, config)
}
/** 多选项卡片中单个选项(用于左右滑动切换) */
export interface EventCardOutcome {
title: string
/** 第一选项概率(来自 outcomePrices[0] */
chanceValue: number
/** Yes 价格 01来自 outcomePrices[0],供交易组件使用 */
yesPrice?: number
/** No 价格 01来自 outcomePrices[1],供交易组件使用 */
noPrice?: number
yesLabel?: string
noLabel?: string
marketId?: string
clobTokenIds?: string[]
}
/**
* 首页卡片项(与 mapEventItemToCard 返回结构一致,用于缓存)
* displayTypesingle = 单一 Yes/Nomulti = 多个选项左右滑动
*/
export interface EventCardItem {
id: string
/** Event slug用于 findPmEvent 传参 */
slug?: string
marketTitle: string
chanceValue: number
marketInfo: string
imageUrl: string
category: string
expiresAt: string
/** 展示类型:单一二元 或 多选项滑动 */
displayType: 'single' | 'multi'
/** 多选项时每个选项的标题与概率,按顺序滑动展示 */
outcomes?: EventCardOutcome[]
/** 单一类型时可选按钮文案,如 "Up"/"Down" */
yesLabel?: string
noLabel?: string
/** 是否显示 NEW 角标 */
isNew?: boolean
/** 当前市场 ID单 market 时为第一个 market 的 ID供交易/Split 使用) */
marketId?: string
/** 用于下单 tokenId单 market 时取自 firstMarket.clobTokenIds */
clobTokenIds?: string[]
/** Yes 价格 01来自 outcomePrices[0],供交易组件使用 */
yesPrice?: number
/** No 价格 01来自 outcomePrices[1],供交易组件使用 */
noPrice?: number
}
/** 内存缓存:列表数据,切换页面时复用,下拉刷新时清空 */
let eventListCache: {
list: EventCardItem[]
page: number
total: number
pageSize: number
} | null = null
export function getEventListCache(): typeof eventListCache {
return eventListCache
}
export function setEventListCache(data: {
list: EventCardItem[]
page: number
total: number
pageSize: number
}) {
eventListCache = data
}
export function clearEventListCache(): void {
eventListCache = null
}
function marketChance(market: PmEventMarketItem): number {
const raw = market?.outcomePrices?.[0]
if (raw == null) return 17
const yesPrice = parseFloat(String(raw))
if (!Number.isFinite(yesPrice)) return 17
return Math.min(100, Math.max(0, Math.round(yesPrice * 100)))
}
/**
* 将 list 单项映射为首页 MarketCard 所需字段
* - 单一 market 或无 marketdisplayType single当前逻辑
* - 多个 marketsdisplayType multioutcomes 为每项标题+概率,卡片内左右滑动切换
*/
export function mapEventItemToCard(item: PmEventListItem): EventCardItem {
const id = String(item.ID ?? '')
const marketTitle = item.title ?? ''
const imageUrl = item.image ?? item.icon ?? ''
const markets = item.markets ?? []
const multi = markets.length > 1
let chanceValue = 17
const firstMarket = markets[0]
if (firstMarket?.outcomePrices?.[0] != null) {
chanceValue = marketChance(firstMarket)
}
let marketInfo = '$0 Vol.'
if (item.volume != null && Number.isFinite(item.volume)) {
const v = item.volume
if (v >= 1000) {
marketInfo = `$${(v / 1000).toFixed(1)}k Vol.`
} else {
marketInfo = `$${Math.round(v)} Vol.`
}
}
let expiresAt = ''
if (item.endDate) {
try {
const d = new Date(item.endDate)
if (!Number.isNaN(d.getTime())) {
expiresAt = d.toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
})
}
} catch {
expiresAt = item.endDate
}
}
const category = item.series?.[0]?.title ?? item.tags?.[0]?.label ?? ''
function parseOutcomePrices(m: PmEventMarketItem): { yesPrice: number; noPrice: number } {
const y = m?.outcomePrices?.[0]
const n = m?.outcomePrices?.[1]
const yesPrice =
y != null && Number.isFinite(parseFloat(String(y)))
? Math.min(1, Math.max(0, parseFloat(String(y))))
: 0.5
const noPrice =
n != null && Number.isFinite(parseFloat(String(n)))
? Math.min(1, Math.max(0, parseFloat(String(n))))
: 1 - yesPrice
return { yesPrice, noPrice }
}
const outcomes: EventCardOutcome[] | undefined = multi
? markets.map((m) => {
const { yesPrice, noPrice } = parseOutcomePrices(m)
return {
title: m.question ?? '',
chanceValue: marketChance(m),
yesPrice,
noPrice,
yesLabel: m.outcomes?.[0] ?? 'Yes',
noLabel: m.outcomes?.[1] ?? 'No',
marketId: getMarketId(m),
clobTokenIds: m.clobTokenIds,
}
})
: undefined
const firstPrices = firstMarket ? parseOutcomePrices(firstMarket) : { yesPrice: 0.5, noPrice: 0.5 }
return {
id,
slug: item.slug ?? undefined,
marketTitle,
chanceValue,
marketInfo,
imageUrl,
category,
expiresAt,
displayType: multi ? 'multi' : 'single',
outcomes,
yesLabel: firstMarket?.outcomes?.[0] ?? 'Yes',
noLabel: firstMarket?.outcomes?.[1] ?? 'No',
isNew: item.new === true,
marketId: getMarketId(firstMarket),
clobTokenIds: firstMarket?.clobTokenIds,
yesPrice: firstPrices.yesPrice,
noPrice: firstPrices.noPrice,
}
}