主力异动
This commit is contained in:
parent
b4f790e185
commit
cf9b9c5d74
@ -24,6 +24,11 @@
|
||||
:loading="mainForceTraceLoading"
|
||||
:error="mainForceTraceError"
|
||||
/>
|
||||
<PositionAnomalyPanel
|
||||
:data="positionAnomaly"
|
||||
:loading="positionAnomalyLoading"
|
||||
:error="positionAnomalyError"
|
||||
/>
|
||||
</div>
|
||||
<aside class="side">
|
||||
<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 ChartPanel from './components/quote/ChartPanel.vue'
|
||||
import MainForceTracePanel from './components/quote/MainForceTracePanel.vue'
|
||||
import PositionAnomalyPanel from './components/quote/PositionAnomalyPanel.vue'
|
||||
import OrderBook from './components/quote/OrderBook.vue'
|
||||
import TradeTape from './components/quote/TradeTape.vue'
|
||||
import LargeOrderAnalysis from './components/quote/LargeOrderAnalysis.vue'
|
||||
@ -102,6 +108,9 @@ const {
|
||||
mainForceTrace,
|
||||
mainForceTraceLoading,
|
||||
mainForceTraceError,
|
||||
positionAnomaly,
|
||||
positionAnomalyLoading,
|
||||
positionAnomalyError,
|
||||
} = storeToRefs(useQuotaStore())
|
||||
const { data: news, loading: newsLoading, error: newsError, refresh: refreshNews } = useNews()
|
||||
const { data: positions } = usePositions()
|
||||
|
||||
@ -19,8 +19,8 @@ export function buildAnalysisMessages(payload: AiAnalysisPayload): Array<{
|
||||
}> {
|
||||
const system = [
|
||||
payload.keywords.trim() || '你是一名专业的国内期货交易员和分析员。',
|
||||
'请仅基于用户提供的行情、盘口、新闻、机构持仓与主力追踪(变盘预警、主力趋势机会)数据给出交易建议。',
|
||||
'主力追踪为全市场数据,请优先关注与当前合约品种相关的条目,并结合整体主力动向综合判断。',
|
||||
'请仅基于用户提供的行情、盘口、新闻、机构持仓、主力追踪(变盘预警、主力趋势机会)与主力异动数据给出交易建议。',
|
||||
'主力追踪与主力异动为全市场数据,请优先关注与当前合约品种相关的条目,并结合整体主力动向综合判断。',
|
||||
'不要编造未提供的数据;信息不足时偏向观望并说明原因。',
|
||||
'必须只输出一个 JSON 对象,不要 Markdown 代码块,不要其它说明文字。',
|
||||
`JSON 格式:${OUTPUT_SCHEMA}`,
|
||||
@ -39,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
|
||||
}
|
||||
|
||||
|
||||
@ -12,6 +12,12 @@ export const jqkaHttp = axios.create({
|
||||
timeout: 15000,
|
||||
})
|
||||
|
||||
/** 开发环境走 Vite 代理 /dq → https://dq.10jqka.com.cn */
|
||||
export const jqkaDqHttp = axios.create({
|
||||
baseURL: '/dq',
|
||||
timeout: 15000,
|
||||
})
|
||||
|
||||
/** 开发环境走 Vite 代理 /deepseek → https://api.deepseek.com */
|
||||
export const deepseekHttp = axios.create({
|
||||
baseURL: '/deepseek',
|
||||
|
||||
43
src/api/jqka/anomaly.ts
Normal file
43
src/api/jqka/anomaly.ts
Normal 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
|
||||
}
|
||||
26
src/api/jqka/anomalyTypes.ts
Normal file
26
src/api/jqka/anomalyTypes.ts
Normal 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
|
||||
}
|
||||
56
src/api/jqka/mapAnomaly.ts
Normal file
56
src/api/jqka/mapAnomaly.ts
Normal 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],
|
||||
}))
|
||||
}
|
||||
260
src/components/quote/PositionAnomalyPanel.vue
Normal file
260
src/components/quote/PositionAnomalyPanel.vue
Normal 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>
|
||||
@ -1,6 +1,13 @@
|
||||
import { defineStore } from 'pinia'
|
||||
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 { getStockKline, getStockQuotation } from '../api/baidu/quotation'
|
||||
import { getFuturesNews } from '../api/baidu/news'
|
||||
@ -20,8 +27,10 @@ import {
|
||||
getMainForceTrace,
|
||||
getPositionProfitRank,
|
||||
} from '../api/jqka/position'
|
||||
import { getPositionAnomaly } from '../api/jqka/anomaly'
|
||||
import { mapJqkaDealPosition, mapJqkaPositionProfit } from '../api/jqka/mapPosition'
|
||||
import { mapJqkaMainForceTrace } from '../api/jqka/mapMainForceTrace'
|
||||
import { mapJqkaPositionAnomaly } from '../api/jqka/mapAnomaly'
|
||||
import {
|
||||
extractVariety,
|
||||
getPositionQueryDate,
|
||||
@ -52,6 +61,8 @@ export interface AnalysisSnapshot {
|
||||
positions: PositionsData | null
|
||||
/** 变盘预警 + 主力趋势机会(全市场,各合约共用) */
|
||||
mainForceTrace: MainForceTraceData | null
|
||||
/** 主力异动六类排行(全市场,各合约共用) */
|
||||
positionAnomaly: PositionAnomalyData | null
|
||||
fetchedAt: string
|
||||
}
|
||||
|
||||
@ -115,6 +126,18 @@ function emptyMainForceTrace(): MainForceTraceData {
|
||||
}
|
||||
}
|
||||
|
||||
function emptyPositionAnomaly(): PositionAnomalyData {
|
||||
return {
|
||||
date: '',
|
||||
netLongIncrease: [],
|
||||
netShortIncrease: [],
|
||||
netLongDecrease: [],
|
||||
netShortDecrease: [],
|
||||
shortToLong: [],
|
||||
longToShort: [],
|
||||
}
|
||||
}
|
||||
|
||||
function activeContract() {
|
||||
return useContractStore().current
|
||||
}
|
||||
@ -240,6 +263,11 @@ export const useQuotaStore = defineStore('quota', () => {
|
||||
const mainForceTraceLoading = ref(false)
|
||||
const mainForceTraceError = ref<unknown>(null)
|
||||
|
||||
// ─── 主力异动(六类排行,全市场)─────────────────────────
|
||||
const positionAnomaly = ref<PositionAnomalyData>(emptyPositionAnomaly())
|
||||
const positionAnomalyLoading = ref(false)
|
||||
const positionAnomalyError = ref<unknown>(null)
|
||||
|
||||
/** 是否已有至少一次成功的行情拉取(可用于 AI 门禁) */
|
||||
const hasQuote = computed(() => quote.value.last > 0 && quote.value.intraday.length > 0)
|
||||
const hasNews = computed(() => news.value.length > 0)
|
||||
@ -247,6 +275,7 @@ export const useQuotaStore = defineStore('quota', () => {
|
||||
const newsLoaded = ref(false)
|
||||
const positionsLoaded = ref(false)
|
||||
const mainForceTraceLoaded = ref(false)
|
||||
const positionAnomalyLoaded = ref(false)
|
||||
const hasPositions = computed(
|
||||
() =>
|
||||
positions.value.long.length > 0 ||
|
||||
@ -258,6 +287,15 @@ export const useQuotaStore = defineStore('quota', () => {
|
||||
mainForceTrace.value.changeRiskList.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() {
|
||||
const c = activeContract()
|
||||
@ -327,10 +365,10 @@ export const useQuotaStore = defineStore('quota', () => {
|
||||
}
|
||||
|
||||
/**
|
||||
* 拉取会员持仓 + 机构盈利 + 主力追踪。
|
||||
* 拉取会员持仓 + 机构盈利 + 主力追踪 + 主力异动。
|
||||
* 持仓日期:交易日 16:00 后查当天,否则查上一交易日;
|
||||
* 若遇节假日空数据则再往前最多试 5 个交易日。
|
||||
* 主力追踪 date 与最终持仓 date 保持一致。
|
||||
* 主力追踪 / 主力异动 date 与最终持仓 date 保持一致。
|
||||
* 盈利默认最近一个月、按当前品种查询。
|
||||
*/
|
||||
async function fetchPositions() {
|
||||
@ -338,6 +376,8 @@ export const useQuotaStore = defineStore('quota', () => {
|
||||
positionsError.value = null
|
||||
mainForceTraceLoading.value = true
|
||||
mainForceTraceError.value = null
|
||||
positionAnomalyLoading.value = true
|
||||
positionAnomalyError.value = null
|
||||
try {
|
||||
const c = activeContract()
|
||||
const contract = c.code
|
||||
@ -372,21 +412,36 @@ 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)
|
||||
}
|
||||
// 全市场数据与机构持仓使用同一 date(失败不阻断持仓)
|
||||
await Promise.all([
|
||||
(async () => {
|
||||
try {
|
||||
const traceRaw = await getMainForceTrace({ date })
|
||||
mainForceTrace.value = mapJqkaMainForceTrace(traceRaw)
|
||||
mainForceTraceLoaded.value = true
|
||||
} catch (e) {
|
||||
mainForceTraceError.value = 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) {
|
||||
positionsError.value = e
|
||||
console.error('[quota] 拉取持仓失败', e)
|
||||
} finally {
|
||||
positionsLoading.value = false
|
||||
mainForceTraceLoading.value = false
|
||||
positionAnomalyLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@ -407,14 +462,17 @@ export const useQuotaStore = defineStore('quota', () => {
|
||||
news.value = []
|
||||
positions.value = emptyPositions()
|
||||
mainForceTrace.value = emptyMainForceTrace()
|
||||
positionAnomaly.value = emptyPositionAnomaly()
|
||||
loadedKlines.value = { day: false, week: false, month: false }
|
||||
newsLoaded.value = false
|
||||
positionsLoaded.value = false
|
||||
mainForceTraceLoaded.value = false
|
||||
positionAnomalyLoaded.value = false
|
||||
quoteError.value = null
|
||||
newsError.value = null
|
||||
positionsError.value = null
|
||||
mainForceTraceError.value = null
|
||||
positionAnomalyError.value = null
|
||||
|
||||
await fetchAll()
|
||||
}
|
||||
@ -422,12 +480,16 @@ export const useQuotaStore = defineStore('quota', () => {
|
||||
/**
|
||||
* AI 分析前取数:行情每次重新拉取;
|
||||
* 新闻 / 机构持仓若已有成功数据则跳过,避免重复请求。
|
||||
* (主力追踪随持仓一并拉取,持仓已加载则跳过)
|
||||
* (主力追踪 / 主力异动随持仓一并拉取)
|
||||
*/
|
||||
async function fetchForAnalysis() {
|
||||
const tasks: Promise<void>[] = [fetchQuote()]
|
||||
if (!newsLoaded.value) tasks.push(fetchNews())
|
||||
if (!positionsLoaded.value || !mainForceTraceLoaded.value) {
|
||||
if (
|
||||
!positionsLoaded.value ||
|
||||
!mainForceTraceLoaded.value ||
|
||||
!positionAnomalyLoaded.value
|
||||
) {
|
||||
tasks.push(fetchPositions())
|
||||
}
|
||||
await Promise.all(tasks)
|
||||
@ -459,6 +521,9 @@ export const useQuotaStore = defineStore('quota', () => {
|
||||
mainForceTrace: hasMainForceTrace.value
|
||||
? clonePlain(mainForceTrace.value)
|
||||
: null,
|
||||
positionAnomaly: hasPositionAnomaly.value
|
||||
? clonePlain(positionAnomaly.value)
|
||||
: null,
|
||||
fetchedAt: new Date().toISOString(),
|
||||
}
|
||||
}
|
||||
@ -479,6 +544,9 @@ export const useQuotaStore = defineStore('quota', () => {
|
||||
mainForceTrace,
|
||||
mainForceTraceLoading,
|
||||
mainForceTraceError,
|
||||
positionAnomaly,
|
||||
positionAnomalyLoading,
|
||||
positionAnomalyError,
|
||||
wsStatus,
|
||||
wsInSession,
|
||||
// computed
|
||||
@ -486,9 +554,11 @@ export const useQuotaStore = defineStore('quota', () => {
|
||||
hasNews,
|
||||
hasPositions,
|
||||
hasMainForceTrace,
|
||||
hasPositionAnomaly,
|
||||
newsLoaded,
|
||||
positionsLoaded,
|
||||
mainForceTraceLoaded,
|
||||
positionAnomalyLoaded,
|
||||
// actions
|
||||
fetchQuote,
|
||||
loadKline,
|
||||
|
||||
@ -192,6 +192,45 @@ export interface MainForceTraceData {
|
||||
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 interface AiAdvice {
|
||||
|
||||
@ -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',
|
||||
},
|
||||
},
|
||||
/** 同花顺 dq 数据源(主力异动等);须排在 /jqka 前,避免前缀被吞 */
|
||||
'/dq': {
|
||||
target: 'https://dq.10jqka.com.cn',
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/dq/, ''),
|
||||
},
|
||||
'/jqka': {
|
||||
target: 'https://fupage.10jqka.com.cn',
|
||||
changeOrigin: true,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user