查询机构品种盈利情况

This commit is contained in:
dongzp 2026-07-21 18:56:08 +08:00
parent f2a18522f8
commit 24a72bfd3f
9 changed files with 344 additions and 16 deletions

View File

@ -1,5 +1,9 @@
import type { PositionRow, PositionsData, TopTwentySum } from '../../types' import type { PositionRow, PositionsData, ProfitRow, TopTwentySum } from '../../types'
import type { JqkaDealPositionData, JqkaPositionItem } from './types' import type {
JqkaDealPositionData,
JqkaPositionItem,
JqkaPositionProfitData,
} from './types'
const TOP_N = 20 const TOP_N = 20
@ -91,7 +95,74 @@ export function mapJqkaDealPosition(data: JqkaDealPositionData): PositionsData {
volume, volume,
netLong, netLong,
netShort, netShort,
profitGain: [],
profitLoss: [],
topTwentySum, topTwentySum,
updatedAt, updatedAt,
} }
} }
function withProfitPercent(list: ProfitRow[]): ProfitRow[] {
const total = list.reduce((s, x) => s + Math.abs(x.profit), 0)
if (total <= 0) {
return list.map((x) => ({ ...x, percent: 0 }))
}
return list.map((x) => ({
...x,
percent: Number(((Math.abs(x.profit) / total) * 100).toFixed(2)),
}))
}
/**
* 将机构盈利接口映射为盈利 / 亏损前 20。
* day_profit 为盈利金额;顺带保留净持仓与增减。
*/
export function mapJqkaPositionProfit(data: JqkaPositionProfitData): {
profitGain: ProfitRow[]
profitLoss: ProfitRow[]
updatedAt: string
} {
const list = (data.profit_detail_list ?? [])
.map((item) => ({
name: item.company,
profit: Number(item.day_profit) || 0,
netQty: item.f024n,
change: item.f025n,
date: item.date,
}))
.filter((x) => x.profit !== 0)
const gainRaw = [...list]
.filter((x) => x.profit > 0)
.sort((a, b) => b.profit - a.profit)
.slice(0, TOP_N)
.map((x, i) => ({
rank: i + 1,
name: x.name,
profit: x.profit,
netQty: x.netQty,
change: x.change,
date: x.date,
percent: 0,
}))
const lossRaw = [...list]
.filter((x) => x.profit < 0)
.sort((a, b) => a.profit - b.profit)
.slice(0, TOP_N)
.map((x, i) => ({
rank: i + 1,
name: x.name,
profit: x.profit,
netQty: x.netQty,
change: x.change,
date: x.date,
percent: 0,
}))
return {
profitGain: withProfitPercent(gainRaw),
profitLoss: withProfitPercent(lossRaw),
updatedAt: data.date || new Date().toISOString().slice(0, 10),
}
}

View File

@ -1,6 +1,16 @@
import { jqkaHttp } from '../http' import { jqkaHttp } from '../http'
import type { JqkaDealPositionData, JqkaDealPositionResponse } from './types' import type {
import { extractVariety, getPreviousTradingDate } from '../../utils/tradingDate' JqkaDealPositionData,
JqkaDealPositionResponse,
JqkaPositionProfitData,
JqkaPositionProfitRequest,
JqkaPositionProfitResponse,
} from './types'
import {
extractVariety,
getPreviousTradingDate,
getRecentMonthRange,
} from '../../utils/tradingDate'
export interface GetDealPositionParams { export interface GetDealPositionParams {
/** 合约代码,如 FG609 */ /** 合约代码,如 FG609 */
@ -42,3 +52,56 @@ export async function getDealPosition(
return data.data return data.data
} }
export interface GetPositionProfitRankParams {
/** 品种代码;缺省时可从 contract 推导 */
variety?: string
contract?: string
/** 默认最近一个月 */
startDate?: string
endDate?: string
/** type=company 时使用;variety 查询可不传 */
company?: string
type?: 'variety' | 'company'
}
/**
* 同花顺 — 机构盈利排行(按品种查看最近盈利)
* POST /futgwapi/api/market/dragon_tiger/v1/position_profit_rank
*/
export async function getPositionProfitRank(
params: GetPositionProfitRankParams = {},
): Promise<JqkaPositionProfitData> {
const variety =
params.variety ??
(params.contract ? extractVariety(params.contract) : undefined)
if (!variety) {
throw new Error('盈利接口需要 variety 或 contract')
}
const range = getRecentMonthRange()
const body: JqkaPositionProfitRequest = {
variety,
type: params.type ?? 'variety',
start_date: params.startDate ?? range.startDate,
end_date: params.endDate ?? range.endDate,
}
if (params.company) {
body.company = params.company
}
const { data } = await jqkaHttp.post<JqkaPositionProfitResponse>(
'/futgwapi/api/market/dragon_tiger/v1/position_profit_rank',
body,
)
if (data.code !== 0) {
throw new Error(`盈利接口失败: code=${data.code}, msg=${data.msg}`)
}
if (!data.data?.profit_detail_list) {
throw new Error('盈利接口返回空 profit_detail_list')
}
return data.data
}

View File

@ -41,3 +41,39 @@ export interface JqkaDealPositionResponse {
msg: string msg: string
data: JqkaDealPositionData data: JqkaDealPositionData
} }
/** 机构盈利排行明细行 */
export interface JqkaProfitDetailItem {
variety: string
company: string
/** 净持仓 */
f024n: number
/** 净持仓增减 */
f025n: number
date: string
variety_name: string
/** 盈利金额(字符串数字) */
day_profit: string
year_profit: string | null
}
export interface JqkaPositionProfitData {
date: string
profit_detail_list: JqkaProfitDetailItem[]
}
export interface JqkaPositionProfitResponse {
code: number
msg: string
data: JqkaPositionProfitData
}
export interface JqkaPositionProfitRequest {
/** 品种代码,如 FG;type=variety 时必填 */
variety: string
type: 'variety' | 'company'
start_date: string
end_date: string
/** type=company 时必填;variety 查询可省略 */
company?: string
}

