主力异动
This commit is contained in:
parent
b4f790e185
commit
cf9b9c5d74
@ -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()
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import type { AnalysisSnapshot } from '../../stores/quota'
|
|||||||
export interface AiAnalysisPayload {
|
export interface AiAnalysisPayload {
|
||||||
/** 设置中的角色/分析关键词 */
|
/** 设置中的角色/分析关键词 */
|
||||||
keywords: string
|
keywords: string
|
||||||
/** 行情 + 新闻 + 机构持仓 + 主力追踪等接口快照 */
|
/** 行情 + 新闻 + 机构持仓 + 主力追踪 + 主力异动等接口快照 */
|
||||||
snapshot: AnalysisSnapshot
|
snapshot: AnalysisSnapshot
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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
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 { 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,
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user