主力异动

This commit is contained in:
dongzp 2026-07-25 12:16:38 +08:00
parent b4f790e185
commit cf9b9c5d74
11 changed files with 533 additions and 18 deletions

View File

@ -24,6 +24,11 @@
:loading="mainForceTraceLoading" :loading="mainForceTraceLoading"
:error="mainForceTraceError" :error="mainForceTraceError"
/> />
<PositionAnomalyPanel
:data="positionAnomaly"
:loading="positionAnomalyLoading"
:error="positionAnomalyError"
/>
</div> </div>
<aside class="side"> <aside class="side">
<OrderBook v-if="quote" :quote="quote" /> <OrderBook v-if="quote" :quote="quote" />
@ -82,6 +87,7 @@ import AppHeader from './components/header/AppHeader.vue'
import QuoteStats from './components/quote/QuoteStats.vue' import QuoteStats from './components/quote/QuoteStats.vue'
import ChartPanel from './components/quote/ChartPanel.vue' import ChartPanel from './components/quote/ChartPanel.vue'
import MainForceTracePanel from './components/quote/MainForceTracePanel.vue' import MainForceTracePanel from './components/quote/MainForceTracePanel.vue'
import PositionAnomalyPanel from './components/quote/PositionAnomalyPanel.vue'
import OrderBook from './components/quote/OrderBook.vue' import OrderBook from './components/quote/OrderBook.vue'
import TradeTape from './components/quote/TradeTape.vue' import TradeTape from './components/quote/TradeTape.vue'
import LargeOrderAnalysis from './components/quote/LargeOrderAnalysis.vue' import LargeOrderAnalysis from './components/quote/LargeOrderAnalysis.vue'
@ -102,6 +108,9 @@ const {
mainForceTrace, mainForceTrace,
mainForceTraceLoading, mainForceTraceLoading,
mainForceTraceError, mainForceTraceError,
positionAnomaly,
positionAnomalyLoading,
positionAnomalyError,
} = storeToRefs(useQuotaStore()) } = storeToRefs(useQuotaStore())
const { data: news, loading: newsLoading, error: newsError, refresh: refreshNews } = useNews() const { data: news, loading: newsLoading, error: newsError, refresh: refreshNews } = useNews()
const { data: positions } = usePositions() const { data: positions } = usePositions()

View File

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

View File

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

View File