View File

@ -5,11 +5,11 @@
<el-tab-pane label="总持仓" name="total" /> <el-tab-pane label="总持仓" name="total" />
<el-tab-pane label="成交量" name="volume" /> <el-tab-pane label="成交量" name="volume" />
<el-tab-pane label="净持仓" name="net" /> <el-tab-pane label="净持仓" name="net" />
<el-tab-pane label="机构盈利" name="profit" />
</el-tabs> </el-tabs>
<span v-if="data.updatedAt" class="updated">数据日期 {{ data.updatedAt }}</span> <span v-if="data.updatedAt" class="updated">数据日期 {{ data.updatedAt }}</span>
</div> </div>
<!-- 总持仓:多空汇总柱状图 + 多 / 空前20 -->
<div v-if="tab === 'total'"> <div v-if="tab === 'total'">
<PositionSumBar <PositionSumBar
v-if="data.topTwentySum?.length" v-if="data.topTwentySum?.length"
@ -29,7 +29,6 @@
</div> </div>
</div> </div>
<!-- 成交量:单列 -->
<div v-else-if="tab === 'volume'" class="cols single"> <div v-else-if="tab === 'volume'" class="cols single">
<div class="col"> <div class="col">
<h3 class="volume-title">成交量前20名</h3> <h3 class="volume-title">成交量前20名</h3>
@ -38,8 +37,7 @@
</div> </div>
</div> </div>
<!-- 净持仓:净多 / 净空 --> <div v-else-if="tab === 'net'" class="cols">
<div v-else class="cols">
<div class="col"> <div class="col">
<h3 class="long-title">净多持仓前20名</h3> <h3 class="long-title">净多持仓前20名</h3>
<PositionDonut :list="data.netLong" /> <PositionDonut :list="data.netLong" />
@ -51,21 +49,50 @@
<PositionTable :list="data.netShort" qty-label="净持仓" /> <PositionTable :list="data.netShort" qty-label="净持仓" />
</div> </div>
</div> </div>
<div v-else class="cols">
<div class="col">
<h3 class="long-title">盈利机构前20名</h3>
<PositionDonut :list="profitGainAsRows" />
<ProfitTable :list="data.profitGain" />
</div>
<div class="col">
<h3 class="short-title">亏损机构前20名</h3>
<PositionDonut :list="profitLossAsRows" />
<ProfitTable :list="data.profitLoss" />
</div>
</div>
</section> </section>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue' import { computed, ref } from 'vue'
import type { PositionsData } from '../../types' import type { PositionRow, PositionsData } from '../../types'
import PositionDonut from './PositionDonut.vue' import PositionDonut from './PositionDonut.vue'
import PositionTable from './PositionTable.vue' import PositionTable from './PositionTable.vue'
import PositionSumBar from './PositionSumBar.vue' import PositionSumBar from './PositionSumBar.vue'
import ProfitTable from './ProfitTable.vue'
defineProps<{ const props = defineProps<{
data: PositionsData data: PositionsData
}>() }>()
const tab = ref('total') const tab = ref('total')
function toDonutRows(
list: PositionsData['profitGain'],
): PositionRow[] {
return (list ?? []).map((x) => ({
rank: x.rank,
name: x.name,
qty: Math.abs(x.profit),
change: x.change,
percent: x.percent,
}))
}
const profitGainAsRows = computed(() => toDonutRows(props.data.profitGain))
const profitLossAsRows = computed(() => toDonutRows(props.data.profitLoss))
</script> </script>
<style scoped> <style scoped>

