主力追踪
This commit is contained in:
parent
573d39696c
commit
b4f790e185
34
src/App.vue
34
src/App.vue
@ -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;
|
||||
|
||||
@ -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
|
||||
|
||||
@ -4,7 +4,7 @@ import type { AnalysisSnapshot } from '../../stores/quota'
|
||||
export interface AiAnalysisPayload {
|
||||
/** 设置中的角色/分析关键词 */
|
||||
keywords: string
|
||||
/** 行情 + 新闻 + 机构持仓等接口快照 */
|
||||
/** 行情 + 新闻 + 机构持仓 + 主力追踪等接口快照 */
|
||||
snapshot: AnalysisSnapshot
|
||||
}
|
||||
|
||||
|
||||
108
src/api/jqka/mapMainForceTrace.ts
Normal file
108
src/api/jqka/mapMainForceTrace.ts
Normal 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 }
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
403
src/components/quote/MainForceTracePanel.vue
Normal file
403
src/components/quote/MainForceTracePanel.vue
Normal 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>
|
||||
@ -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,
|
||||
|
||||
@ -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 {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user