主力追踪

This commit is contained in:
dongzp 2026-07-25 12:09:17 +08:00
parent 573d39696c
commit b4f790e185
9 changed files with 759 additions and 14 deletions

View File

@ -12,12 +12,19 @@
<section class="quote-section">
<QuoteStats v-if="quote" :quote="quote" />
<div class="quote-grid">
<ChartPanel
v-if="quote"
:quote="quote"
:kline-loading="klineLoading"
:load-kline="loadKline"
/>
<div class="chart-col">
<ChartPanel
v-if="quote"
:quote="quote"
:kline-loading="klineLoading"
:load-kline="loadKline"
/>
<MainForceTracePanel
:data="mainForceTrace"
:loading="mainForceTraceLoading"
:error="mainForceTraceError"
/>
</div>
<aside class="side">
<OrderBook v-if="quote" :quote="quote" />
<TradeTape v-if="quote" :quote="quote" />
@ -74,6 +81,7 @@ import AppLayout from './layout/AppLayout.vue'
import AppHeader from './components/header/AppHeader.vue'
import QuoteStats from './components/quote/QuoteStats.vue'
import ChartPanel from './components/quote/ChartPanel.vue'
import MainForceTracePanel from './components/quote/MainForceTracePanel.vue'
import OrderBook from './components/quote/OrderBook.vue'
import TradeTape from './components/quote/TradeTape.vue'
import LargeOrderAnalysis from './components/quote/LargeOrderAnalysis.vue'
@ -88,7 +96,13 @@ import { useAiAdvice } from './composables/useAiAdvice'
import { useQuotaStore } from './stores/quota'
const { data: quote, klineLoading, loadKline } = useQuote()
const { wsStatus, wsInSession } = storeToRefs(useQuotaStore())
const {
wsStatus,
wsInSession,
mainForceTrace,
mainForceTraceLoading,
mainForceTraceError,
} = storeToRefs(useQuotaStore())
const { data: news, loading: newsLoading, error: newsError, refresh: refreshNews } = useNews()
const { data: positions } = usePositions()
const { data: advice, loading: adviceLoading, refresh: refreshAdvice } = useAiAdvice()
@ -105,6 +119,12 @@ const positionOpen = ref(false)
align-items: start;
}
.chart-col {
min-width: 0;
display: flex;
flex-direction: column;
}
@media (max-width: 960px) {
.quote-grid {
grid-template-columns: 1fr;

View File

@ -19,7 +19,8 @@ export function buildAnalysisMessages(payload: AiAnalysisPayload): Array<{
}> {
const system = [
payload.keywords.trim() || '你是一名专业的国内期货交易员和分析员。',
'请仅基于用户提供的行情、盘口、新闻与机构持仓数据给出交易建议。',
'请仅基于用户提供的行情、盘口、新闻、机构持仓与主力追踪(变盘预警、主力趋势机会)数据给出交易建议。',
'主力追踪为全市场数据,请优先关注与当前合约品种相关的条目,并结合整体主力动向综合判断。',
'不要编造未提供的数据;信息不足时偏向观望并说明原因。',
'必须只输出一个 JSON 对象,不要 Markdown 代码块,不要其它说明文字。',
`JSON 格式:${OUTPUT_SCHEMA}`,
@ -38,7 +39,7 @@ export function buildAnalysisMessages(payload: AiAnalysisPayload): Array<{
/**
* 压缩超长数组,保留分析关键字段,避免 prompt 过大。
* 盘口、统计、持仓、新闻完整保留;分时/成交/K 线做尾部截断。
* 盘口、统计、持仓、新闻、主力追踪完整保留;分时/成交/K 线做尾部截断。
*/
function slimSnapshot(snapshot: AnalysisSnapshot): AnalysisSnapshot {
const quote = snapshot.quote

View File

@ -4,7 +4,7 @@ import type { AnalysisSnapshot } from '../../stores/quota'
export interface AiAnalysisPayload {
/** 设置中的角色/分析关键词 */
keywords: string
/** 行情 + 新闻 + 机构持仓等接口快照 */
/** 行情 + 新闻 + 机构持仓 + 主力追踪等接口快照 */
snapshot: AnalysisSnapshot
}

View File

@ -0,0 +1,108 @@
import type { ChangeRiskItem, MainForceTraceData, MainForceTrendItem } from '../../types'
import type { JqkaMainForceTraceData, JqkaVarietyPositionItem } from './types'
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):
* - 净持仓减少且多单也在减 → 主力净减多头
* - 净持仓减少但多单未减(空单增更多)→ 主力净增空头
* - 净持仓增加且空单也在减 → 主力净减空头
* - 其余净增 → 主力净增多头
* 手数取 |f025n|。
*/
export function buildChangeRiskAction(item: {
f013n: number
f019n: number
f025n: number
}): { actionLabel: string; actionLots: number } {
const lots = Math.abs(item.f025n)
if (item.f025n < 0) {
return {
actionLabel: item.f013n < 0 ? '主力净减多头' : '主力净增空头',
actionLots: lots,
}
}
if (item.f025n > 0) {
return {
actionLabel: item.f019n < 0 ? '主力净减空头' : '主力净增多头',
actionLots: lots,
}
}
return { actionLabel: '主力持仓不变', actionLots: 0 }
}
/** 主力趋势多空:看净增减 / 资金流向符号,与接口 type 无关 */
function resolveTrendSide(f025n: number, pupilFlow: number): 'long' | 'short' {
if (f025n !== 0) return f025n > 0 ? 'long' : 'short'
return pupilFlow >= 0 ? 'long' : 'short'
}
function mapChangeRisk(item: JqkaVarietyPositionItem): ChangeRiskItem {
const action = buildChangeRiskAction(item)
return {
variety: item.variety,
varietyName: item.variety_name,
date: item.date,
changeRisk: toNum(item.change_risk),
dayMood: toNum(item.day_mood),
percent: toNum(item.percent),
pupilFlow: toNum(item.pupil_flow),
linkClosePrice: toNum(item.link_close_price),
indexClosePrice: toNum(item.index_close_price),
linkSettlePrice: toNum(item.link_settle_price),
twentyDayAvg: toNum(item.twenty_day_avg),
maxFunds: toNum(item.max_funds),
longChange: item.f013n,
shortChange: item.f019n,
netQty: item.f024n,
netChange: item.f025n,
actionLabel: action.actionLabel,
actionLots: action.actionLots,
}
}
/**
* 将主力追踪接口映射为页面 / AI 用 MainForceTraceData。
* variety_position_list → 变盘预警;main_force_trend_list → 主力趋势机会。
*/
export function mapJqkaMainForceTrace(data: JqkaMainForceTraceData): MainForceTraceData {
const changeRiskList: ChangeRiskItem[] = (data.variety_position_list ?? [])
.map(mapChangeRisk)
.sort((a, b) => b.changeRisk - a.changeRisk)
const mainForceTrendList: MainForceTrendItem[] = (data.main_force_trend_list ?? []).map(
(item) => {
const netChange = item.f025n
const pupilFlow = toNum(item.pupil_flow)
return {
variety: item.variety,
varietyName: item.variety_name,
company: item.company,
logo: item.logo || '',
date: item.date,
rawType: item.type,
side: resolveTrendSide(netChange, pupilFlow),
mood: toNum(item.mood),
weekRate: toNum(item.week_rate),
yearProfit: toNum(item.year_profit),
pupilFlow,
percent: toNum(item.percent),
closePrice: toNum(item.close_price),
netChange,
}
},
)
const date =
changeRiskList[0]?.date ||
mainForceTrendList[0]?.date ||
new Date().toISOString().slice(0, 10)
return { changeRiskList, mainForceTrendList, date }
}

View File

@ -2,6 +2,8 @@ import { jqkaHttp } from '../http'
import type {
JqkaDealPositionData,
JqkaDealPositionResponse,
JqkaMainForceTraceData,
JqkaMainForceTraceResponse,
JqkaPositionProfitData,
JqkaPositionProfitRequest,
JqkaPositionProfitResponse,
@ -105,3 +107,38 @@ export async function getPositionProfitRank(
return data.data
}
export interface GetMainForceTraceParams {
/**
* 查询日期 YYYY-MM-DD。
* 应与机构持仓 date 一致;缺省走持仓默认日期规则。
*/
date?: string
}
/**
* 同花顺 — 主力追踪(变盘预警 + 主力趋势机会)
* GET /futgwapi/api/market/dragon_tiger/v1/main_force_trace?date=
*/
export async function getMainForceTrace(
params: GetMainForceTraceParams = {},
): Promise<JqkaMainForceTraceData> {
const date = params.date ?? getPositionQueryDate()
const { data } = await jqkaHttp.get<JqkaMainForceTraceResponse>(
'/futgwapi/api/market/dragon_tiger/v1/main_force_trace',
{
params: { date },
},
)
if (data.code !== 0) {
throw new Error(`主力追踪接口失败: code=${data.code}, msg=${data.msg}`)
}
if (!data.data) {
throw new Error('主力追踪接口返回空 data')
}
return data.data
}

View File

@ -77,3 +77,58 @@ export interface JqkaPositionProfitRequest {
/** type=company 时必填;variety 查询可省略 */
company?: string
}
/** 变盘预警(variety_position_list) */
export interface JqkaVarietyPositionItem {
variety: string
market: string
f003n: number
f007n: number
f009n: number
f013n: number
f015n: number
f019n: number
f024n: number
f025n: number
percent: string
variety_name: string
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
}
/** 主力趋势机会(main_force_trend_list) */
export interface JqkaMainForceTrendItem {
date: string
percent: string
variety: string
market: string
company: string
/** 1=多 / 其它=空(接口约定) */
type: string
mood: string
f025n: number
logo: string
close_price: string
variety_name: string
year_profit: string
week_rate: string
pupil_flow: string
}
export interface JqkaMainForceTraceData {
variety_position_list: JqkaVarietyPositionItem[]
main_force_trend_list: JqkaMainForceTrendItem[]
}
export interface JqkaMainForceTraceResponse {
code: number
msg: string
data: JqkaMainForceTraceData
}

View File

@ -0,0 +1,403 @@
<template>
<section class="main-force-trace card" v-loading="loading">
<div class="head">
<el-tabs v-model="tab">
<el-tab-pane label="变盘预警" name="risk" />
<el-tab-pane :label="trendTabLabel" name="trend" />
</el-tabs>
<span v-if="data?.date" class="updated">数据日期 {{ data.date }}</span>
</div>
<div v-if="error" class="error">主力追踪加载失败</div>
<!-- 变盘预警:对齐 H5 卡片信息 -->
<div v-else-if="tab === 'risk'" class="risk-list">
<div
v-for="row in changeRiskList"
:key="row.variety"
class="risk-row"
:class="{ current: isCurrent(row.variety) }"
>
<div class="risk-main">
<div class="name">{{ row.varietyName }}</div>
<div class="action">{{ row.actionLabel }}{{ row.actionLots }}手</div>
</div>
<div class="risk-price">
<div class="px">{{ formatPrice(row.linkClosePrice) }}</div>
<div :class="pctClass(row.percent)">{{ formatSignedPct(row.percent) }}</div>
</div>
<div class="risk-meter">
<div class="meter-label">变盘风险值</div>
<div class="meter-bar">
<div class="meter-fill" :style="{ width: `${row.changeRisk * 100}%` }" />
</div>
<div class="meter-pct" :class="riskClass(row.changeRisk)">
{{ formatPct(row.changeRisk) }}
</div>
</div>
</div>
<div v-if="!changeRiskList.length" class="empty">暂无变盘预警</div>
</div>
<!-- 主力趋势机会:按机构分组,对齐 H5 -->
<div v-else class="trend-list">
<div v-for="group in trendGroups" :key="group.company" class="trend-group">
<div class="group-head">
<img v-if="group.logo" :src="group.logo" alt="" class="logo" />
<span class="company">{{ group.company }}</span>
<span class="group-count">{{ group.items.length }}</span>
</div>
<div
v-for="row in group.items"
:key="`${row.company}-${row.variety}`"
class="trend-card"
:class="{ current: isCurrent(row.variety) }"
>
<div class="trend-top">
<span class="name">{{ row.varietyName }}</span>
<span :class="pctClass(row.percent)">{{ formatSignedPct(row.percent) }}</span>
<span class="flow-tag" :class="row.side === 'long' ? 'price-up' : 'price-down'">
单日{{ formatPct(row.mood) }}{{ row.side === 'long' ? '流多' : '流空' }}
</span>
</div>
<div class="trend-detail">
<div>
单日加{{ row.side === 'long' ? '多' : '空'
}}{{ Math.abs(row.netChange) }}手,流{{ row.side === 'long' ? '多' : '空'
}}{{ formatFlowAbs(row.pupilFlow) }}
</div>
<div>近一周胜率: {{ formatPct(row.weekRate) }}</div>
<div>近一年盈利: {{ formatProfitAbs(row.yearProfit) }}</div>
</div>
</div>
</div>
<div v-if="!trendGroups.length" class="empty">暂无主力趋势</div>
</div>
</section>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
import { storeToRefs } from 'pinia'
import type { MainForceTraceData, MainForceTrendItem } from '../../types'
import { useContractStore } from '../../stores/contract'
import { extractVariety } from '../../utils/tradingDate'
const props = defineProps<{
data: MainForceTraceData | null
loading?: boolean
error?: unknown
}>()
const tab = ref<'risk' | 'trend'>('risk')
const { current } = storeToRefs(useContractStore())
const currentVariety = computed(() => {
const v = current.value.variety || extractVariety(current.value.code)
return v.toUpperCase()
})
function isCurrent(variety: string) {
return variety.toUpperCase() === currentVariety.value
}
/** 当前合约品种置顶 */
function prioritizeCurrent<T extends { variety: string }>(list: T[]): T[] {
const key = currentVariety.value
if (!key) return list
const matched: T[] = []
const rest: T[] = []
for (const item of list) {
if (item.variety.toUpperCase() === key) matched.push(item)
else rest.push(item)
}
return [...matched, ...rest]
}
const changeRiskList = computed(() =>
prioritizeCurrent(props.data?.changeRiskList ?? []),
)
const trendTabLabel = computed(() => {
const n = props.data?.mainForceTrendList?.length ?? 0
return n > 0 ? `主力趋势机会 (${n}个)` : '主力趋势机会'
})
interface TrendGroup {
company: string
logo: string
items: MainForceTrendItem[]
}
/** 按机构分组,组内当前品种置顶;含当前品种的机构优先 */
const trendGroups = computed((): TrendGroup[] => {
const list = props.data?.mainForceTrendList ?? []
const map = new Map<string, TrendGroup>()
for (const item of list) {
let g = map.get(item.company)
if (!g) {
g = { company: item.company, logo: item.logo, items: [] }
map.set(item.company, g)
}
g.items.push(item)
}
const groups = [...map.values()].map((g) => ({
...g,
items: prioritizeCurrent(g.items),
}))
const key = currentVariety.value
if (!key) return groups
return groups.sort((a, b) => {
const aHit = a.items.some((x) => x.variety.toUpperCase() === key) ? 0 : 1
const bHit = b.items.some((x) => x.variety.toUpperCase() === key) ? 0 : 1
return aHit - bHit
})
})
function formatPct(n: number): string {
return `${(n * 100).toFixed(1)}%`
}
function formatSignedPct(n: number): string {
const pct = n * 100
if (Math.abs(pct) < 0.005) return '0.00%'
const sign = pct > 0 ? '+' : ''
return `${sign}${pct.toFixed(2)}%`
}
function pctClass(n: number): string {
if (n > 0) return 'price-up'
if (n < 0) return 'price-down'
return 'pct-flat'
}
function formatPrice(n: number): string {
if (!Number.isFinite(n) || n === 0) return '—'
return n >= 100 ? n.toFixed(0) : n.toFixed(2)
}
/** 资金流向绝对值(H5:流多3.1亿) */
function formatFlowAbs(n: number): string {
const abs = Math.abs(n)
if (abs >= 1e8) return `${(abs / 1e8).toFixed(1)}亿`
if (abs >= 1e4) return `${(abs / 1e4).toFixed(0)}万`
return abs.toFixed(0)
}
/** 年盈利绝对值(H5:5531.69万元 / 2.49亿元) */
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 riskClass(risk: number): string {
if (risk >= 0.8) return 'risk-high'
if (risk >= 0.5) return 'risk-mid'
return 'risk-low'
}
</script>
<style scoped>
.main-force-trace {
margin-top: 12px;
padding: 8px 14px 14px;
}
.head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.head :deep(.el-tabs__header) {
margin-bottom: 0;
}
.updated {
font-size: 12px;
color: var(--text-secondary, #8c8c8c);
flex-shrink: 0;
}
.error,
.empty {
padding: 20px;
font-size: 13px;
text-align: center;
color: var(--text-secondary, #8c8c8c);
}
.error {
color: var(--down, #e54545);
}
.risk-list {
display: flex;
flex-direction: column;
gap: 10px;
margin-top: 10px;
max-height: 360px;
overflow: auto;
}
.risk-row {
display: grid;
grid-template-columns: minmax(120px, 1.2fr) minmax(72px, 0.6fr) minmax(140px, 1fr);
gap: 12px;
align-items: center;
padding: 10px 12px;
border-radius: 8px;
background: var(--el-fill-color-lighter, #fafafa);
}
.risk-row.current,
.trend-card.current {
outline: 1px solid rgba(22, 119, 255, 0.35);
background: rgba(22, 119, 255, 0.04);
}
.name {
font-size: 15px;
font-weight: 600;
}
.action {
margin-top: 2px;
font-size: 12px;
color: var(--text-secondary, #8c8c8c);
}
.risk-price {
text-align: right;
}
.px {
font-size: 14px;
font-weight: 500;
}
.pct-flat {
color: var(--text-primary, #333);
}
.risk-meter {
display: grid;
grid-template-columns: auto 1fr auto;
gap: 8px;
align-items: center;
}
.meter-label {
font-size: 11px;
color: var(--text-secondary, #8c8c8c);
white-space: nowrap;
}
.meter-bar {
height: 8px;
border-radius: 4px;
background: #f0f0f0;
overflow: hidden;
}
.meter-fill {
height: 100%;
border-radius: 4px;
background: linear-gradient(90deg, #ff9c6e, #f5222d);
}
.meter-pct {
font-size: 13px;
font-weight: 600;
min-width: 48px;
text-align: right;
}
.risk-high {
color: #d4380d;
}
.risk-mid {
color: #d48806;
}
.risk-low {
color: var(--text-secondary, #8c8c8c);
}
.trend-list {
display: flex;
flex-direction: column;
gap: 14px;
margin-top: 10px;
max-height: 420px;
overflow: auto;
}
.group-head {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
}
.logo {
width: 20px;
height: 20px;
border-radius: 4px;
object-fit: cover;
}
.company {
font-size: 14px;
font-weight: 600;
}
.group-count {
font-size: 12px;
color: var(--text-secondary, #8c8c8c);
}
.trend-card {
padding: 10px 12px;
margin-bottom: 8px;
border-radius: 8px;
border: 1px solid var(--border, #eee);
}
.trend-top {
display: flex;
align-items: baseline;
gap: 12px;
flex-wrap: wrap;
}
.flow-tag {
margin-left: auto;
font-size: 13px;
font-weight: 600;
}
.trend-detail {
margin-top: 8px;
padding: 8px 10px;
border-radius: 6px;
background: var(--el-fill-color-light, #f5f7fa);
font-size: 12px;
line-height: 1.7;
color: var(--text-secondary, #666);
}
@media (max-width: 720px) {
.risk-row {
grid-template-columns: 1fr 1fr;
}
.risk-meter {
grid-column: 1 / -1;
}
}
</style>

View File

@ -1,6 +1,6 @@
import { defineStore } from 'pinia'
import { computed, ref, toRaw } from 'vue'
import type { Candle, NewsItem, PositionsData, QuoteData } from '../types'
import type { Candle, MainForceTraceData, NewsItem, PositionsData, QuoteData } from '../types'
import { useContractStore } from './contract'
import { getStockKline, getStockQuotation } from '../api/baidu/quotation'
import { getFuturesNews } from '../api/baidu/news'
@ -15,8 +15,13 @@ import type {
BaiduWsSnapshotData,
BaiduWsTickData,
} from '../api/baidu/wsTypes'
import { getDealPosition, getPositionProfitRank } from '../api/jqka/position'
import {
getDealPosition,
getMainForceTrace,
getPositionProfitRank,
} from '../api/jqka/position'
import { mapJqkaDealPosition, mapJqkaPositionProfit } from '../api/jqka/mapPosition'
import { mapJqkaMainForceTrace } from '../api/jqka/mapMainForceTrace'
import {
extractVariety,
getPositionQueryDate,
@ -45,6 +50,8 @@ export interface AnalysisSnapshot {
quote: QuoteData | null
news: NewsItem[]
positions: PositionsData | null
/** 变盘预警 + 主力趋势机会(全市场,各合约共用) */
mainForceTrace: MainForceTraceData | null
fetchedAt: string
}
@ -100,6 +107,14 @@ function emptyPositions(): PositionsData {
}
}
function emptyMainForceTrace(): MainForceTraceData {
return {
changeRiskList: [],
mainForceTrendList: [],
date: '',
}
}
function activeContract() {
return useContractStore().current
}
@ -220,18 +235,29 @@ export const useQuotaStore = defineStore('quota', () => {
const positionsLoading = ref(false)
const positionsError = ref<unknown>(null)
// ─── 主力追踪(变盘预警 + 主力趋势,全市场)─────────────
const mainForceTrace = ref<MainForceTraceData>(emptyMainForceTrace())
const mainForceTraceLoading = ref(false)
const mainForceTraceError = ref<unknown>(null)
/** 是否已有至少一次成功的行情拉取(可用于 AI 门禁) */
const hasQuote = computed(() => quote.value.last > 0 && quote.value.intraday.length > 0)
const hasNews = computed(() => news.value.length > 0)
/** 新闻 / 持仓是否已成功拉取过真实接口(有则 AI 刷新可跳过) */
const newsLoaded = ref(false)
const positionsLoaded = ref(false)
const mainForceTraceLoaded = ref(false)
const hasPositions = computed(
() =>
positions.value.long.length > 0 ||
positions.value.short.length > 0 ||
positions.value.volume.length > 0,
)
const hasMainForceTrace = computed(
() =>
mainForceTrace.value.changeRiskList.length > 0 ||
mainForceTrace.value.mainForceTrendList.length > 0,
)
async function fetchQuote() {
const c = activeContract()
@ -301,14 +327,17 @@ export const useQuotaStore = defineStore('quota', () => {
}
/**
* 拉取会员持仓 + 机构盈利。
* 拉取会员持仓 + 机构盈利 + 主力追踪。
* 持仓日期:交易日 16:00 后查当天,否则查上一交易日;
* 若遇节假日空数据则再往前最多试 5 个交易日。
* 主力追踪 date 与最终持仓 date 保持一致。
* 盈利默认最近一个月、按当前品种查询。
*/
async function fetchPositions() {
positionsLoading.value = true
positionsError.value = null
mainForceTraceLoading.value = true
mainForceTraceError.value = null
try {
const c = activeContract()
const contract = c.code
@ -342,11 +371,22 @@ export const useQuotaStore = defineStore('quota', () => {
positions.value = mapped
positionsLoaded.value = true
// 主力追踪与机构持仓使用同一 date(全市场,失败不阻断持仓)
try {
const traceRaw = await getMainForceTrace({ date })
mainForceTrace.value = mapJqkaMainForceTrace(traceRaw)
mainForceTraceLoaded.value = true
} catch (e) {
mainForceTraceError.value = e
console.error('[quota] 拉取主力追踪失败', e)
}
} catch (e) {
positionsError.value = e
console.error('[quota] 拉取持仓失败', e)
} finally {
positionsLoading.value = false
mainForceTraceLoading.value = false
}
}
@ -366,12 +406,15 @@ export const useQuotaStore = defineStore('quota', () => {
quote.value = emptyQuote()
news.value = []
positions.value = emptyPositions()
mainForceTrace.value = emptyMainForceTrace()
loadedKlines.value = { day: false, week: false, month: false }
newsLoaded.value = false
positionsLoaded.value = false
mainForceTraceLoaded.value = false
quoteError.value = null
newsError.value = null
positionsError.value = null
mainForceTraceError.value = null
await fetchAll()
}
@ -379,11 +422,14 @@ export const useQuotaStore = defineStore('quota', () => {
/**
* AI 分析前取数:行情每次重新拉取;
* 新闻 / 机构持仓若已有成功数据则跳过,避免重复请求。
* (主力追踪随持仓一并拉取,持仓已加载则跳过)
*/
async function fetchForAnalysis() {
const tasks: Promise<void>[] = [fetchQuote()]
if (!newsLoaded.value) tasks.push(fetchNews())
if (!positionsLoaded.value) tasks.push(fetchPositions())
if (!positionsLoaded.value || !mainForceTraceLoaded.value) {
tasks.push(fetchPositions())
}
await Promise.all(tasks)
}
@ -410,6 +456,9 @@ export const useQuotaStore = defineStore('quota', () => {
quote: quote.value ? clonePlain(quote.value) : null,
news: clonePlain(news.value),
positions: positions.value ? clonePlain(positions.value) : null,
mainForceTrace: hasMainForceTrace.value
? clonePlain(mainForceTrace.value)
: null,
fetchedAt: new Date().toISOString(),
}
}
@ -427,14 +476,19 @@ export const useQuotaStore = defineStore('quota', () => {
positions,
positionsLoading,
positionsError,
mainForceTrace,
mainForceTraceLoading,
mainForceTraceError,
wsStatus,
wsInSession,
// computed
hasQuote,
hasNews,
hasPositions,
hasMainForceTrace,
newsLoaded,
positionsLoaded,
mainForceTraceLoaded,
// actions
fetchQuote,
loadKline,

View File

@ -125,6 +125,73 @@ export interface PositionsData {
updatedAt: string
}
/** 变盘预警行(主力追踪 variety_position_list) */
export interface ChangeRiskItem {
variety: string
varietyName: string
date: string
/** 变盘风险 0~1 */
changeRisk: number
/** 日内情绪 0~1 */
dayMood: number
/** 涨跌幅(小数,如 -0.00088) */
percent: number
/** 散户资金流向 */
pupilFlow: number
linkClosePrice: number
indexClosePrice: number
linkSettlePrice: number
twentyDayAvg: number
maxFunds: number
/** 多单增减 f013n */
longChange: number
/** 空单增减 f019n */
shortChange: number
/** 净持仓 f024n */
netQty: number
/** 净持仓增减 f025n */
netChange: number
/**
* H5 文案动作:净减多头 / 净增空头 / 净增多头 / 净减空头
* 由 f025n + f013n 推导,手数取 |f025n|
*/
actionLabel: string
actionLots: number
}
/** 主力趋势机会行(main_force_trend_list) */
export interface MainForceTrendItem {
variety: string
varietyName: string
company: string
logo: string
date: string
/**
* 接口原始 type(1/3/5 等),非多空方向。
* 多空看 side(由 f025n / pupil_flow 符号决定)。
*/
rawType: string
/** 资金流向方向:long=流多 / short=流空 */
side: 'long' | 'short'
/** 情绪/流向强度 0~1,H5 展示为「单日 xx%流多/流空」 */
mood: number
/** 周胜率 0~1 */
weekRate: number
yearProfit: number
pupilFlow: number
percent: number
closePrice: number
/** 净持仓增减 f025n:>0 加多,<0 加空 */
netChange: number
}
/** 主力追踪(变盘预警 + 主力趋势机会),全市场,各合约共用 */
export interface MainForceTraceData {
changeRiskList: ChangeRiskItem[]
mainForceTrendList: MainForceTrendItem[]
date: string
}
export type AdviceDirection = 'long' | 'short' | 'neutral'
export interface AiAdvice {