View File

@ -0,0 +1,57 @@
<template>
<el-table :data="list" size="small" stripe class="profit-table" max-height="360">
<el-table-column prop="rank" label="名次" width="56" />
<el-table-column prop="name" label="会员简称" min-width="110" />
<el-table-column label="盈利金额" min-width="120">
<template #default="{ row }">
<span :class="row.profit >= 0 ? 'price-up' : 'price-down'">
{{ formatProfit(row.profit) }}
</span>
</template>
</el-table-column>
<el-table-column label="净持仓" min-width="90">
<template #default="{ row }">
{{ row.netQty }}
</template>
</el-table-column>
<el-table-column label="增减" min-width="80">
<template #default="{ row }">
<span :class="row.change >= 0 ? 'price-up' : 'chg-down'">
{{ row.change > 0 ? '+' : '' }}{{ row.change }}
</span>
</template>
</el-table-column>
<el-table-column prop="date" label="日期" width="110" />
</el-table>
</template>
<script setup lang="ts">
import type { ProfitRow } from '../../types'
defineProps<{
list: ProfitRow[]
}>()
/** 盈利金额格式化:≥1万用「万」,≥1亿用「亿」 */
function formatProfit(n: number): string {
const sign = n > 0 ? '+' : n < 0 ? '-' : ''
const abs = Math.abs(n)
if (abs >= 1e8) {
return `${sign}${(abs / 1e8).toFixed(2)}亿`
}
if (abs >= 1e4) {
return `${sign}${(abs / 1e4).toFixed(2)}万`
}
return `${sign}${abs.toFixed(0)}`
}
</script>
<style scoped>
.profit-table {
width: 100%;
}
.chg-down {
color: var(--short);
}
</style>

View File

