持仓数据接口
This commit is contained in:
parent
6a81305bad
commit
249be8d882
@ -5,3 +5,9 @@ export const baiduHttp = axios.create({
|
||||
baseURL: '/baidu',
|
||||
timeout: 15000,
|
||||
})
|
||||
|
||||
/** 开发环境走 Vite 代理 /jqka → https://fupage.10jqka.com.cn */
|
||||
export const jqkaHttp = axios.create({
|
||||
baseURL: '/jqka',
|
||||
timeout: 15000,
|
||||
})
|
||||
|
||||
97
src/api/jqka/mapPosition.ts
Normal file
97
src/api/jqka/mapPosition.ts
Normal file
@ -0,0 +1,97 @@
|
||||
import type { PositionRow, PositionsData, TopTwentySum } from '../../types'
|
||||
import type { JqkaDealPositionData, JqkaPositionItem } from './types'
|
||||
|
||||
const TOP_N = 20
|
||||
|
||||
function withPercent(list: PositionRow[], totalHint?: number): PositionRow[] {
|
||||
const total =
|
||||
totalHint && totalHint > 0
|
||||
? totalHint
|
||||
: list.reduce((s, x) => s + Math.abs(x.qty), 0)
|
||||
if (total <= 0) {
|
||||
return list.map((x) => ({ ...x, percent: 0 }))
|
||||
}
|
||||
return list.map((x) => ({
|
||||
...x,
|
||||
percent: Number(((Math.abs(x.qty) / total) * 100).toFixed(2)),
|
||||
}))
|
||||
}
|
||||
|
||||
function toRows(
|
||||
list: JqkaPositionItem[],
|
||||
pickQty: (x: JqkaPositionItem) => number,
|
||||
pickChange: (x: JqkaPositionItem) => number,
|
||||
opts?: { absQty?: boolean; filter?: (x: JqkaPositionItem) => boolean },
|
||||
): PositionRow[] {
|
||||
const filtered = list.filter((item) => {
|
||||
if (opts?.filter && !opts.filter(item)) return false
|
||||
// 空持仓不展示、不参与统计
|
||||
return pickQty(item) !== 0
|
||||
})
|
||||
const ranked = [...filtered]
|
||||
.sort((a, b) => Math.abs(pickQty(b)) - Math.abs(pickQty(a)))
|
||||
.slice(0, TOP_N)
|
||||
|
||||
return ranked.map((item, i) => {
|
||||
const raw = pickQty(item)
|
||||
return {
|
||||
rank: i + 1,
|
||||
name: item.company,
|
||||
qty: opts?.absQty ? Math.abs(raw) : raw,
|
||||
change: pickChange(item),
|
||||
percent: 0,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 将同花顺持仓接口映射为页面用 PositionsData(总持仓 / 成交量 / 净持仓)。
|
||||
* 多空等列表均为非零持仓按数量排序后的前 20。
|
||||
*/
|
||||
export function mapJqkaDealPosition(data: JqkaDealPositionData): PositionsData {
|
||||
const list = data.positionList ?? []
|
||||
const sumLatest = data.topTwentyPositionSum?.[0]
|
||||
|
||||
const long = withPercent(
|
||||
toRows(list, (x) => x.f009n, (x) => x.f013n),
|
||||
sumLatest?.f009nSum,
|
||||
)
|
||||
const short = withPercent(
|
||||
toRows(list, (x) => x.f015n, (x) => x.f019n),
|
||||
sumLatest?.f015nSum,
|
||||
)
|
||||
const volume = withPercent(toRows(list, (x) => x.f003n, (x) => x.f007n))
|
||||
|
||||
const netLong = withPercent(
|
||||
toRows(list, (x) => x.f024n, (x) => x.f025n, {
|
||||
filter: (x) => x.f024n > 0,
|
||||
}),
|
||||
)
|
||||
const netShort = withPercent(
|
||||
toRows(list, (x) => x.f024n, (x) => x.f025n, {
|
||||
absQty: true,
|
||||
filter: (x) => x.f024n < 0,
|
||||
}),
|
||||
)
|
||||
|
||||
const topTwentySum: TopTwentySum[] = (data.topTwentyPositionSum ?? []).map(
|
||||
(s) => ({
|
||||
tradeDate: s.tradeDate,
|
||||
longSum: s.f009nSum,
|
||||
shortSum: s.f015nSum,
|
||||
}),
|
||||
)
|
||||
|
||||
const updatedAt =
|
||||
list[0]?.tradeDate || sumLatest?.tradeDate || new Date().toISOString().slice(0, 10)
|
||||
|
||||
return {
|
||||
long,
|
||||
short,
|
||||
volume,
|
||||
netLong,
|
||||
netShort,
|
||||
topTwentySum,
|
||||
updatedAt,
|
||||
}
|
||||
}
|
||||
44
src/api/jqka/position.ts
Normal file
44
src/api/jqka/position.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import { jqkaHttp } from '../http'
|
||||
import type { JqkaDealPositionData, JqkaDealPositionResponse } from './types'
|
||||
import { extractVariety, getPreviousTradingDate } from '../../utils/tradingDate'
|
||||
|
||||
export interface GetDealPositionParams {
|
||||
/** 合约代码,如 FG609 */
|
||||
contract: string
|
||||
/** 品种代码;缺省时从 contract 字母前缀推导 */
|
||||
variety?: string
|
||||
/**
|
||||
* 查询日期 YYYY-MM-DD。
|
||||
* 缺省为上一交易日(接口一般只能查到上一交易日)。
|
||||
*/
|
||||
date?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 同花顺 — 期货会员成交持仓(龙虎榜)
|
||||
* GET /futgwapi/api/market/v1/position/getDealPosition/?date=&variety=&contract=
|
||||
*/
|
||||
export async function getDealPosition(
|
||||
params: GetDealPositionParams,
|
||||
): Promise<JqkaDealPositionData> {
|
||||
const contract = params.contract
|
||||
const variety = params.variety ?? extractVariety(contract)
|
||||
const date = params.date ?? getPreviousTradingDate()
|
||||
|
||||
const { data } = await jqkaHttp.get<JqkaDealPositionResponse>(
|
||||
'/futgwapi/api/market/v1/position/getDealPosition/',
|
||||
{
|
||||
params: { date, variety, contract },
|
||||
},
|
||||
)
|
||||
|
||||
if (data.code !== 0) {
|
||||
throw new Error(`持仓接口失败: code=${data.code}, msg=${data.msg}`)
|
||||
}
|
||||
|
||||
if (!data.data?.positionList) {
|
||||
throw new Error('持仓接口返回空 positionList')
|
||||
}
|
||||
|
||||
return data.data
|
||||
}
|
||||
43
src/api/jqka/types.ts
Normal file
43
src/api/jqka/types.ts
Normal file
@ -0,0 +1,43 @@
|
||||
/** 同花顺期货网关 — 会员持仓明细行 */
|
||||
export interface JqkaPositionItem {
|
||||
tradeDate: string
|
||||
contract: string
|
||||
variety: string
|
||||
company: string
|
||||
/** 成交量 */
|
||||
f003n: number
|
||||
/** 成交量增减 */
|
||||
f007n: number
|
||||
/** 多单持仓数 */
|
||||
f009n: number
|
||||
/** 多单增减 */
|
||||
f013n: number
|
||||
/** 空单持仓数 */
|
||||
f015n: number
|
||||
/** 空单增减 */
|
||||
f019n: number
|
||||
/** 净持仓数 */
|
||||
f024n: number
|
||||
/** 净持仓增减 */
|
||||
f025n: number
|
||||
}
|
||||
|
||||
/** 前 20 持仓量汇总(按交易日) */
|
||||
export interface JqkaTopTwentySum {
|
||||
tradeDate: string
|
||||
/** 多单持仓合计 */
|
||||
f009nSum: number
|
||||
/** 空单持仓合计 */
|
||||
f015nSum: number
|
||||
}
|
||||
|
||||
export interface JqkaDealPositionData {
|
||||
positionList: JqkaPositionItem[]
|
||||
topTwentyPositionSum: JqkaTopTwentySum[]
|
||||
}
|
||||
|
||||
export interface JqkaDealPositionResponse {
|
||||
code: number
|
||||
msg: string
|
||||
data: JqkaDealPositionData
|
||||
}
|
||||
@ -1,12 +1,16 @@
|
||||
<template>
|
||||
<section class="position-panel card">
|
||||
<el-tabs v-model="tab">
|
||||
<el-tab-pane label="总持仓" name="total" />
|
||||
<el-tab-pane label="成交量" name="volume" />
|
||||
<el-tab-pane label="净持仓" name="net" />
|
||||
</el-tabs>
|
||||
<div v-if="tab !== 'total'" class="placeholder-tip">当前为占位数据(结构与总持仓一致)</div>
|
||||
<div class="cols">
|
||||
<div class="head">
|
||||
<el-tabs v-model="tab">
|
||||
<el-tab-pane label="总持仓" name="total" />
|
||||
<el-tab-pane label="成交量" name="volume" />
|
||||
<el-tab-pane label="净持仓" name="net" />
|
||||
</el-tabs>
|
||||
<span v-if="data.updatedAt" class="updated">数据日期 {{ data.updatedAt }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 总持仓:多 / 空 -->
|
||||
<div v-if="tab === 'total'" class="cols">
|
||||
<div class="col">
|
||||
<h3 class="long-title">多单持仓前20名</h3>
|
||||
<PositionDonut :list="data.long" />
|
||||
@ -18,6 +22,29 @@
|
||||
<PositionTable :list="data.short" qty-label="空单" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 成交量:单列 -->
|
||||
<div v-else-if="tab === 'volume'" class="cols single">
|
||||
<div class="col">
|
||||
<h3 class="volume-title">成交量前20名</h3>
|
||||
<PositionDonut :list="data.volume" />
|
||||
<PositionTable :list="data.volume" qty-label="成交量" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 净持仓:净多 / 净空 -->
|
||||
<div v-else class="cols">
|
||||
<div class="col">
|
||||
<h3 class="long-title">净多持仓前20名</h3>
|
||||
<PositionDonut :list="data.netLong" />
|
||||
<PositionTable :list="data.netLong" qty-label="净持仓" />
|
||||
</div>
|
||||
<div class="col">
|
||||
<h3 class="short-title">净空持仓前20名</h3>
|
||||
<PositionDonut :list="data.netShort" />
|
||||
<PositionTable :list="data.netShort" qty-label="净持仓" />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
@ -40,10 +67,23 @@ const tab = ref('total')
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.placeholder-tip {
|
||||
.head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.head :deep(.el-tabs) {
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.updated {
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
margin: -4px 0 8px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.cols {
|
||||
@ -52,6 +92,11 @@ const tab = ref('total')
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.cols.single {
|
||||
grid-template-columns: 1fr;
|
||||
max-width: 560px;
|
||||
}
|
||||
|
||||
.long-title {
|
||||
margin: 0 0 8px;
|
||||
color: var(--long);
|
||||
@ -64,9 +109,15 @@ const tab = ref('total')
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.volume-title {
|
||||
margin: 0 0 8px;
|
||||
color: var(--text-primary, #303133);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.cols {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
@ -37,4 +37,4 @@ withDefaults(
|
||||
.chg-down {
|
||||
color: var(--short);
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
@ -1,8 +1,10 @@
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { onMounted } from 'vue'
|
||||
import { useQuotaStore } from '../stores/quota'
|
||||
|
||||
/**
|
||||
* 机构持仓 composable:状态统一落在 quota store(暂 mock)。
|
||||
* 机构持仓 composable:状态统一落在 quota store。
|
||||
* 打开页面时按上一交易日拉取同花顺会员持仓。
|
||||
*/
|
||||
export function usePositions() {
|
||||
const store = useQuotaStore()
|
||||
@ -12,6 +14,10 @@ export function usePositions() {
|
||||
positionsError: error,
|
||||
} = storeToRefs(store)
|
||||
|
||||
onMounted(() => {
|
||||
void store.fetchPositions()
|
||||
})
|
||||
|
||||
return {
|
||||
data,
|
||||
loading,
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
/** 当前关注的期货合约(后续可改为可切换) */
|
||||
export const contractConfig = {
|
||||
/** 合约代码,对应百度行情接口 code 参数 */
|
||||
/** 合约代码,对应百度行情 / 同花顺持仓接口 code/contract */
|
||||
code: 'FG609',
|
||||
/** 品种代码,持仓接口 variety;缺省可从 code 字母前缀推导 */
|
||||
variety: 'FG',
|
||||
/** 展示名称;接口未返回 name 时使用 */
|
||||
name: '玻璃2609',
|
||||
/** 交易所展示名 */
|
||||
|
||||
@ -68,8 +68,21 @@ const shortNames = [
|
||||
'其他',
|
||||
]
|
||||
|
||||
const long = withPercent(makeList(longNames, 650000))
|
||||
const short = withPercent(makeList(shortNames, 640000))
|
||||
const volume = withPercent(makeList(longNames, 480000))
|
||||
const netLong = withPercent(makeList(longNames.slice(0, 12), 120000))
|
||||
const netShort = withPercent(makeList(shortNames.slice(0, 12), 110000))
|
||||
|
||||
export const positionsMock: PositionsData = {
|
||||
long: withPercent(makeList(longNames, 650000)),
|
||||
short: withPercent(makeList(shortNames, 640000)),
|
||||
long,
|
||||
short,
|
||||
volume,
|
||||
netLong,
|
||||
netShort,
|
||||
topTwentySum: [
|
||||
{ tradeDate: '2026-07-15', longSum: 1073446, shortSum: 1502020 },
|
||||
{ tradeDate: '2026-07-14', longSum: 1094199, shortSum: 1519906 },
|
||||
],
|
||||
updatedAt: '2026-07-15',
|
||||
}
|
||||
|
||||
@ -11,6 +11,12 @@ import { mapBaiduQuotationToQuote } from '../api/baidu/mapQuote'
|
||||
import { mapBaiduKlineToCandles } from '../api/baidu/mapKline'
|
||||
import { mapBaiduNewsToItems } from '../api/baidu/mapNews'
|
||||
import type { BaiduKlineType } from '../api/baidu/types'
|
||||
import { getDealPosition } from '../api/jqka/position'
|
||||
import { mapJqkaDealPosition } from '../api/jqka/mapPosition'
|
||||
import {
|
||||
extractVariety,
|
||||
getPreviousTradingDate,
|
||||
} from '../utils/tradingDate'
|
||||
|
||||
/** K 线周期(日/周/月) */
|
||||
export type KlinePeriod = 'day' | 'week' | 'month'
|
||||
@ -55,7 +61,7 @@ export const useQuotaStore = defineStore('quota', () => {
|
||||
const newsLoading = ref(false)
|
||||
const newsError = ref<unknown>(null)
|
||||
|
||||
// ─── 机构持仓(暂 mock,后续接 API)────────────────────
|
||||
// ─── 机构持仓 ───────────────────────────────────────────
|
||||
const positions = ref<PositionsData>(structuredClone(positionsMock))
|
||||
const positionsLoading = ref(false)
|
||||
const positionsError = ref<unknown>(null)
|
||||
@ -121,13 +127,33 @@ export const useQuotaStore = defineStore('quota', () => {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 拉取会员持仓。日期默认上一交易日;若遇节假日空数据则再往前最多试 5 个交易日。
|
||||
*/
|
||||
async function fetchPositions() {
|
||||
positionsLoading.value = true
|
||||
positionsError.value = null
|
||||
try {
|
||||
// TODO: 接入真实持仓 API 后替换
|
||||
await new Promise((r) => setTimeout(r, 300))
|
||||
positions.value = structuredClone(positionsMock)
|
||||
const contract = contractConfig.code
|
||||
const variety = contractConfig.variety || extractVariety(contract)
|
||||
let date = getPreviousTradingDate()
|
||||
let mapped: PositionsData | null = null
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const raw = await getDealPosition({ contract, variety, date })
|
||||
if (raw.positionList?.length) {
|
||||
mapped = mapJqkaDealPosition(raw)
|
||||
break
|
||||
}
|
||||
// 空列表:再往前一个交易日(覆盖长假)
|
||||
const d = new Date(`${date}T12:00:00`)
|
||||
date = getPreviousTradingDate(d)
|
||||
}
|
||||
|
||||
if (!mapped) {
|
||||
throw new Error(`持仓无数据(已回溯至 ${date})`)
|
||||
}
|
||||
positions.value = mapped
|
||||
} catch (e) {
|
||||
positionsError.value = e
|
||||
console.error('[quota] 拉取持仓失败', e)
|
||||
|
||||
@ -82,9 +82,26 @@ export interface PositionRow {
|
||||
percent: number
|
||||
}
|
||||
|
||||
/** 前 20 多空持仓汇总(按交易日) */
|
||||
export interface TopTwentySum {
|
||||
tradeDate: string
|
||||
longSum: number
|
||||
shortSum: number
|
||||
}
|
||||
|
||||
export interface PositionsData {
|
||||
/** 多单持仓前 20(f009n / f013n) */
|
||||
long: PositionRow[]
|
||||
/** 空单持仓前 20(f015n / f019n) */
|
||||
short: PositionRow[]
|
||||
/** 成交量前 20(f003n / f007n) */
|
||||
volume: PositionRow[]
|
||||
/** 净多持仓(f024n > 0) */
|
||||
netLong: PositionRow[]
|
||||
/** 净空持仓(f024n < 0,qty 取绝对值) */
|
||||
netShort: PositionRow[]
|
||||
/** 前 20 持仓量汇总 */
|
||||
topTwentySum: TopTwentySum[]
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
|
||||
35
src/utils/tradingDate.ts
Normal file
35
src/utils/tradingDate.ts
Normal file
@ -0,0 +1,35 @@
|
||||
/**
|
||||
* 格式化为 YYYY-MM-DD(本地时区)
|
||||
*/
|
||||
export function formatDateYmd(d: Date): string {
|
||||
const y = d.getFullYear()
|
||||
const m = String(d.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(d.getDate()).padStart(2, '0')
|
||||
return `${y}-${m}-${day}`
|
||||
}
|
||||
|
||||
function isWeekend(d: Date): boolean {
|
||||
const day = d.getDay()
|
||||
return day === 0 || day === 6
|
||||
}
|
||||
|
||||
/**
|
||||
* 上一个交易日(跳过周末;不含法定节假日日历)。
|
||||
* 持仓接口一般只能查到上一交易日数据,故默认用此日期。
|
||||
*/
|
||||
export function getPreviousTradingDate(from: Date = new Date()): string {
|
||||
const d = new Date(from.getFullYear(), from.getMonth(), from.getDate())
|
||||
d.setDate(d.getDate() - 1)
|
||||
while (isWeekend(d)) {
|
||||
d.setDate(d.getDate() - 1)
|
||||
}
|
||||
return formatDateYmd(d)
|
||||
}
|
||||
|
||||
/**
|
||||
* 从合约代码提取品种,如 FG609 → FG、au2512 → au
|
||||
*/
|
||||
export function extractVariety(contract: string): string {
|
||||
const m = contract.match(/^([A-Za-z]+)/)
|
||||
return m ? m[1] : contract
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user