查询机构品种盈利情况
This commit is contained in:
parent
f2a18522f8
commit
24a72bfd3f
@ -1,5 +1,9 @@
|
||||
import type { PositionRow, PositionsData, TopTwentySum } from '../../types'
|
||||
import type { JqkaDealPositionData, JqkaPositionItem } from './types'
|
||||
import type { PositionRow, PositionsData, ProfitRow, TopTwentySum } from '../../types'
|
||||
import type {
|
||||
JqkaDealPositionData,
|
||||
JqkaPositionItem,
|
||||
JqkaPositionProfitData,
|
||||
} from './types'
|
||||
|
||||
const TOP_N = 20
|
||||
|
||||
@ -91,7 +95,74 @@ export function mapJqkaDealPosition(data: JqkaDealPositionData): PositionsData {
|
||||
volume,
|
||||
netLong,
|
||||
netShort,
|
||||
profitGain: [],
|
||||
profitLoss: [],
|
||||
topTwentySum,
|
||||
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),
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,16 @@
|
||||
import { jqkaHttp } from '../http'
|
||||
import type { JqkaDealPositionData, JqkaDealPositionResponse } from './types'
|
||||
import { extractVariety, getPreviousTradingDate } from '../../utils/tradingDate'
|
||||
import type {
|
||||
JqkaDealPositionData,
|
||||
JqkaDealPositionResponse,
|
||||
JqkaPositionProfitData,
|
||||
JqkaPositionProfitRequest,
|
||||
JqkaPositionProfitResponse,
|
||||
} from './types'
|
||||
import {
|
||||
extractVariety,
|
||||
getPreviousTradingDate,
|
||||
getRecentMonthRange,
|
||||
} from '../../utils/tradingDate'
|
||||
|
||||
export interface GetDealPositionParams {
|
||||
/** 合约代码,如 FG609 */
|
||||
@ -42,3 +52,56 @@ export async function getDealPosition(
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@ -41,3 +41,39 @@ export interface JqkaDealPositionResponse {
|
||||
msg: string
|
||||
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
|
||||
}
|
||||
|
||||
@ -5,11 +5,11 @@
|
||||
<el-tab-pane label="总持仓" name="total" />
|
||||
<el-tab-pane label="成交量" name="volume" />
|
||||
<el-tab-pane label="净持仓" name="net" />
|
||||
<el-tab-pane label="机构盈利" name="profit" />
|
||||
</el-tabs>
|
||||
<span v-if="data.updatedAt" class="updated">数据日期 {{ data.updatedAt }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 总持仓:多空汇总柱状图 + 多 / 空前20 -->
|
||||
<div v-if="tab === 'total'">
|
||||
<PositionSumBar
|
||||
v-if="data.topTwentySum?.length"
|
||||
@ -29,7 +29,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 成交量:单列 -->
|
||||
<div v-else-if="tab === 'volume'" class="cols single">
|
||||
<div class="col">
|
||||
<h3 class="volume-title">成交量前20名</h3>
|
||||
@ -38,8 +37,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 净持仓:净多 / 净空 -->
|
||||
<div v-else class="cols">
|
||||
<div v-else-if="tab === 'net'" class="cols">
|
||||
<div class="col">
|
||||
<h3 class="long-title">净多持仓前20名</h3>
|
||||
<PositionDonut :list="data.netLong" />
|
||||
@ -51,21 +49,50 @@
|
||||
<PositionTable :list="data.netShort" qty-label="净持仓" />
|
||||
</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>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import type { PositionsData } from '../../types'
|
||||
import { computed, ref } from 'vue'
|
||||
import type { PositionRow, PositionsData } from '../../types'
|
||||
import PositionDonut from './PositionDonut.vue'
|
||||
import PositionTable from './PositionTable.vue'
|
||||
import PositionSumBar from './PositionSumBar.vue'
|
||||
import ProfitTable from './ProfitTable.vue'
|
||||
|
||||
defineProps<{
|
||||
const props = defineProps<{
|
||||
data: PositionsData
|
||||
}>()
|
||||
|
||||
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>
|
||||
|
||||
<style scoped>
|
||||
|
||||
57
src/components/position/ProfitTable.vue
Normal file
57
src/components/position/ProfitTable.vue
Normal 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>
|
||||
@ -1,4 +1,4 @@
|
||||
import type { PositionRow, PositionsData } from '../types'
|
||||
import type { PositionRow, PositionsData, ProfitRow } from '../types'
|
||||
|
||||
function makeList(names: string[], base: number): PositionRow[] {
|
||||
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 = [
|
||||
'中财期货',
|
||||
'东证期货',
|
||||
@ -73,6 +97,8 @@ 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))
|
||||
const profitGain = makeProfit(longNames.slice(0, 12), 2e8, 1)
|
||||
const profitLoss = makeProfit(shortNames.slice(0, 12), 1.5e8, -1)
|
||||
|
||||
export const positionsMock: PositionsData = {
|
||||
long,
|
||||
@ -80,6 +106,8 @@ export const positionsMock: PositionsData = {
|
||||
volume,
|
||||
netLong,
|
||||
netShort,
|
||||
profitGain,
|
||||
profitLoss,
|
||||
topTwentySum: [
|
||||
{ tradeDate: '2026-07-15', longSum: 1073446, shortSum: 1502020 },
|
||||
{ tradeDate: '2026-07-14', longSum: 1094199, shortSum: 1519906 },
|
||||
|
||||
@ -11,8 +11,8 @@ 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 { getDealPosition, getPositionProfitRank } from '../api/jqka/position'
|
||||
import { mapJqkaDealPosition, mapJqkaPositionProfit } from '../api/jqka/mapPosition'
|
||||
import {
|
||||
extractVariety,
|
||||
getPreviousTradingDate,
|
||||
@ -128,7 +128,9 @@ export const useQuotaStore = defineStore('quota', () => {
|
||||
}
|
||||
|
||||
/**
|
||||
* 拉取会员持仓。日期默认上一交易日;若遇节假日空数据则再往前最多试 5 个交易日。
|
||||
* 拉取会员持仓 + 机构盈利。
|
||||
* 持仓日期默认上一交易日;若遇节假日空数据则再往前最多试 5 个交易日。
|
||||
* 盈利默认最近一个月、按当前品种查询。
|
||||
*/
|
||||
async function fetchPositions() {
|
||||
positionsLoading.value = true
|
||||
@ -153,6 +155,16 @@ export const useQuotaStore = defineStore('quota', () => {
|
||||
if (!mapped) {
|
||||
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
|
||||
} catch (e) {
|
||||
positionsError.value = e
|
||||
|
||||
@ -82,6 +82,20 @@ export interface PositionRow {
|
||||
percent: number
|
||||
}
|
||||
|
||||
/** 机构盈利排行行(day_profit) */
|
||||
export interface ProfitRow {
|
||||
rank: number
|
||||
name: string
|
||||
/** 盈利金额(可正可负) */
|
||||
profit: number
|
||||
/** 净持仓 f024n */
|
||||
netQty: number
|
||||
/** 净持仓增减 f025n */
|
||||
change: number
|
||||
date: string
|
||||
percent: number
|
||||
}
|
||||
|
||||
/** 前 20 多空持仓汇总(按交易日) */
|
||||
export interface TopTwentySum {
|
||||
tradeDate: string
|
||||
@ -100,6 +114,10 @@ export interface PositionsData {
|
||||
netLong: PositionRow[]
|
||||
/** 净空持仓(f024n < 0,qty 取绝对值) */
|
||||
netShort: PositionRow[]
|
||||
/** 盈利机构前 20(day_profit > 0) */
|
||||
profitGain: ProfitRow[]
|
||||
/** 亏损机构前 20(day_profit < 0) */
|
||||
profitLoss: ProfitRow[]
|
||||
/** 前 20 持仓量汇总 */
|
||||
topTwentySum: TopTwentySum[]
|
||||
updatedAt: string
|
||||
|
||||
@ -33,3 +33,19 @@ export function extractVariety(contract: string): string {
|
||||
const m = contract.match(/^([A-Za-z]+)/)
|
||||
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),
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user