@ -1,4 +1,4 @@
import type { PositionRow, PositionsData } from '../types' import type { PositionRow, PositionsData, ProfitRow } from '../types'
function makeList(names: string[], base: number): PositionRow[] { function makeList(names: string[], base: number): PositionRow[] {
return names.map((name, i) => { return names.map((name, i) => {
@ -22,6 +22,30 @@ function withPercent(list: PositionRow[]): PositionRow[] {
})) }))
} }
function makeProfit(
names: string[],
base: number,
sign: 1 | -1,
): ProfitRow[] {
const list = names.map((name, i) => {
const profit = sign * Math.floor(base * (0.2 - i * 0.01) + Math.random() * 5e6)
return {
rank: i + 1,
name,
profit,
netQty: Math.floor((Math.random() - 0.5) * 80000),
change: Math.floor((Math.random() - 0.5) * 2000),
date: '2026-07-21',
percent: 0,
}
})
const total = list.reduce((s, x) => s + Math.abs(x.profit), 0)
return list.map((x) => ({
...x,
percent: total > 0 ? Number(((Math.abs(x.profit) / total) * 100).toFixed(2)) : 0,
}))
}
const longNames = [ const longNames = [
'中财期货', '中财期货',
'东证期货', '东证期货',
@ -73,6 +97,8 @@ const short = withPercent(makeList(shortNames, 640000))
const volume = withPercent(makeList(longNames, 480000)) const volume = withPercent(makeList(longNames, 480000))
const netLong = withPercent(makeList(longNames.slice(0, 12), 120000)) const netLong = withPercent(makeList(longNames.slice(0, 12), 120000))
const netShort = withPercent(makeList(shortNames.slice(0, 12), 110000)) const netShort = withPercent(makeList(shortNames.slice(0, 12), 110000))
const profitGain = makeProfit(longNames.slice(0, 12), 2e8, 1)
const profitLoss = makeProfit(shortNames.slice(0, 12), 1.5e8, -1)
export const positionsMock: PositionsData = { export const positionsMock: PositionsData = {
long, long,
@ -80,6 +106,8 @@ export const positionsMock: PositionsData = {
volume, volume,
netLong, netLong,
netShort, netShort,
profitGain,
profitLoss,
topTwentySum: [ topTwentySum: [
{ tradeDate: '2026-07-15', longSum: 1073446, shortSum: 1502020 }, { tradeDate: '2026-07-15', longSum: 1073446, shortSum: 1502020 },
{ tradeDate: '2026-07-14', longSum: 1094199, shortSum: 1519906 }, { tradeDate: '2026-07-14', longSum: 1094199, shortSum: 1519906 },

View File

@ -11,8 +11,8 @@ import { mapBaiduQuotationToQuote } from '../api/baidu/mapQuote'
import { mapBaiduKlineToCandles } from '../api/baidu/mapKline' import { mapBaiduKlineToCandles } from '../api/baidu/mapKline'
import { mapBaiduNewsToItems } from '../api/baidu/mapNews' import { mapBaiduNewsToItems } from '../api/baidu/mapNews'
import type { BaiduKlineType } from '../api/baidu/types' import type { BaiduKlineType } from '../api/baidu/types'
import { getDealPosition } from '../api/jqka/position' import { getDealPosition, getPositionProfitRank } from '../api/jqka/position'
import { mapJqkaDealPosition } from '../api/jqka/mapPosition' import { mapJqkaDealPosition, mapJqkaPositionProfit } from '../api/jqka/mapPosition'
import { import {
extractVariety, extractVariety,
getPreviousTradingDate, getPreviousTradingDate,
@ -128,7 +128,9 @@ export const useQuotaStore = defineStore('quota', () => {
} }
/** /**
* 拉取会员持仓。日期默认上一交易日;若遇节假日空数据则再往前最多试 5 个交易日。 * 拉取会员持仓 + 机构盈利。
* 持仓日期默认上一交易日;若遇节假日空数据则再往前最多试 5 个交易日。
* 盈利默认最近一个月、按当前品种查询。
*/ */
async function fetchPositions() { async function fetchPositions() {
positionsLoading.value = true positionsLoading.value = true
@ -153,6 +155,16 @@ export const useQuotaStore = defineStore('quota', () => {
if (!mapped) { if (!mapped) {
throw new Error(`持仓无数据(已回溯至 ${date})`) throw new Error(`持仓无数据(已回溯至 ${date})`)
} }
try {
const profitRaw = await getPositionProfitRank({ variety, contract })
const profit = mapJqkaPositionProfit(profitRaw)
mapped.profitGain = profit.profitGain
mapped.profitLoss = profit.profitLoss
} catch (e) {
console.error('[quota] 拉取机构盈利失败', e)
}
positions.value = mapped positions.value = mapped
} catch (e) { } catch (e) {
positionsError.value = e positionsError.value = e

View File

@ -82,6 +82,20 @@ export interface PositionRow {
percent: number percent: number
} }
/** 机构盈利排行行(day_profit) */
export interface ProfitRow {
rank: number
name: string
/** 盈利金额(可正可负) */
profit: number
/** 净持仓 f024n */
netQty: number
/** 净持仓增减 f025n */
change: number
date: string
percent: number
}
/** 前 20 多空持仓汇总(按交易日) */ /** 前 20 多空持仓汇总(按交易日) */
export interface TopTwentySum { export interface TopTwentySum {
tradeDate: string tradeDate: string
@ -100,6 +114,10 @@ export interface PositionsData {
netLong: PositionRow[] netLong: PositionRow[]
/** 净空持仓(f024n < 0,qty 取绝对值) */ /** 净空持仓(f024n < 0,qty 取绝对值) */
netShort: PositionRow[] netShort: PositionRow[]
/** 盈利机构前 20(day_profit > 0) */
profitGain: ProfitRow[]
/** 亏损机构前 20(day_profit < 0) */
profitLoss: ProfitRow[]
/** 前 20 持仓量汇总 */ /** 前 20 持仓量汇总 */
topTwentySum: TopTwentySum[] topTwentySum: TopTwentySum[]
updatedAt: string updatedAt: string

View File

@ -33,3 +33,19 @@ export function extractVariety(contract: string): string {
const m = contract.match(/^([A-Za-z]+)/) const m = contract.match(/^([A-Za-z]+)/)
return m ? m[1] : contract return m ? m[1] : contract
} }
/**
* 最近一个月日期区间(含起止日),用于机构盈利等按区间查询的接口。
*/
export function getRecentMonthRange(from: Date = new Date()): {
startDate: string
endDate: string
} {
const end = new Date(from.getFullYear(), from.getMonth(), from.getDate())
const start = new Date(end)
start.setMonth(start.getMonth() - 1)
return {
startDate: formatDateYmd(start),
endDate: formatDateYmd(end),
}
}