新增机构主力分析

This commit is contained in:
dongzp 2026-07-25 12:56:18 +08:00
parent 3e2e2ffc90
commit d88fc616f3
10 changed files with 455 additions and 15 deletions

View File

@ -1,6 +1,8 @@
import type { AnalysisSnapshot } from '../../stores/quota'
import type {
ChangeRiskItem,
MainForceAnalysisData,
MainForceAnalysisItem,
MainForceTrendItem,
NewsItem,
PositionAnomalyItem,
@ -40,8 +42,8 @@ export function buildAnalysisMessages(payload: AiAnalysisPayload): Array<{
}> {
const system = [
payload.keywords.trim() || '你是一名专业的国内期货交易员和分析员。',
'请仅基于用户提供的行情、盘口、大单分析、新闻、机构持仓、主力追踪(变盘预警、主力趋势机会)与主力异动数据给出交易建议。',
'主力追踪与主力异动为全市场数据,请优先关注标注为「当前品种」的条目,并结合整体主力动向综合判断。',
'请仅基于用户提供的行情、盘口、大单分析、新闻、机构持仓、主力分析(盈利席位榜)、主力追踪(变盘预警、主力趋势机会)与主力异动数据给出交易建议。',
'主力分析为当前品种盈利席位榜;主力追踪与主力异动为全市场数据,请优先关注标注为「当前品种」的条目,并结合整体主力动向综合判断。',
'文案中的单位与页面一致(手、万手、万、亿、百分比);不要编造未提供的数据;信息不足时偏向观望并说明原因。',
'必须只输出一个 JSON 对象,不要 Markdown 代码块,不要其它说明文字。',
`JSON 格式:${OUTPUT_SCHEMA}`,
@ -195,10 +197,43 @@ function formatPositions(data: PositionsData | null): string {
'',
`亏损机构前${POSITION_TOP_N}名:`,
...formatProfitRows(data.profitLoss),
'',
'主力分析(盈利席位榜前20名):',
...formatMainForceAnalysis(data.mainForceAnalysis),
]
return lines.join('\n')
}
function formatMainForceAnalysis(data: MainForceAnalysisData | null | undefined): string[] {
if (!data?.list?.length) return ['(无)']
const lines = [
`品种净持仓 ${fmtSignedInt(data.varietyNetQty)}|净增减 ${fmtSignedInt(data.varietyNetChange)}|日内情绪 ${formatPct(data.varietyDayMood)}|变盘风险 ${formatPct(data.varietyChangeRisk)}`,
]
for (const row of data.list.slice(0, 20)) {
lines.push(formatMainForceAnalysisRow(row))
}
return lines
}
function formatMainForceAnalysisRow(row: MainForceAnalysisItem): string {
const action =
row.netChange === 0
? '持仓不变'
: row.netChange > 0
? `加多${Math.abs(row.netChange)}手`
: `加空${Math.abs(row.netChange)}手`
return [
`${row.rank}. ${row.name}`,
`净持仓 ${fmtSignedInt(row.netQty)}`,
action,
`近一年盈亏 ${formatProfitAbs(row.yearProfit)}`,
`一周胜率 ${formatPct(row.weekRate)}`,
`多空流向 ${formatPct(row.dayMood)}(${row.flowLabel})`,
`3日情绪 ${formatPct(row.threeDayMood)}|5日情绪 ${formatPct(row.fiveDayMood)}`,
`价差持仓 ${row.priceDiffContract || '无'}`,
].join('|')
}
function formatPositionRows(list: PositionRow[], qtyLabel: string): string[] {
const top = list.slice(0, POSITION_TOP_N)
if (!top.length) return ['(无)']

View File

@ -4,7 +4,7 @@ import type { AnalysisSnapshot } from '../../stores/quota'
export interface AiAnalysisPayload {
/** 设置中的角色/分析关键词 */
keywords: string
/** 行情 + 新闻 + 机构持仓 + 主力追踪 + 主力异动等接口快照 */
/** 行情 + 新闻 + 机构持仓(含主力分析)+ 主力追踪 + 主力异动等接口快照 */
snapshot: AnalysisSnapshot
/** 大单手数阈值,与 UI 大单分析一致 */
largeOrderLots: number

View File

@ -0,0 +1,91 @@
import type { MainForceAnalysisData, MainForceAnalysisItem } from '../../types'
import type {
JqkaMainForceAnalysisData,
JqkaMainForceAnalysisItem,
} from './types'
const TOP_N = 20
function toNum(v: string | number | null | undefined): number {
if (typeof v === 'number') return Number.isFinite(v) ? v : 0
if (v == null || v === '') return 0
const n = Number(v)
return Number.isFinite(n) ? n : 0
}
/**
* 多空流向文案(对齐同花顺 H5):
* - day_mood < 15% → 持仓平稳
* - < 40% → 小幅流多/流空
* - < 90% → 中度流多/流空
* - ≥ 90% → 大幅流多/流空
* 方向由净持仓增减 f025n 决定(>0 流多,<0 流空)。
*/
export function buildFlowLabel(
dayMood: number,
netChange: number,
): { flowSide: MainForceAnalysisItem['flowSide']; flowLabel: string } {
if (dayMood < 0.15 || netChange === 0) {
return { flowSide: 'flat', flowLabel: '持仓平稳' }
}
const intens =
dayMood >= 0.9 ? '大幅' : dayMood >= 0.4 ? '中度' : '小幅'
if (netChange > 0) {
return { flowSide: 'long', flowLabel: `${intens}流多` }
}
return { flowSide: 'short', flowLabel: `${intens}流空` }
}
function mapItem(
item: JqkaMainForceAnalysisItem,
rank: number,
): MainForceAnalysisItem {
const netChange = item.f025n
const dayMood = toNum(item.day_mood)
const flow = buildFlowLabel(dayMood, netChange)
const unitFunds = toNum(item.pre_pupil_flow)
return {
rank,
name: item.company,
netQty: item.f024n,
netChange,
changeFunds: netChange * unitFunds,
dayProfit: toNum(item.day_profit),
yearProfit: toNum(item.year_profit),
weekRate: toNum(item.week_rate),
dayMood,
flowSide: flow.flowSide,
flowLabel: flow.flowLabel,
threeDayMood: toNum(item.three_day_mood),
fiveDayMood: toNum(item.five_day_mood),
threeDayNetChange: item.three_day_f025n,
fiveDayNetChange: item.five_day_f025n,
priceDiffContract: (item.price_diff_contract || '').trim(),
date: item.date,
}
}
/**
* 主力分析接口 → 页面 / AI 用数据。
* 仅取 position_list 前 20 条(接口顺序即盈利席位榜顺序)。
*/
export function mapJqkaMainForceAnalysis(
data: JqkaMainForceAnalysisData,
): MainForceAnalysisData {
const list = (data.position_list ?? [])
.slice(0, TOP_N)
.map((item, i) => mapItem(item, i + 1))
const vp = data.variety_position
const date =
list[0]?.date || vp?.date || new Date().toISOString().slice(0, 10)
return {
list,
date,
varietyNetQty: vp?.f024n ?? 0,
varietyNetChange: vp?.f025n ?? 0,
varietyDayMood: toNum(vp?.day_mood),
varietyChangeRisk: toNum(vp?.change_risk),
}
}

View File

@ -103,6 +103,14 @@ export function mapJqkaDealPosition(data: JqkaDealPositionData): PositionsData {
netShort,
profitGain: [],
profitLoss: [],
mainForceAnalysis: {
list: [],
date: '',
varietyNetQty: 0,
varietyNetChange: 0,
varietyDayMood: 0,
varietyChangeRisk: 0,
},
topTwentySum,
updatedAt,
}

View File

@ -2,6 +2,8 @@ import { jqkaHttp } from '../http'
import type {
JqkaDealPositionData,
JqkaDealPositionResponse,
JqkaMainForceAnalysisData,
JqkaMainForceAnalysisResponse,
JqkaMainForceTraceData,
JqkaMainForceTraceResponse,
JqkaPositionProfitData,
@ -142,3 +144,44 @@ export async function getMainForceTrace(
return data.data
}
export interface GetMainForceAnalysisParams {
/** 品种代码,如 FG */
variety: string
/**
* 查询日期 YYYY-MM-DD。
* 应与机构持仓 date 一致;缺省走持仓默认日期规则。
*/
date?: string
}
/**
* 同花顺 — 主力分析(盈利席位榜)
* GET /futgwapi/api/market/dragon_tiger/v1/main_force_analysis?variety=&date=
*/
export async function getMainForceAnalysis(
params: GetMainForceAnalysisParams,
): Promise<JqkaMainForceAnalysisData> {
const variety = params.variety?.trim()
if (!variety) {
throw new Error('主力分析接口需要 variety')
}
const date = params.date ?? getPositionQueryDate()
const { data } = await jqkaHttp.get<JqkaMainForceAnalysisResponse>(
'/futgwapi/api/market/dragon_tiger/v1/main_force_analysis',
{
params: { variety, date },
},
)
if (data.code !== 0) {
throw new Error(`主力分析接口失败: code=${data.code}, msg=${data.msg}`)
}
if (!data.data?.position_list) {
throw new Error('主力分析接口返回空 position_list')
}
return data.data
}

View File

@ -132,3 +132,67 @@ export interface JqkaMainForceTraceResponse {
msg: string
data: JqkaMainForceTraceData
}
/** 主力分析 — 品种汇总(variety_position) */
export interface JqkaMainForceAnalysisVariety {
variety: string
market: string | null
f003n: number
f007n: number
f009n: number
f013n: number
f015n: number
f019n: number
f024n: number
f025n: number
percent: string
variety_name: string | null
date: string
day_mood: string
twenty_day_avg: string
change_risk: string
link_close_price: string
index_close_price: string
link_settle_price: string
max_funds: string
pupil_flow: string
}
/** 主力分析 — 机构席位行(position_list) */
export interface JqkaMainForceAnalysisItem {
variety: string
company: string
f003n: number
f007n: number
f009n: number
f013n: number
f015n: number
f019n: number
f024n: number
f025n: number
date: string
variety_name: string | null
day_profit: string
year_profit: string
week_rate: string
day_mood: string
three_day_mood: string
five_day_mood: string
three_day_f025n: number
five_day_f025n: number
max_funds: string
price_diff_contract: string
pre_pupil_flow: string
}
export interface JqkaMainForceAnalysisData {
type: string
variety_position: JqkaMainForceAnalysisVariety | null
position_list: JqkaMainForceAnalysisItem[]
}
export interface JqkaMainForceAnalysisResponse {
code: number
msg: string
data: JqkaMainForceAnalysisData
}

View File

@ -0,0 +1,118 @@
<template>
<el-table
:data="list"
size="small"
stripe
class="main-force-table"
max-height="520"
>
<el-table-column prop="rank" label="名次" width="56" />
<el-table-column prop="name" label="会员简称" min-width="120" />
<el-table-column prop="netQty" label="净持仓" min-width="100">
<template #default="{ row }">
<span :class="qtyClass(row.netQty)">{{ formatSignedInt(row.netQty) }}</span>
</template>
</el-table-column>
<el-table-column label="持仓变化" min-width="160">
<template #default="{ row }">
<span :class="changeClass(row.netChange)">
{{ formatPositionChange(row) }}
</span>
</template>
</el-table-column>
<el-table-column prop="yearProfit" label="近一年盈亏(估)" min-width="120">
<template #default="{ row }">
<span :class="row.yearProfit >= 0 ? 'price-up' : 'price-down'">
{{ formatProfitAbs(row.yearProfit) }}
</span>
</template>
</el-table-column>
<el-table-column prop="weekRate" label="一周胜率" width="90">
<template #default="{ row }">
{{ formatPct(row.weekRate) }}
</template>
</el-table-column>
<el-table-column label="多空流向值" min-width="140">
<template #default="{ row }">
<span :class="flowClass(row.flowSide)">
{{ formatPct(row.dayMood) }}({{ row.flowLabel }})
</span>
</template>
</el-table-column>
<el-table-column label="价差持仓" min-width="110">
<template #default="{ row }">
{{ row.priceDiffContract || '无' }}
</template>
</el-table-column>
</el-table>
</template>
<script setup lang="ts">
import type { MainForceAnalysisItem } from '../../types'
defineProps<{
list: MainForceAnalysisItem[]
}>()
function formatSignedInt(n: number): string {
if (!Number.isFinite(n)) return '—'
return `${n > 0 ? '+' : ''}${n}`
}
function formatPct(n: number): string {
return `${(n * 100).toFixed(1)}%`
}
function formatFunds(n: number): string {
const abs = Math.abs(n)
if (abs >= 1e8) return `${(abs / 1e8).toFixed(2)}亿`
if (abs >= 1e4) return `${(abs / 1e4).toFixed(2)}万`
return abs.toFixed(0)
}
function formatProfitAbs(n: number): string {
const abs = Math.abs(n)
if (abs >= 1e8) return `${(abs / 1e8).toFixed(2)}亿`
if (abs >= 1e4) return `${(abs / 1e4).toFixed(2)}万`
return `${abs.toFixed(0)}`
}
/** 加多 / 加空 + 手数 +(资金估算) */
function formatPositionChange(row: MainForceAnalysisItem): string {
if (row.netChange === 0) return '不变'
const action = row.netChange > 0 ? '加多' : '加空'
const lots = Math.abs(row.netChange)
const fundsAbs = formatFunds(Math.abs(row.changeFunds))
// 加空资金为负,加多为正(与同花顺 H5 一致)
const fundsText = row.netChange < 0 ? `(-${fundsAbs})` : `(${fundsAbs})`
return `${action} ${lots} ${fundsText}`
}
function qtyClass(n: number): string {
if (n > 0) return 'price-up'
if (n < 0) return 'price-down'
return ''
}
function changeClass(n: number): string {
if (n > 0) return 'price-up'
if (n < 0) return 'price-down'
return ''
}
function flowClass(side: MainForceAnalysisItem['flowSide']): string {
if (side === 'long') return 'price-up'
if (side === 'short') return 'price-down'
return 'flow-flat'
}
</script>
<style scoped>
.main-force-table {
width: 100%;
}
.flow-flat {
color: var(--text-secondary, #909399);
}
</style>

View File

@ -6,6 +6,7 @@
<el-tab-pane label="成交量" name="volume" />
<el-tab-pane label="净持仓" name="net" />
<el-tab-pane label="机构盈利" name="profit" />
<el-tab-pane label="主力分析" name="mainForce" />
</el-tabs>
<span v-if="data.updatedAt" class="updated">数据日期 {{ data.updatedAt }}</span>
</div>
@ -50,7 +51,7 @@
</div>
</div>
<div v-else class="cols">
<div v-else-if="tab === 'profit'" class="cols">
<div class="col">
<h3 class="long-title">盈利机构前20名</h3>
<PositionDonut :list="profitGainAsRows" />
@ -62,6 +63,13 @@
<ProfitTable :list="data.profitLoss" />
</div>
</div>
<div v-else class="cols single wide">
<div class="col">
<h3 class="volume-title">盈利席位榜前20名</h3>
<MainForceAnalysisTable :list="data.mainForceAnalysis?.list ?? []" />
</div>
</div>
</section>
</template>
@ -72,6 +80,7 @@ import PositionDonut from './PositionDonut.vue'
import PositionTable from './PositionTable.vue'
import PositionSumBar from './PositionSumBar.vue'
import ProfitTable from './ProfitTable.vue'
import MainForceAnalysisTable from './MainForceAnalysisTable.vue'
const props = defineProps<{
data: PositionsData
@ -131,6 +140,10 @@ const profitLossAsRows = computed(() => toDonutRows(props.data.profitLoss))
max-width: 560px;
}
.cols.single.wide {
max-width: none;
}
.long-title {
margin: 0 0 8px;
color: var(--long);

View File

@ -24,11 +24,13 @@ import type {
} from '../api/baidu/wsTypes'
import {
getDealPosition,
getMainForceAnalysis,
getMainForceTrace,
getPositionProfitRank,
} from '../api/jqka/position'
import { getPositionAnomaly } from '../api/jqka/anomaly'
import { mapJqkaDealPosition, mapJqkaPositionProfit } from '../api/jqka/mapPosition'
import { mapJqkaMainForceAnalysis } from '../api/jqka/mapMainForceAnalysis'
import { mapJqkaMainForceTrace } from '../api/jqka/mapMainForceTrace'
import { mapJqkaPositionAnomaly } from '../api/jqka/mapAnomaly'
import {
@ -113,6 +115,14 @@ function emptyPositions(): PositionsData {
netShort: [],
profitGain: [],
profitLoss: [],
mainForceAnalysis: {
list: [],
date: '',
varietyNetQty: 0,
varietyNetChange: 0,
varietyDayMood: 0,
varietyChangeRisk: 0,
},
topTwentySum: [],
updatedAt: '',
}
@ -365,10 +375,10 @@ export const useQuotaStore = defineStore('quota', () => {
}
/**
* 拉取会员持仓 + 机构盈利 + 主力追踪 + 主力异动。
* 拉取会员持仓 + 机构盈利 + 主力分析 + 主力追踪 + 主力异动。
* 持仓日期:交易日 16:00 后查当天,否则查上一交易日;
* 若遇节假日空数据则再往前最多试 5 个交易日。
* 主力追踪 / 主力异动 date 与最终持仓 date 保持一致。
* 主力追踪 / 主力异动 / 主力分析 date 与最终持仓 date 保持一致。
* 盈利默认最近一个月、按当前品种查询。
*/
async function fetchPositions() {
@ -400,14 +410,26 @@ export const useQuotaStore = defineStore('quota', () => {
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)
}
await Promise.all([
(async () => {
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)
}
})(),
(async () => {
try {
const analysisRaw = await getMainForceAnalysis({ variety, date })
mapped!.mainForceAnalysis = mapJqkaMainForceAnalysis(analysisRaw)
} catch (e) {
console.error('[quota] 拉取主力分析失败', e)
}
})(),
])
positions.value = mapped
positionsLoaded.value = true
@ -426,7 +448,7 @@ export const useQuotaStore = defineStore('quota', () => {
})(),
(async () => {
try {
const anomalyRaw = await getPositionAnomaly({ date, limit: 3 })
const anomalyRaw = await getPositionAnomaly({ date, limit: 5 })
positionAnomaly.value = mapJqkaPositionAnomaly(anomalyRaw)
positionAnomalyLoaded.value = true
} catch (e) {

View File

@ -98,6 +98,50 @@ export interface ProfitRow {
percent: number
}
/** 主力分析(盈利席位榜)行 */
export interface MainForceAnalysisItem {
rank: number
name: string
/** 净持仓 f024n */
netQty: number
/** 净持仓增减 f025n:>0 加多,<0 加空 */
netChange: number
/** 持仓变化资金估算(约等于 f025n × pre_pupil_flow) */
changeFunds: number
dayProfit: number
/** 近一年盈亏(估) */
yearProfit: number
/** 一周胜率 0~1 */
weekRate: number
/** 多空流向强度 0~1 */
dayMood: number
/** 流向方向:long=流多 / short=流空 / flat=平稳 */
flowSide: 'long' | 'short' | 'flat'
/** 如「中度流空」「持仓平稳」 */
flowLabel: string
threeDayMood: number
fiveDayMood: number
threeDayNetChange: number
fiveDayNetChange: number
/** 价差持仓原文;空则页面显示「无」 */
priceDiffContract: string
date: string
}
/** 主力分析(当前品种盈利席位榜前 20) */
export interface MainForceAnalysisData {
list: MainForceAnalysisItem[]
date: string
/** 品种净持仓 */
varietyNetQty: number
/** 品种净持仓增减 */
varietyNetChange: number
/** 品种日内情绪 0~1 */
varietyDayMood: number
/** 品种变盘风险 0~1 */
varietyChangeRisk: number
}
/** 前 20 多空持仓汇总(按交易日) */
export interface TopTwentySum {
tradeDate: string
@ -120,6 +164,8 @@ export interface PositionsData {
profitGain: ProfitRow[]
/** 亏损机构前 20(day_profit < 0) */
profitLoss: ProfitRow[]
/** 主力分析(盈利席位榜前 20 + 品种汇总) */
mainForceAnalysis: MainForceAnalysisData
/** 前 20 持仓量汇总 */
topTwentySum: TopTwentySum[]
updatedAt: string