新增合约配置
This commit is contained in:
parent
1a5ea5b967
commit
63b92550d3
@ -1,7 +1,12 @@
|
|||||||
import type { IntradayPoint, QuoteData, TradeTick } from '../../types'
|
import type { IntradayPoint, QuoteData, TradeTick } from '../../types'
|
||||||
import { contractConfig } from '../../config/contract'
|
|
||||||
import type { BaiduQuotationResult } from './types'
|
import type { BaiduQuotationResult } from './types'
|
||||||
|
|
||||||
|
export type QuoteFallback = {
|
||||||
|
name: string
|
||||||
|
code: string
|
||||||
|
exchange: string
|
||||||
|
}
|
||||||
|
|
||||||
function toNum(value: string | number | undefined | null, fallback = 0): number {
|
function toNum(value: string | number | undefined | null, fallback = 0): number {
|
||||||
if (value == null || value === '' || value === '--') return fallback
|
if (value == null || value === '' || value === '--') return fallback
|
||||||
if (typeof value === 'number') return Number.isFinite(value) ? value : fallback
|
if (typeof value === 'number') return Number.isFinite(value) ? value : fallback
|
||||||
@ -75,7 +80,10 @@ function parseTrades(result: BaiduQuotationResult): TradeTick[] {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
export function mapBaiduQuotationToQuote(result: BaiduQuotationResult): QuoteData {
|
export function mapBaiduQuotationToQuote(
|
||||||
|
result: BaiduQuotationResult,
|
||||||
|
fallback: QuoteFallback,
|
||||||
|
): QuoteData {
|
||||||
const op = result.pankouinfos?.origin_pankou
|
const op = result.pankouinfos?.origin_pankou
|
||||||
const cur = result.cur
|
const cur = result.cur
|
||||||
const basic = result.basicinfos
|
const basic = result.basicinfos
|
||||||
@ -121,9 +129,9 @@ export function mapBaiduQuotationToQuote(result: BaiduQuotationResult): QuoteDat
|
|||||||
)
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: basic?.name || contractConfig.name,
|
name: basic?.name || fallback.name,
|
||||||
code: basic?.code || contractConfig.code,
|
code: basic?.code || fallback.code,
|
||||||
exchange: basic?.exchange || contractConfig.exchange,
|
exchange: basic?.exchange || fallback.exchange,
|
||||||
status: update?.stockStatus || '未知',
|
status: update?.stockStatus || '未知',
|
||||||
last: toNum(cur?.price ?? op?.currentPrice),
|
last: toNum(cur?.price ?? op?.currentPrice),
|
||||||
change: toNum(cur?.increase),
|
change: toNum(cur?.increase),
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { contractConfig } from '../../config/contract'
|
import { useContractStore } from '../../stores/contract'
|
||||||
import type { BaiduWsItem, BaiduWsMessage, BaiduWsOutbound } from './wsTypes'
|
import type { BaiduWsItem, BaiduWsMessage, BaiduWsOutbound } from './wsTypes'
|
||||||
|
|
||||||
const PING_MS = 6_000
|
const PING_MS = 6_000
|
||||||
@ -26,9 +26,10 @@ export type BaiduQuoteWsHandlers = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function buildItem(): BaiduWsItem {
|
function buildItem(): BaiduWsItem {
|
||||||
|
const c = useContractStore().current
|
||||||
return {
|
return {
|
||||||
code: contractConfig.code,
|
code: c.code,
|
||||||
name: contractConfig.name,
|
name: c.name,
|
||||||
market: 'ab',
|
market: 'ab',
|
||||||
financeType: 'futures',
|
financeType: 'futures',
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,18 +19,39 @@
|
|||||||
<span class="time">更新 {{ quote.updatedAt }}</span>
|
<span class="time">更新 {{ quote.updatedAt }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<el-button :icon="Setting" circle title="设置" @click="settingsVisible = true" />
|
<el-button :icon="Setting" circle title="AI 设置" @click="settingsVisible = true" />
|
||||||
|
<el-select
|
||||||
|
class="contract-select"
|
||||||
|
:model-value="selectedId"
|
||||||
|
placeholder="选择合约"
|
||||||
|
size="default"
|
||||||
|
@change="onContractChange"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="c in contracts"
|
||||||
|
:key="c.id"
|
||||||
|
:label="`${c.name} (${c.code})`"
|
||||||
|
:value="c.id"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
<el-button title="合约设置" @click="contractSettingsVisible = true">合约</el-button>
|
||||||
</div>
|
</div>
|
||||||
<SettingsDialog v-model="settingsVisible" />
|
<SettingsDialog v-model="settingsVisible" />
|
||||||
|
<ContractSettingsDialog v-model="contractSettingsVisible" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
|
import { storeToRefs } from 'pinia'
|
||||||
import { Setting } from '@element-plus/icons-vue'
|
import { Setting } from '@element-plus/icons-vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
import type { QuoteData } from '../../types'
|
import type { QuoteData } from '../../types'
|
||||||
import type { WsConnectionStatus } from '../../api/baidu/ws'
|
import type { WsConnectionStatus } from '../../api/baidu/ws'
|
||||||
import SettingsDialog from './SettingsDialog.vue'
|
import SettingsDialog from './SettingsDialog.vue'
|
||||||
|
import ContractSettingsDialog from './ContractSettingsDialog.vue'
|
||||||
|
import { useContractStore } from '../../stores/contract'
|
||||||
|
import { useQuotaStore } from '../../stores/quota'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
quote: QuoteData
|
quote: QuoteData
|
||||||
@ -39,6 +60,18 @@ const props = defineProps<{
|
|||||||
}>()
|
}>()
|
||||||
|
|
||||||
const settingsVisible = ref(false)
|
const settingsVisible = ref(false)
|
||||||
|
const contractSettingsVisible = ref(false)
|
||||||
|
|
||||||
|
const contractStore = useContractStore()
|
||||||
|
const quota = useQuotaStore()
|
||||||
|
const { contracts, selectedId } = storeToRefs(contractStore)
|
||||||
|
|
||||||
|
async function onContractChange(id: string) {
|
||||||
|
if (!id || id === selectedId.value) return
|
||||||
|
contractStore.select(id)
|
||||||
|
await quota.reloadForContract()
|
||||||
|
ElMessage.success('已切换合约')
|
||||||
|
}
|
||||||
|
|
||||||
const priceClass = computed(() =>
|
const priceClass = computed(() =>
|
||||||
props.quote.change >= 0 ? 'price-up' : 'price-down',
|
props.quote.change >= 0 ? 'price-up' : 'price-down',
|
||||||
@ -175,5 +208,12 @@ function formatSigned(n: number) {
|
|||||||
|
|
||||||
.actions {
|
.actions {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contract-select {
|
||||||
|
width: 180px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
236
src/components/header/ContractSettingsDialog.vue
Normal file
236
src/components/header/ContractSettingsDialog.vue
Normal file
@ -0,0 +1,236 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog
|
||||||
|
:model-value="modelValue"
|
||||||
|
title="合约设置"
|
||||||
|
width="760px"
|
||||||
|
destroy-on-close
|
||||||
|
@update:model-value="emit('update:modelValue', $event)"
|
||||||
|
>
|
||||||
|
<div class="toolbar">
|
||||||
|
<el-button type="primary" @click="startAdd">新增合约</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-table :data="contracts" size="small" stripe empty-text="暂无合约">
|
||||||
|
<el-table-column label="当前" width="64" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-radio
|
||||||
|
:model-value="selectedId"
|
||||||
|
:value="row.id"
|
||||||
|
@change="onSelect(row.id)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="name" label="名称" min-width="100" />
|
||||||
|
<el-table-column prop="code" label="代码" width="90" />
|
||||||
|
<el-table-column prop="variety" label="品种" width="70" />
|
||||||
|
<el-table-column prop="exchange" label="交易所" width="90" />
|
||||||
|
<el-table-column prop="largeOrderLots" label="大单阈值" width="88" />
|
||||||
|
<el-table-column prop="keywords" label="关键字" min-width="120" show-overflow-tooltip />
|
||||||
|
<el-table-column label="操作" width="140" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button link type="primary" @click="startEdit(row)">编辑</el-button>
|
||||||
|
<el-button
|
||||||
|
link
|
||||||
|
type="danger"
|
||||||
|
:disabled="contracts.length <= 1"
|
||||||
|
@click="onRemove(row.id)"
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<el-dialog
|
||||||
|
v-model="formVisible"
|
||||||
|
:title="editingId ? '编辑合约' : '新增合约'"
|
||||||
|
width="420px"
|
||||||
|
append-to-body
|
||||||
|
destroy-on-close
|
||||||
|
>
|
||||||
|
<el-form label-position="top">
|
||||||
|
<el-form-item label="合约代码" required>
|
||||||
|
<el-input
|
||||||
|
v-model="form.code"
|
||||||
|
placeholder="如 FG609"
|
||||||
|
@blur="onCodeBlur"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="展示名称" required>
|
||||||
|
<el-input v-model="form.name" placeholder="如 玻璃2609" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="品种代码">
|
||||||
|
<el-input v-model="form.variety" placeholder="缺省从代码前缀推导" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="交易所">
|
||||||
|
<el-input v-model="form.exchange" placeholder="如 郑商所" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="大单手数阈值">
|
||||||
|
<el-input-number
|
||||||
|
v-model="form.largeOrderLots"
|
||||||
|
:min="1"
|
||||||
|
:max="999999"
|
||||||
|
:step="10"
|
||||||
|
controls-position="right"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="合约关键字">
|
||||||
|
<el-input
|
||||||
|
v-model="form.keywords"
|
||||||
|
type="textarea"
|
||||||
|
:rows="3"
|
||||||
|
placeholder="如:当前持仓、止损位、关注看点;AI 分析时追加在全局「额外关键字」之后"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="formVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="saveForm">保存</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { reactive, ref } from 'vue'
|
||||||
|
import { storeToRefs } from 'pinia'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import { useContractStore, DEFAULT_CONTRACT } from '../../stores/contract'
|
||||||
|
import { useQuotaStore } from '../../stores/quota'
|
||||||
|
import { extractVariety } from '../../utils/tradingDate'
|
||||||
|
import type { ContractItem } from '../../types'
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
modelValue?: boolean
|
||||||
|
}>()
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:modelValue': [value: boolean]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const contractStore = useContractStore()
|
||||||
|
const quota = useQuotaStore()
|
||||||
|
const { contracts, selectedId } = storeToRefs(contractStore)
|
||||||
|
|
||||||
|
const formVisible = ref(false)
|
||||||
|
const editingId = ref<string | null>(null)
|
||||||
|
const form = reactive({
|
||||||
|
code: '',
|
||||||
|
name: '',
|
||||||
|
variety: '',
|
||||||
|
exchange: '',
|
||||||
|
largeOrderLots: DEFAULT_CONTRACT.largeOrderLots,
|
||||||
|
keywords: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
function resetForm(seed?: Partial<ContractItem>) {
|
||||||
|
form.code = seed?.code || ''
|
||||||
|
form.name = seed?.name || ''
|
||||||
|
form.variety = seed?.variety || ''
|
||||||
|
form.exchange = seed?.exchange || ''
|
||||||
|
form.largeOrderLots = seed?.largeOrderLots || DEFAULT_CONTRACT.largeOrderLots
|
||||||
|
form.keywords = seed?.keywords || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function startAdd() {
|
||||||
|
editingId.value = null
|
||||||
|
resetForm({
|
||||||
|
exchange: DEFAULT_CONTRACT.exchange,
|
||||||
|
largeOrderLots: DEFAULT_CONTRACT.largeOrderLots,
|
||||||
|
})
|
||||||
|
formVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function startEdit(row: ContractItem) {
|
||||||
|
editingId.value = row.id
|
||||||
|
resetForm(row)
|
||||||
|
formVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function onCodeBlur() {
|
||||||
|
const code = form.code.trim().toUpperCase()
|
||||||
|
form.code = code
|
||||||
|
if (!form.variety.trim() && code) {
|
||||||
|
form.variety = extractVariety(code)
|
||||||
|
}
|
||||||
|
if (!form.name.trim() && code) {
|
||||||
|
form.name = code
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onSelect(id: string) {
|
||||||
|
if (id === selectedId.value) return
|
||||||
|
contractStore.select(id)
|
||||||
|
await quota.reloadForContract()
|
||||||
|
ElMessage.success('已切换合约')
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onRemove(id: string) {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm('确定删除该合约?', '提示', { type: 'warning' })
|
||||||
|
} catch {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const wasSelected = id === selectedId.value
|
||||||
|
if (!contractStore.remove(id)) {
|
||||||
|
ElMessage.warning('至少保留一个合约')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ElMessage.success('已删除')
|
||||||
|
if (wasSelected) {
|
||||||
|
await quota.reloadForContract()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveForm() {
|
||||||
|
const payload = {
|
||||||
|
code: form.code.trim(),
|
||||||
|
name: form.name.trim(),
|
||||||
|
variety: form.variety.trim(),
|
||||||
|
exchange: form.exchange.trim(),
|
||||||
|
largeOrderLots: form.largeOrderLots,
|
||||||
|
keywords: form.keywords.trim(),
|
||||||
|
}
|
||||||
|
if (!payload.code) {
|
||||||
|
ElMessage.warning('请填写合约代码')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!payload.name) {
|
||||||
|
ElMessage.warning('请填写展示名称')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (editingId.value) {
|
||||||
|
const id = editingId.value
|
||||||
|
const prev = contracts.value.find((c) => c.id === id)
|
||||||
|
const wasCurrent = id === selectedId.value
|
||||||
|
const identityChanged =
|
||||||
|
!!prev &&
|
||||||
|
(prev.code !== payload.code.toUpperCase() ||
|
||||||
|
(prev.variety || '') !== (payload.variety.toUpperCase() || prev.variety))
|
||||||
|
if (!contractStore.update(id, payload)) {
|
||||||
|
ElMessage.error('保存失败')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
formVisible.value = false
|
||||||
|
ElMessage.success('已更新')
|
||||||
|
if (wasCurrent && identityChanged) {
|
||||||
|
void quota.reloadForContract()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const item = contractStore.add(payload)
|
||||||
|
if (!item) {
|
||||||
|
ElMessage.error('保存失败')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
formVisible.value = false
|
||||||
|
ElMessage.success('已新增')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.toolbar {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -27,13 +27,14 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
|
import { storeToRefs } from 'pinia'
|
||||||
import { use } from 'echarts/core'
|
import { use } from 'echarts/core'
|
||||||
import { CanvasRenderer } from 'echarts/renderers'
|
import { CanvasRenderer } from 'echarts/renderers'
|
||||||
import { PieChart } from 'echarts/charts'
|
import { PieChart } from 'echarts/charts'
|
||||||
import { TooltipComponent } from 'echarts/components'
|
import { TooltipComponent } from 'echarts/components'
|
||||||
import type { EChartsOption } from 'echarts'
|
import type { EChartsOption } from 'echarts'
|
||||||
import VChart from 'vue-echarts'
|
import VChart from 'vue-echarts'
|
||||||
import { contractConfig } from '../../config/contract'
|
import { useContractStore } from '../../stores/contract'
|
||||||
import type { QuoteData, TradeTick } from '../../types'
|
import type { QuoteData, TradeTick } from '../../types'
|
||||||
|
|
||||||
use([CanvasRenderer, PieChart, TooltipComponent])
|
use([CanvasRenderer, PieChart, TooltipComponent])
|
||||||
@ -42,7 +43,8 @@ const props = defineProps<{
|
|||||||
quote: QuoteData
|
quote: QuoteData
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const threshold = contractConfig.largeOrderLots
|
const { current } = storeToRefs(useContractStore())
|
||||||
|
const threshold = computed(() => current.value.largeOrderLots)
|
||||||
|
|
||||||
const COLORS = {
|
const COLORS = {
|
||||||
largeBuy: '#f0435c',
|
largeBuy: '#f0435c',
|
||||||
@ -59,14 +61,14 @@ interface Bucket {
|
|||||||
pct: string
|
pct: string
|
||||||
}
|
}
|
||||||
|
|
||||||
function aggregate(trades: TradeTick[]) {
|
function aggregate(trades: TradeTick[], lots: number) {
|
||||||
let largeBuy = 0
|
let largeBuy = 0
|
||||||
let largeSell = 0
|
let largeSell = 0
|
||||||
let retailBuy = 0
|
let retailBuy = 0
|
||||||
let retailSell = 0
|
let retailSell = 0
|
||||||
|
|
||||||
for (const t of trades) {
|
for (const t of trades) {
|
||||||
const isLarge = t.volume > threshold
|
const isLarge = t.volume > lots
|
||||||
if (t.side === 'B') {
|
if (t.side === 'B') {
|
||||||
if (isLarge) largeBuy += t.volume
|
if (isLarge) largeBuy += t.volume
|
||||||
else retailBuy += t.volume
|
else retailBuy += t.volume
|
||||||
@ -121,7 +123,7 @@ function toItems(
|
|||||||
return { buyItems, sellItems, data: [...buyItems, ...sellItems] }
|
return { buyItems, sellItems, data: [...buyItems, ...sellItems] }
|
||||||
}
|
}
|
||||||
|
|
||||||
const stats = computed(() => toItems(aggregate(props.quote.trades)))
|
const stats = computed(() => toItems(aggregate(props.quote.trades, threshold.value)))
|
||||||
const buyItems = computed(() => stats.value.buyItems)
|
const buyItems = computed(() => stats.value.buyItems)
|
||||||
const sellItems = computed(() => stats.value.sellItems)
|
const sellItems = computed(() => stats.value.sellItems)
|
||||||
|
|
||||||
|
|||||||
@ -4,10 +4,20 @@ import type { AiAdvice } from '../types'
|
|||||||
import { fetchDeepSeekAdvice } from '../api/deepseek/analyze'
|
import { fetchDeepSeekAdvice } from '../api/deepseek/analyze'
|
||||||
import type { AiAnalysisPayload } from '../api/deepseek/types'
|
import type { AiAnalysisPayload } from '../api/deepseek/types'
|
||||||
import { useQuotaStore, type KlinePeriod } from '../stores/quota'
|
import { useQuotaStore, type KlinePeriod } from '../stores/quota'
|
||||||
|
import { useContractStore } from '../stores/contract'
|
||||||
import { useSettingsStore } from '../stores/settings'
|
import { useSettingsStore } from '../stores/settings'
|
||||||
import { isWsTradingSession } from '../utils/tradingDate'
|
import { isWsTradingSession } from '../utils/tradingDate'
|
||||||
import { useTitleBlink } from './useTitleBlink'
|
import { useTitleBlink } from './useTitleBlink'
|
||||||
|
|
||||||
|
/** 全局额外关键字 + 当前合约关键字(合约段追加在后) */
|
||||||
|
function mergeKeywords(globalKeywords: string, contractKeywords: string): string {
|
||||||
|
const global = globalKeywords.trim()
|
||||||
|
const extra = contractKeywords.trim()
|
||||||
|
if (!extra) return global
|
||||||
|
if (!global) return extra
|
||||||
|
return `${global}\n${extra}`
|
||||||
|
}
|
||||||
|
|
||||||
const KLINE_PERIODS: KlinePeriod[] = ['day', 'week', 'month']
|
const KLINE_PERIODS: KlinePeriod[] = ['day', 'week', 'month']
|
||||||
|
|
||||||
/** 调度轮询间隔:检查距上次执行是否已超过设定分钟 */
|
/** 调度轮询间隔:检查距上次执行是否已超过设定分钟 */
|
||||||
@ -31,11 +41,29 @@ export function useAiAdvice() {
|
|||||||
/** 上次成功执行分析的时间戳(ms) */
|
/** 上次成功执行分析的时间戳(ms) */
|
||||||
const lastExecutedAt = ref<number | null>(null)
|
const lastExecutedAt = ref<number | null>(null)
|
||||||
const quota = useQuotaStore()
|
const quota = useQuotaStore()
|
||||||
|
const contractStore = useContractStore()
|
||||||
const settings = useSettingsStore()
|
const settings = useSettingsStore()
|
||||||
|
|
||||||
useTitleBlink(data)
|
useTitleBlink(data)
|
||||||
|
|
||||||
let scheduleTimer: ReturnType<typeof setInterval> | null = null
|
let scheduleTimer: ReturnType<typeof setInterval> | null = null
|
||||||
|
/** 切换合约时递增,丢弃进行中的旧分析结果 */
|
||||||
|
let analysisEpoch = 0
|
||||||
|
|
||||||
|
function clearAdvice() {
|
||||||
|
data.value = emptyAdvice()
|
||||||
|
error.value = null
|
||||||
|
lastExecutedAt.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => contractStore.selectedId,
|
||||||
|
() => {
|
||||||
|
analysisEpoch += 1
|
||||||
|
loading.value = false
|
||||||
|
clearAdvice()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
/** 尽量补全日/周/月 K 线,失败不阻断主流程 */
|
/** 尽量补全日/周/月 K 线,失败不阻断主流程 */
|
||||||
async function ensureKlines() {
|
async function ensureKlines() {
|
||||||
@ -66,30 +94,37 @@ export function useAiAdvice() {
|
|||||||
}
|
}
|
||||||
if (loading.value) return
|
if (loading.value) return
|
||||||
|
|
||||||
|
const epoch = analysisEpoch
|
||||||
loading.value = true
|
loading.value = true
|
||||||
error.value = null
|
error.value = null
|
||||||
try {
|
try {
|
||||||
await quota.fetchForAnalysis()
|
await quota.fetchForAnalysis()
|
||||||
|
if (epoch !== analysisEpoch) return
|
||||||
await ensureKlines()
|
await ensureKlines()
|
||||||
|
if (epoch !== analysisEpoch) return
|
||||||
|
|
||||||
const payload: AiAnalysisPayload = {
|
const payload: AiAnalysisPayload = {
|
||||||
keywords: settings.keywords,
|
keywords: mergeKeywords(settings.keywords, contractStore.current.keywords),
|
||||||
snapshot: quota.getAnalysisSnapshot(),
|
snapshot: quota.getAnalysisSnapshot(),
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[AI] 提交给 DeepSeek 的关键词', payload.keywords)
|
console.log('[AI] 提交给 DeepSeek 的关键词', payload.keywords)
|
||||||
console.log('[AI] 提交给 DeepSeek 的数据快照', payload.snapshot)
|
console.log('[AI] 提交给 DeepSeek 的数据快照', payload.snapshot)
|
||||||
|
|
||||||
data.value = await fetchDeepSeekAdvice(apiKey, payload)
|
const advice = await fetchDeepSeekAdvice(apiKey, payload)
|
||||||
|
if (epoch !== analysisEpoch) return
|
||||||
|
|
||||||
|
data.value = advice
|
||||||
lastExecutedAt.value = Date.now()
|
lastExecutedAt.value = Date.now()
|
||||||
if (!silent) ElMessage.success('AI 建议已更新')
|
if (!silent) ElMessage.success('AI 建议已更新')
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
if (epoch !== analysisEpoch) return
|
||||||
error.value = e
|
error.value = e
|
||||||
const msg = formatError(e)
|
const msg = formatError(e)
|
||||||
console.error('[AI] 一键获取建议失败', e)
|
console.error('[AI] 一键获取建议失败', e)
|
||||||
if (!silent) ElMessage.error(msg)
|
if (!silent) ElMessage.error(msg)
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
if (epoch === analysisEpoch) loading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,16 +1,5 @@
|
|||||||
/** 当前关注的期货合约(后续可改为可切换) */
|
|
||||||
export const contractConfig = {
|
|
||||||
/** 合约代码,对应百度行情 / 同花顺持仓接口 code/contract */
|
|
||||||
code: 'FG609',
|
|
||||||
/** 品种代码,持仓接口 variety;缺省可从 code 字母前缀推导 */
|
|
||||||
variety: 'FG',
|
|
||||||
/** 展示名称;接口未返回 name 时使用 */
|
|
||||||
name: '玻璃2609',
|
|
||||||
/** 交易所展示名 */
|
|
||||||
exchange: '郑商所',
|
|
||||||
/**
|
/**
|
||||||
* 大单手数阈值:现手大于该值计为大单,否则计为散单。
|
* @deprecated 合约配置已迁至 localStorage(stores/contract.ts)。
|
||||||
* 用于分时成交「大单分析」饼图。
|
* 此处仅保留默认种子,供首次安装与兼容引用。
|
||||||
*/
|
*/
|
||||||
largeOrderLots: 100,
|
export { DEFAULT_CONTRACT } from '../stores/contract'
|
||||||
} as const
|
|
||||||
|
|||||||
133
src/stores/contract.ts
Normal file
133
src/stores/contract.ts
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { computed, ref, watch } from 'vue'
|
||||||
|
import type { ContractItem, ContractStorage } from '../types'
|
||||||
|
import { extractVariety } from '../utils/tradingDate'
|
||||||
|
|
||||||
|
const STORAGE_KEY = 'ai-trade-contracts'
|
||||||
|
|
||||||
|
/** 首次安装时的默认合约(原 config/contract.ts) */
|
||||||
|
export const DEFAULT_CONTRACT: Omit<ContractItem, 'id'> = {
|
||||||
|
code: 'FG609',
|
||||||
|
variety: 'FG',
|
||||||
|
name: '玻璃2609',
|
||||||
|
exchange: '郑商所',
|
||||||
|
largeOrderLots: 100,
|
||||||
|
keywords: '',
|
||||||
|
}
|
||||||
|
|
||||||
|
function createId(): string {
|
||||||
|
return `c_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeItem(raw: Partial<ContractItem> & { code?: string }): ContractItem | null {
|
||||||
|
const code = String(raw.code || '').trim().toUpperCase()
|
||||||
|
if (!code) return null
|
||||||
|
const variety = String(raw.variety || '').trim().toUpperCase() || extractVariety(code)
|
||||||
|
const name = String(raw.name || '').trim() || code
|
||||||
|
const exchange = String(raw.exchange || '').trim() || ''
|
||||||
|
const lots = Number(raw.largeOrderLots)
|
||||||
|
const largeOrderLots =
|
||||||
|
Number.isFinite(lots) && lots > 0 ? Math.floor(lots) : DEFAULT_CONTRACT.largeOrderLots
|
||||||
|
const keywords = String(raw.keywords ?? '').trim()
|
||||||
|
return {
|
||||||
|
id: String(raw.id || createId()),
|
||||||
|
code,
|
||||||
|
variety,
|
||||||
|
name,
|
||||||
|
exchange,
|
||||||
|
largeOrderLots,
|
||||||
|
keywords,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function load(): ContractStorage {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(localStorage.getItem(STORAGE_KEY) || '{}') as Partial<ContractStorage>
|
||||||
|
const list = Array.isArray(parsed.contracts)
|
||||||
|
? parsed.contracts.map((c) => normalizeItem(c)).filter((c): c is ContractItem => c != null)
|
||||||
|
: []
|
||||||
|
if (list.length === 0) {
|
||||||
|
const first = normalizeItem({ ...DEFAULT_CONTRACT, id: createId() })!
|
||||||
|
return { contracts: [first], selectedId: first.id }
|
||||||
|
}
|
||||||
|
const selectedId =
|
||||||
|
typeof parsed.selectedId === 'string' && list.some((c) => c.id === parsed.selectedId)
|
||||||
|
? parsed.selectedId
|
||||||
|
: list[0].id
|
||||||
|
return { contracts: list, selectedId }
|
||||||
|
} catch {
|
||||||
|
const first = normalizeItem({ ...DEFAULT_CONTRACT, id: createId() })!
|
||||||
|
return { contracts: [first], selectedId: first.id }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useContractStore = defineStore('contract', () => {
|
||||||
|
const saved = load()
|
||||||
|
const contracts = ref<ContractItem[]>(saved.contracts)
|
||||||
|
const selectedId = ref(saved.selectedId)
|
||||||
|
|
||||||
|
const current = computed(() => {
|
||||||
|
const found = contracts.value.find((c) => c.id === selectedId.value)
|
||||||
|
return found ?? contracts.value[0]
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
[contracts, selectedId],
|
||||||
|
() => {
|
||||||
|
// 选中项失效时回落到第一项
|
||||||
|
if (!contracts.value.some((c) => c.id === selectedId.value) && contracts.value[0]) {
|
||||||
|
selectedId.value = contracts.value[0].id
|
||||||
|
}
|
||||||
|
const payload: ContractStorage = {
|
||||||
|
contracts: contracts.value,
|
||||||
|
selectedId: selectedId.value,
|
||||||
|
}
|
||||||
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(payload))
|
||||||
|
},
|
||||||
|
{ deep: true },
|
||||||
|
)
|
||||||
|
|
||||||
|
function select(id: string) {
|
||||||
|
if (!contracts.value.some((c) => c.id === id)) return
|
||||||
|
selectedId.value = id
|
||||||
|
}
|
||||||
|
|
||||||
|
function add(input: Omit<ContractItem, 'id'>): ContractItem | null {
|
||||||
|
const item = normalizeItem({ ...input, id: createId() })
|
||||||
|
if (!item) return null
|
||||||
|
contracts.value = [...contracts.value, item]
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
|
||||||
|
function update(id: string, input: Partial<Omit<ContractItem, 'id'>>): boolean {
|
||||||
|
const idx = contracts.value.findIndex((c) => c.id === id)
|
||||||
|
if (idx < 0) return false
|
||||||
|
const merged = normalizeItem({ ...contracts.value[idx], ...input, id })
|
||||||
|
if (!merged) return false
|
||||||
|
const next = contracts.value.slice()
|
||||||
|
next[idx] = merged
|
||||||
|
contracts.value = next
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
function remove(id: string): boolean {
|
||||||
|
if (contracts.value.length <= 1) return false
|
||||||
|
const next = contracts.value.filter((c) => c.id !== id)
|
||||||
|
if (next.length === contracts.value.length) return false
|
||||||
|
contracts.value = next
|
||||||
|
if (selectedId.value === id) {
|
||||||
|
selectedId.value = next[0].id
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
contracts,
|
||||||
|
selectedId,
|
||||||
|
current,
|
||||||
|
select,
|
||||||
|
add,
|
||||||
|
update,
|
||||||
|
remove,
|
||||||
|
}
|
||||||
|
})
|
||||||
@ -1,7 +1,7 @@
|
|||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { computed, ref, toRaw } from 'vue'
|
import { computed, ref, toRaw } from 'vue'
|
||||||
import type { Candle, NewsItem, PositionsData, QuoteData } from '../types'
|
import type { Candle, NewsItem, PositionsData, QuoteData } from '../types'
|
||||||
import { contractConfig } from '../config/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'
|
||||||
import { mapBaiduQuotationToQuote } from '../api/baidu/mapQuote'
|
import { mapBaiduQuotationToQuote } from '../api/baidu/mapQuote'
|
||||||
@ -52,10 +52,11 @@ function emptyCandles(): QuoteData['candles'] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function emptyQuote(): QuoteData {
|
function emptyQuote(): QuoteData {
|
||||||
|
const c = useContractStore().current
|
||||||
return {
|
return {
|
||||||
name: contractConfig.name,
|
name: c.name,
|
||||||
code: contractConfig.code,
|
code: c.code,
|
||||||
exchange: contractConfig.exchange,
|
exchange: c.exchange,
|
||||||
status: '',
|
status: '',
|
||||||
last: 0,
|
last: 0,
|
||||||
change: 0,
|
change: 0,
|
||||||
@ -98,6 +99,10 @@ function emptyPositions(): PositionsData {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function activeContract() {
|
||||||
|
return useContractStore().current
|
||||||
|
}
|
||||||
|
|
||||||
export const useQuotaStore = defineStore('quota', () => {
|
export const useQuotaStore = defineStore('quota', () => {
|
||||||
// ─── 行情 ───────────────────────────────────────────────
|
// ─── 行情 ───────────────────────────────────────────────
|
||||||
const quote = ref<QuoteData>(emptyQuote())
|
const quote = ref<QuoteData>(emptyQuote())
|
||||||
@ -129,7 +134,8 @@ export const useQuotaStore = defineStore('quota', () => {
|
|||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (msg.data.code && msg.data.code !== contractConfig.code) return
|
const code = activeContract().code
|
||||||
|
if (msg.data.code && msg.data.code !== code) return
|
||||||
const product = msg.data.product
|
const product = msg.data.product
|
||||||
if (product === 'tick') {
|
if (product === 'tick') {
|
||||||
quote.value = applyWsTick(quote.value, msg.data as BaiduWsTickData)
|
quote.value = applyWsTick(quote.value, msg.data as BaiduWsTickData)
|
||||||
@ -224,11 +230,16 @@ export const useQuotaStore = defineStore('quota', () => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
async function fetchQuote() {
|
async function fetchQuote() {
|
||||||
|
const c = activeContract()
|
||||||
quoteLoading.value = true
|
quoteLoading.value = true
|
||||||
quoteError.value = null
|
quoteError.value = null
|
||||||
try {
|
try {
|
||||||
const result = await getStockQuotation({ code: contractConfig.code })
|
const result = await getStockQuotation({ code: c.code })
|
||||||
const mapped = mapBaiduQuotationToQuote(result)
|
const mapped = mapBaiduQuotationToQuote(result, {
|
||||||
|
name: c.name,
|
||||||
|
code: c.code,
|
||||||
|
exchange: c.exchange,
|
||||||
|
})
|
||||||
mapped.candles = emptyCandles()
|
mapped.candles = emptyCandles()
|
||||||
loadedKlines.value = { day: false, week: false, month: false }
|
loadedKlines.value = { day: false, week: false, month: false }
|
||||||
quote.value = mapped
|
quote.value = mapped
|
||||||
@ -248,11 +259,12 @@ export const useQuotaStore = defineStore('quota', () => {
|
|||||||
return quote.value.candles[period]
|
return quote.value.candles[period]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const c = activeContract()
|
||||||
klineLoading.value = true
|
klineLoading.value = true
|
||||||
quoteError.value = null
|
quoteError.value = null
|
||||||
try {
|
try {
|
||||||
const result = await getStockKline({
|
const result = await getStockKline({
|
||||||
code: contractConfig.code,
|
code: c.code,
|
||||||
ktype: KTYPE_MAP[period],
|
ktype: KTYPE_MAP[period],
|
||||||
})
|
})
|
||||||
const candles = mapBaiduKlineToCandles(result)
|
const candles = mapBaiduKlineToCandles(result)
|
||||||
@ -269,10 +281,11 @@ export const useQuotaStore = defineStore('quota', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function fetchNews() {
|
async function fetchNews() {
|
||||||
|
const c = activeContract()
|
||||||
newsLoading.value = true
|
newsLoading.value = true
|
||||||
newsError.value = null
|
newsError.value = null
|
||||||
try {
|
try {
|
||||||
const list = await getFuturesNews({ code: contractConfig.code })
|
const list = await getFuturesNews({ code: c.code })
|
||||||
news.value = mapBaiduNewsToItems(list)
|
news.value = mapBaiduNewsToItems(list)
|
||||||
newsLoaded.value = news.value.length > 0
|
newsLoaded.value = news.value.length > 0
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -293,8 +306,9 @@ export const useQuotaStore = defineStore('quota', () => {
|
|||||||
positionsLoading.value = true
|
positionsLoading.value = true
|
||||||
positionsError.value = null
|
positionsError.value = null
|
||||||
try {
|
try {
|
||||||
const contract = contractConfig.code
|
const c = activeContract()
|
||||||
const variety = contractConfig.variety || extractVariety(contract)
|
const contract = c.code
|
||||||
|
const variety = c.variety || extractVariety(contract)
|
||||||
let date = getPositionQueryDate()
|
let date = getPositionQueryDate()
|
||||||
let mapped: PositionsData | null = null
|
let mapped: PositionsData | null = null
|
||||||
|
|
||||||
@ -337,6 +351,27 @@ export const useQuotaStore = defineStore('quota', () => {
|
|||||||
await Promise.all([fetchQuote(), fetchNews(), fetchPositions()])
|
await Promise.all([fetchQuote(), fetchNews(), fetchPositions()])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 切换合约后:断开旧 WS、清空缓存、重新拉取并按需重连。
|
||||||
|
*/
|
||||||
|
async function reloadForContract() {
|
||||||
|
quoteWs?.disconnect()
|
||||||
|
quoteWs = null
|
||||||
|
wsStatus.value = 'disconnected'
|
||||||
|
|
||||||
|
quote.value = emptyQuote()
|
||||||
|
news.value = []
|
||||||
|
positions.value = emptyPositions()
|
||||||
|
loadedKlines.value = { day: false, week: false, month: false }
|
||||||
|
newsLoaded.value = false
|
||||||
|
positionsLoaded.value = false
|
||||||
|
quoteError.value = null
|
||||||
|
newsError.value = null
|
||||||
|
positionsError.value = null
|
||||||
|
|
||||||
|
await fetchAll()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AI 分析前取数:行情每次重新拉取;
|
* AI 分析前取数:行情每次重新拉取;
|
||||||
* 新闻 / 机构持仓若已有成功数据则跳过,避免重复请求。
|
* 新闻 / 机构持仓若已有成功数据则跳过,避免重复请求。
|
||||||
@ -361,11 +396,12 @@ export const useQuotaStore = defineStore('quota', () => {
|
|||||||
* 返回深拷贝,避免分析过程中被行情刷新污染。
|
* 返回深拷贝,避免分析过程中被行情刷新污染。
|
||||||
*/
|
*/
|
||||||
function getAnalysisSnapshot(): AnalysisSnapshot {
|
function getAnalysisSnapshot(): AnalysisSnapshot {
|
||||||
|
const c = activeContract()
|
||||||
return {
|
return {
|
||||||
contract: {
|
contract: {
|
||||||
code: quote.value.code || contractConfig.code,
|
code: quote.value.code || c.code,
|
||||||
name: quote.value.name || contractConfig.name,
|
name: quote.value.name || c.name,
|
||||||
exchange: quote.value.exchange || contractConfig.exchange,
|
exchange: quote.value.exchange || c.exchange,
|
||||||
},
|
},
|
||||||
quote: quote.value ? clonePlain(quote.value) : null,
|
quote: quote.value ? clonePlain(quote.value) : null,
|
||||||
news: clonePlain(news.value),
|
news: clonePlain(news.value),
|
||||||
@ -402,6 +438,7 @@ export const useQuotaStore = defineStore('quota', () => {
|
|||||||
fetchPositions,
|
fetchPositions,
|
||||||
fetchAll,
|
fetchAll,
|
||||||
fetchForAnalysis,
|
fetchForAnalysis,
|
||||||
|
reloadForContract,
|
||||||
getAnalysisSnapshot,
|
getAnalysisSnapshot,
|
||||||
enableWs,
|
enableWs,
|
||||||
connectWs,
|
connectWs,
|
||||||
|
|||||||
@ -144,3 +144,32 @@ export interface AppSettings {
|
|||||||
/** 定时分析间隔(分钟) */
|
/** 定时分析间隔(分钟) */
|
||||||
scheduledAnalysisMinutes: number
|
scheduledAnalysisMinutes: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 单个期货合约配置(可多份,存 localStorage) */
|
||||||
|
export interface ContractItem {
|
||||||
|
id: string
|
||||||
|
/** 合约代码,对应百度行情 / 同花顺持仓接口 code/contract */
|
||||||
|
code: string
|
||||||
|
/** 品种代码,持仓接口 variety;缺省可从 code 字母前缀推导 */
|
||||||
|
variety: string
|
||||||
|
/** 展示名称;接口未返回 name 时使用 */
|
||||||
|
name: string
|
||||||
|
/** 交易所展示名 */
|
||||||
|
exchange: string
|
||||||
|
/**
|
||||||
|
* 大单手数阈值:现手大于该值计为大单,否则计为散单。
|
||||||
|
* 用于分时成交「大单分析」饼图。
|
||||||
|
*/
|
||||||
|
largeOrderLots: number
|
||||||
|
/**
|
||||||
|
* 合约专属关键字(持仓/止损/看点等)。
|
||||||
|
* AI 分析时追加在全局「额外关键字」之后。
|
||||||
|
*/
|
||||||
|
keywords: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** localStorage 中的合约列表与当前选中 */
|
||||||
|
export interface ContractStorage {
|
||||||
|
contracts: ContractItem[]
|
||||||
|
selectedId: string
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user