@ -12,6 +12,12 @@ export const jqkaHttp = axios.create({
timeout: 15000, timeout: 15000,
}) })
/** 开发环境走 Vite 代理 /dq → https://dq.10jqka.com.cn */
export const jqkaDqHttp = axios.create({
baseURL: '/dq',
timeout: 15000,
})
/** 开发环境走 Vite 代理 /deepseek → https://api.deepseek.com */ /** 开发环境走 Vite 代理 /deepseek → https://api.deepseek.com */
export const deepseekHttp = axios.create({ export const deepseekHttp = axios.create({
baseURL: '/deepseek', baseURL: '/deepseek',

43
src/api/jqka/anomaly.ts Normal file
View File

@ -0,0 +1,43 @@
import { jqkaDqHttp } from '../http'
import type { JqkaPositionAnomalyData, JqkaPositionAnomalyResponse } from './anomalyTypes'
import { getPositionQueryDate } from '../../utils/tradingDate'
export interface GetPositionAnomalyParams {
/**
* 查询日期 YYYY-MM-DD。
* 应与机构持仓 date 一致;缺省走持仓默认日期规则。
*/
date?: string
/** 每类排行条数,默认 3 */
limit?: number
}
/**
* 同花顺 dq — 主力异动排行
* GET /fuyao/futures_ds_comps/f10/v1/position_anomaly?date=&limit=
*/
export async function getPositionAnomaly(
params: GetPositionAnomalyParams = {},
): Promise<JqkaPositionAnomalyData> {
const date = params.date ?? getPositionQueryDate()
const limit = params.limit ?? 3
const { data } = await jqkaDqHttp.get<JqkaPositionAnomalyResponse>(
'/fuyao/futures_ds_comps/f10/v1/position_anomaly',
{
params: { date, limit },
},
)
if (data.status_code !== 0) {
throw new Error(
`主力异动接口失败: status_code=${data.status_code}, msg=${data.status_msg}`,
)
}
if (!data.data) {
throw new Error('主力异动接口返回空 data')
}
return data.data
}

View File

@ -0,0 +1,26 @@
/** 同花顺 dq — 主力异动排行单行 */
export interface JqkaPositionAnomalyItem {
net_position_change: number
bar: number
variety: string
net_position: number
max_company_name: string
name: string
net_position_change_rate: number
}
export interface JqkaPositionAnomalyData {
date: string
net_long_increase: JqkaPositionAnomalyItem[]
net_short_increase: JqkaPositionAnomalyItem[]
net_long_decrease: JqkaPositionAnomalyItem[]
net_short_decrease: JqkaPositionAnomalyItem[]
short_to_long: JqkaPositionAnomalyItem[]
long_to_short: JqkaPositionAnomalyItem[]
}
export interface JqkaPositionAnomalyResponse {
status_code: number
status_msg: string
data: JqkaPositionAnomalyData
}

View File

@ -0,0 +1,56 @@
import type { PositionAnomalyData, PositionAnomalyItem, PositionAnomalySection } from '../../types'
import type { JqkaPositionAnomalyData, JqkaPositionAnomalyItem } from './anomalyTypes'
/** 展示顺序:净多增 → 净空增 → 净多减 → 净空减 → 空转多 → 多转空 */
export const POSITION_ANOMALY_SECTIONS: Array<{
key: keyof Omit<PositionAnomalyData, 'date'>
apiKey: keyof Omit<JqkaPositionAnomalyData, 'date'>
title: string
}> = [
{ key: 'netLongIncrease', apiKey: 'net_long_increase', title: '净多增仓量排行' },
{ key: 'netShortIncrease', apiKey: 'net_short_increase', title: '净空增仓量排行' },
{ key: 'netLongDecrease', apiKey: 'net_long_decrease', title: '净多减仓量排行' },
{ key: 'netShortDecrease', apiKey: 'net_short_decrease', title: '净空减仓量排行' },
{ key: 'shortToLong', apiKey: 'short_to_long', title: '空转多排行' },
{ key: 'longToShort', apiKey: 'long_to_short', title: '多转空排行' },
]
function mapItem(item: JqkaPositionAnomalyItem): PositionAnomalyItem {
return {
variety: item.variety,
varietyName: item.name,
maxCompanyName: item.max_company_name,
netPosition: item.net_position,
netPositionChange: item.net_position_change,
netPositionChangeRate: item.net_position_change_rate,
bar: item.bar,
}
}
function mapList(list: JqkaPositionAnomalyItem[] | undefined): PositionAnomalyItem[] {
return (list ?? []).map(mapItem)
}
/**
* 将主力异动接口映射为页面 / AI 用 PositionAnomalyData。
*/
export function mapJqkaPositionAnomaly(data: JqkaPositionAnomalyData): PositionAnomalyData {
return {
date: data.date || new Date().toISOString().slice(0, 10),
netLongIncrease: mapList(data.net_long_increase),
netShortIncrease: mapList(data.net_short_increase),
netLongDecrease: mapList(data.net_long_decrease),
netShortDecrease: mapList(data.net_short_decrease),
shortToLong: mapList(data.short_to_long),
longToShort: mapList(data.long_to_short),
}
}
/** 按固定顺序展开为带标题的分区(供 UI / Prompt 使用) */
export function toAnomalySections(data: PositionAnomalyData): PositionAnomalySection[] {
return POSITION_ANOMALY_SECTIONS.map(({ key, title }) => ({
key,
title,
items: data[key],
}))
}

View File

@ -0,0 +1,260 @@
<template>
<section class="position-anomaly card" v-loading="loading">
<div class="head">
<h3 class="title">主力异动</h3>
<span v-if="data?.date" class="updated">数据日期 {{ data.date }}</span>
</div>
<div v-if="error" class="error">主力异动加载失败</div>
<div v-else class="sections">
<div v-for="section in sections" :key="section.key" class="section">
<h4 class="section-title">{{ section.title }}</h4>
<div v-if="!section.items.length" class="empty">暂无数据</div>
<div
v-for="(row, idx) in section.items"
:key="`${section.key}-${row.variety}`"
class="row"
:class="{ current: isCurrent(row.variety) }"
>
<span class="rank">{{ idx + 1 }}</span>
<div class="meta">
<div class="name-line">
<span class="name">{{ row.varietyName }}</span>
<span class="code">{{ row.variety }}</span>
</div>
<div class="company">主导 {{ row.maxCompanyName }}</div>
<div class="bar-track">
<div
class="bar-fill"
:class="barTone(section.key)"
:style="{ width: `${Math.min(100, Math.max(0, row.bar))}%` }"
/>
</div>
</div>
<div class="nums">
<div :class="changeClass(row.netPositionChange)">
{{ formatChange(row.netPositionChange) }}
</div>
<div class="rate" :class="changeClass(row.netPositionChangeRate)">
{{ formatRate(row.netPositionChangeRate) }}
</div>
<div class="net">净持仓 {{ formatChange(row.netPosition) }}</div>
</div>
</div>
</div>
</div>
</section>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { storeToRefs } from 'pinia'
import type { PositionAnomalyData } from '../../types'
import { toAnomalySections } from '../../api/jqka/mapAnomaly'
import { useContractStore } from '../../stores/contract'
import { extractVariety } from '../../utils/tradingDate'
const props = defineProps<{
data: PositionAnomalyData | null
loading?: boolean
error?: unknown
}>()
const { current } = storeToRefs(useContractStore())
const currentVariety = computed(() => {
const v = current.value.variety || extractVariety(current.value.code)
return v.toUpperCase()
})
const sections = computed(() =>
props.data ? toAnomalySections(props.data) : [],
)
function isCurrent(variety: string) {
return variety.toUpperCase() === currentVariety.value
}
function formatChange(n: number): string {
return `${n > 0 ? '+' : ''}${n}`
}
function formatRate(n: number): string {
const sign = n > 0 ? '+' : ''
return `${sign}${n.toFixed(1)}%`
}
function changeClass(n: number): string {
if (n > 0) return 'price-up'
if (n < 0) return 'price-down'
return ''
}
/** 增仓类偏红,减仓/转空偏绿 */
function barTone(key: string): string {
if (
key === 'netLongIncrease' ||
key === 'shortToLong' ||
key === 'netShortDecrease'
) {
return 'tone-long'
}
return 'tone-short'
}
</script>
<style scoped>
.position-anomaly {
margin-top: 12px;
padding: 12px 14px 14px;
}
.head {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 12px;
margin-bottom: 10px;
}
.title {
margin: 0;
font-size: 15px;
font-weight: 600;
}
.updated {
font-size: 12px;
color: var(--text-secondary, #8c8c8c);
}
.error,
.empty {
padding: 12px;
font-size: 13px;
text-align: center;
color: var(--text-secondary, #8c8c8c);
}
.error {
color: var(--down, #e54545);
}
.sections {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 12px;
}
@media (max-width: 1100px) {
.sections {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 720px) {
.sections {
grid-template-columns: 1fr;
}
}
.section {
padding: 10px;
border-radius: 8px;
background: var(--el-fill-color-lighter, #fafafa);
}
.section-title {
margin: 0 0 8px;
font-size: 13px;
font-weight: 600;
}
.row {
display: grid;
grid-template-columns: 22px 1fr auto;
gap: 8px;
align-items: start;
padding: 8px 0;
border-top: 1px solid var(--border, #eee);
}
.row:first-of-type {
border-top: none;
}
.row.current {
margin: 0 -6px;
padding-left: 6px;
padding-right: 6px;
border-radius: 6px;
background: rgba(22, 119, 255, 0.06);
}
.rank {
font-size: 12px;
font-weight: 600;
color: var(--text-secondary, #8c8c8c);
line-height: 20px;
}
.name-line {
display: flex;
align-items: baseline;
gap: 4px;
}
.name {
font-size: 13px;
font-weight: 600;
}
.code {
font-size: 11px;
color: var(--text-secondary, #8c8c8c);
}
.company {
margin-top: 2px;
font-size: 11px;
color: var(--text-secondary, #8c8c8c);
}
.bar-track {
margin-top: 6px;
height: 6px;
border-radius: 3px;
background: #eee;
overflow: hidden;
}
.bar-fill {
height: 100%;
border-radius: 3px;
}
.tone-long {
background: linear-gradient(90deg, #ffa39e, #f5222d);
}
.tone-short {
background: linear-gradient(90deg, #95de64, #12b37b);
}
.nums {
text-align: right;
font-size: 12px;
line-height: 1.45;
white-space: nowrap;
}
.rate {
font-weight: 500;
}
.net {
color: var(--text-secondary, #8c8c8c);
font-size: 11px;
}
</style>

View File

@ -1,6 +1,13 @@
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { computed, ref, toRaw } from 'vue' import { computed, ref, toRaw } from 'vue'
import type { Candle, MainForceTraceData, NewsItem, PositionsData, QuoteData } from '../types' import type {
Candle,
MainForceTraceData,
NewsItem,
PositionAnomalyData,
PositionsData,
QuoteData,
} from '../types'
import { useContractStore } from './contract' import { useContractStore } from './contract'
import { getStockKline, getStockQuotation } from '../api/baidu/quotation' import { getStockKline, getStockQuotation } from '../api/baidu/quotation'
import { getFuturesNews } from '../api/baidu/news' import { getFuturesNews } from '../api/baidu/news'
@ -20,8 +27,10 @@ import {
getMainForceTrace, getMainForceTrace,
getPositionProfitRank, getPositionProfitRank,
} from '../api/jqka/position' } from '../api/jqka/position'
import { getPositionAnomaly } from '../api/jqka/anomaly'
import { mapJqkaDealPosition, mapJqkaPositionProfit } from '../api/jqka/mapPosition' import { mapJqkaDealPosition, mapJqkaPositionProfit } from '../api/jqka/mapPosition'
import { mapJqkaMainForceTrace } from '../api/jqka/mapMainForceTrace' import { mapJqkaMainForceTrace } from '../api/jqka/mapMainForceTrace'
import { mapJqkaPositionAnomaly } from '../api/jqka/mapAnomaly'
import { import {
extractVariety, extractVariety,
getPositionQueryDate, getPositionQueryDate,
@ -52,6 +61,8 @@ export interface AnalysisSnapshot {
positions: PositionsData | null positions: PositionsData | null
/** 变盘预警 + 主力趋势机会(全市场,各合约共用) */ /** 变盘预警 + 主力趋势机会(全市场,各合约共用) */
mainForceTrace: MainForceTraceData | null mainForceTrace: MainForceTraceData | null
/** 主力异动六类排行(全市场,各合约共用) */
positionAnomaly: PositionAnomalyData | null
fetchedAt: string fetchedAt: string
} }
@ -115,6 +126,18 @@ function emptyMainForceTrace(): MainForceTraceData {
} }
} }
function emptyPositionAnomaly(): PositionAnomalyData {
return {
date: '',
netLongIncrease: [],
netShortIncrease: [],
netLongDecrease: [],
netShortDecrease: [],
shortToLong: [],
longToShort: [],
}
}
function activeContract() { function activeContract() {
return useContractStore().current return useContractStore().current
} }
@ -240,6 +263,11 @@ export const useQuotaStore = defineStore('quota', () => {
const mainForceTraceLoading = ref(false) const mainForceTraceLoading = ref(false)
const mainForceTraceError = ref<unknown>(null) const mainForceTraceError = ref<unknown>(null)
// ─── 主力异动(六类排行,全市场)─────────────────────────
const positionAnomaly = ref<PositionAnomalyData>(emptyPositionAnomaly())
const positionAnomalyLoading = ref(false)
const positionAnomalyError = ref<unknown>(null)
/** 是否已有至少一次成功的行情拉取(可用于 AI 门禁) */ /** 是否已有至少一次成功的行情拉取(可用于 AI 门禁) */
const hasQuote = computed(() => quote.value.last > 0 && quote.value.intraday.length > 0) const hasQuote = computed(() => quote.value.last > 0 && quote.value.intraday.length > 0)
const hasNews = computed(() => news.value.length > 0) const hasNews = computed(() => news.value.length > 0)
@ -247,6 +275,7 @@ export const useQuotaStore = defineStore('quota', () => {
const newsLoaded = ref(false) const newsLoaded = ref(false)
const positionsLoaded = ref(false) const positionsLoaded = ref(false)
const mainForceTraceLoaded = ref(false) const mainForceTraceLoaded = ref(false)
const positionAnomalyLoaded = ref(false)
const hasPositions = computed( const hasPositions = computed(
() => () =>
positions.value.long.length > 0 || positions.value.long.length > 0 ||
@ -258,6 +287,15 @@ export const useQuotaStore = defineStore('quota', () => {
mainForceTrace.value.changeRiskList.length > 0 || mainForceTrace.value.changeRiskList.length > 0 ||
mainForceTrace.value.mainForceTrendList.length > 0, mainForceTrace.value.mainForceTrendList.length > 0,
) )
const hasPositionAnomaly = computed(
() =>
positionAnomaly.value.netLongIncrease.length > 0 ||
positionAnomaly.value.netShortIncrease.length > 0 ||
positionAnomaly.value.netLongDecrease.length > 0 ||
positionAnomaly.value.netShortDecrease.length > 0 ||
positionAnomaly.value.shortToLong.length > 0 ||
positionAnomaly.value.longToShort.length > 0,
)
async function fetchQuote() { async function fetchQuote() {
const c = activeContract() const c = activeContract()
@ -327,10 +365,10 @@ export const useQuotaStore = defineStore('quota', () => {
} }
/** /**
* 拉取会员持仓 + 机构盈利 + 主力追踪。 * 拉取会员持仓 + 机构盈利 + 主力追踪 + 主力异动。
* 持仓日期:交易日 16:00 后查当天,否则查上一交易日; * 持仓日期:交易日 16:00 后查当天,否则查上一交易日;
* 若遇节假日空数据则再往前最多试 5 个交易日。 * 若遇节假日空数据则再往前最多试 5 个交易日。
* 主力追踪 date 与最终持仓 date 保持一致。 * 主力追踪 / 主力异动 date 与最终持仓 date 保持一致。
* 盈利默认最近一个月、按当前品种查询。 * 盈利默认最近一个月、按当前品种查询。
*/ */
async function fetchPositions() { async function fetchPositions() {
@ -338,6 +376,8 @@ export const useQuotaStore = defineStore('quota', () => {
positionsError.value = null positionsError.value = null
mainForceTraceLoading.value = true mainForceTraceLoading.value = true
mainForceTraceError.value = null mainForceTraceError.value = null
positionAnomalyLoading.value = true
positionAnomalyError.value = null
try { try {
const c = activeContract() const c = activeContract()
const contract = c.code const contract = c.code
@ -372,7 +412,9 @@ export const useQuotaStore = defineStore('quota', () => {
positions.value = mapped positions.value = mapped
positionsLoaded.value = true positionsLoaded.value = true
// 主力追踪与机构持仓使用同一 date(全市场,失败不阻断持仓) // 全市场数据与机构持仓使用同一 date(失败不阻断持仓)
await Promise.all([
(async () => {
try { try {
const traceRaw = await getMainForceTrace({ date }) const traceRaw = await getMainForceTrace({ date })
mainForceTrace.value = mapJqkaMainForceTrace(traceRaw) mainForceTrace.value = mapJqkaMainForceTrace(traceRaw)
@ -381,12 +423,25 @@ export const useQuotaStore = defineStore('quota', () => {
mainForceTraceError.value = e mainForceTraceError.value = e
console.error('[quota] 拉取主力追踪失败', e) console.error('[quota] 拉取主力追踪失败', e)
} }
})(),
(async () => {
try {
const anomalyRaw = await getPositionAnomaly({ date, limit: 3 })
positionAnomaly.value = mapJqkaPositionAnomaly(anomalyRaw)
positionAnomalyLoaded.value = true
} catch (e) {
positionAnomalyError.value = e
console.error('[quota] 拉取主力异动失败', e)
}
})(),
])
} catch (e) { } catch (e) {
positionsError.value = e positionsError.value = e
console.error('[quota] 拉取持仓失败', e) console.error('[quota] 拉取持仓失败', e)
} finally { } finally {
positionsLoading.value = false positionsLoading.value = false
mainForceTraceLoading.value = false mainForceTraceLoading.value = false
positionAnomalyLoading.value = false
} }
} }
@ -407,14 +462,17 @@ export const useQuotaStore = defineStore('quota', () => {
news.value = [] news.value = []
positions.value = emptyPositions() positions.value = emptyPositions()
mainForceTrace.value = emptyMainForceTrace() mainForceTrace.value = emptyMainForceTrace()
positionAnomaly.value = emptyPositionAnomaly()
loadedKlines.value = { day: false, week: false, month: false } loadedKlines.value = { day: false, week: false, month: false }
newsLoaded.value = false newsLoaded.value = false
positionsLoaded.value = false positionsLoaded.value = false
mainForceTraceLoaded.value = false mainForceTraceLoaded.value = false
positionAnomalyLoaded.value = false
quoteError.value = null quoteError.value = null
newsError.value = null newsError.value = null
positionsError.value = null positionsError.value = null
mainForceTraceError.value = null mainForceTraceError.value = null
positionAnomalyError.value = null
await fetchAll() await fetchAll()
} }
@ -422,12 +480,16 @@ export const useQuotaStore = defineStore('quota', () => {
/** /**
* AI 分析前取数:行情每次重新拉取; * AI 分析前取数:行情每次重新拉取;
* 新闻 / 机构持仓若已有成功数据则跳过,避免重复请求。 * 新闻 / 机构持仓若已有成功数据则跳过,避免重复请求。
* (主力追踪随持仓一并拉取,持仓已加载则跳过) * (主力追踪 / 主力异动随持仓一并拉取)
*/ */
async function fetchForAnalysis() { async function fetchForAnalysis() {
const tasks: Promise<void>[] = [fetchQuote()] const tasks: Promise<void>[] = [fetchQuote()]
if (!newsLoaded.value) tasks.push(fetchNews()) if (!newsLoaded.value) tasks.push(fetchNews())
if (!positionsLoaded.value || !mainForceTraceLoaded.value) { if (
!positionsLoaded.value ||
!mainForceTraceLoaded.value ||
!positionAnomalyLoaded.value
) {
tasks.push(fetchPositions()) tasks.push(fetchPositions())
} }
await Promise.all(tasks) await Promise.all(tasks)
@ -459,6 +521,9 @@ export const useQuotaStore = defineStore('quota', () => {
mainForceTrace: hasMainForceTrace.value mainForceTrace: hasMainForceTrace.value
? clonePlain(mainForceTrace.value) ? clonePlain(mainForceTrace.value)
: null, : null,
positionAnomaly: hasPositionAnomaly.value
? clonePlain(positionAnomaly.value)
: null,
fetchedAt: new Date().toISOString(), fetchedAt: new Date().toISOString(),
} }
} }
@ -479,6 +544,9 @@ export const useQuotaStore = defineStore('quota', () => {
mainForceTrace, mainForceTrace,
mainForceTraceLoading, mainForceTraceLoading,
mainForceTraceError, mainForceTraceError,
positionAnomaly,
positionAnomalyLoading,
positionAnomalyError,
wsStatus, wsStatus,
wsInSession, wsInSession,
// computed // computed
@ -486,9 +554,11 @@ export const useQuotaStore = defineStore('quota', () => {
hasNews, hasNews,
hasPositions, hasPositions,
hasMainForceTrace, hasMainForceTrace,
hasPositionAnomaly,
newsLoaded, newsLoaded,
positionsLoaded, positionsLoaded,
mainForceTraceLoaded, mainForceTraceLoaded,
positionAnomalyLoaded,
// actions // actions
fetchQuote, fetchQuote,
loadKline, loadKline,

View File

@ -192,6 +192,45 @@ export interface MainForceTraceData {
date: string date: string
} }
/** 主力异动排行行 */
export interface PositionAnomalyItem {
variety: string
varietyName: string
/** 增/减仓量最大的会员 */
maxCompanyName: string
/** 当前净持仓 */
netPosition: number
/** 净持仓增减 */
netPositionChange: number
/** 净持仓增减幅度(%) */
netPositionChangeRate: number
/** 相对条形图比例 0~100 */
bar: number
}
/** 主力异动六类排行(全市场,各合约共用) */
export interface PositionAnomalyData {
date: string
/** 净多增仓量排行 */
netLongIncrease: PositionAnomalyItem[]
/** 净空增仓量排行 */
netShortIncrease: PositionAnomalyItem[]
/** 净多减仓量排行 */
netLongDecrease: PositionAnomalyItem[]
/** 净空减仓量排行 */
netShortDecrease: PositionAnomalyItem[]
/** 空转多排行 */
shortToLong: PositionAnomalyItem[]
/** 多转空排行 */
longToShort: PositionAnomalyItem[]
}
export interface PositionAnomalySection {
key: keyof Omit<PositionAnomalyData, 'date'>
title: string
items: PositionAnomalyItem[]
}
export type AdviceDirection = 'long' | 'short' | 'neutral' export type AdviceDirection = 'long' | 'short' | 'neutral'
export interface AiAdvice { export interface AiAdvice {

View File

@ -41,6 +41,12 @@ export default defineConfig({
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
}, },
}, },
/** 同花顺 dq 数据源(主力异动等);须排在 /jqka 前,避免前缀被吞 */
'/dq': {
target: 'https://dq.10jqka.com.cn',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/dq/, ''),
},
'/jqka': { '/jqka': {
target: 'https://fupage.10jqka.com.cn', target: 'https://fupage.10jqka.com.cn',
changeOrigin: true, changeOrigin: true,