新增大单提醒
This commit is contained in:
parent
4dae5e5d57
commit
1ae0639fc2
@ -76,8 +76,9 @@ function upsertIntraday(
|
|||||||
/**
|
/**
|
||||||
* Merge tick detailinfos into quote.trades (newest first, dedupe, cap 200).
|
* Merge tick detailinfos into quote.trades (newest first, dedupe, cap 200).
|
||||||
* Mutates quote in place so Vue only invalidates trade-dependent effects.
|
* Mutates quote in place so Vue only invalidates trade-dependent effects.
|
||||||
|
* @returns newly inserted ticks (not already in quote.trades)
|
||||||
*/
|
*/
|
||||||
export function applyWsTick(quote: QuoteData, data: BaiduWsTickData): void {
|
export function applyWsTick(quote: QuoteData, data: BaiduWsTickData): TradeTick[] {
|
||||||
const incoming: TradeTick[] = [...(data.detailinfos ?? [])]
|
const incoming: TradeTick[] = [...(data.detailinfos ?? [])]
|
||||||
.reverse()
|
.reverse()
|
||||||
.map((t) => ({
|
.map((t) => ({
|
||||||
@ -87,6 +88,12 @@ export function applyWsTick(quote: QuoteData, data: BaiduWsTickData): void {
|
|||||||
side: t.bsFlag === 'B' ? 'B' : 'S',
|
side: t.bsFlag === 'B' ? 'B' : 'S',
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
const existingKeys = new Set(quote.trades.map(tradeKey))
|
||||||
|
const added: TradeTick[] = []
|
||||||
|
for (const t of incoming) {
|
||||||
|
if (!existingKeys.has(tradeKey(t))) added.push(t)
|
||||||
|
}
|
||||||
|
|
||||||
const seen = new Set<string>()
|
const seen = new Set<string>()
|
||||||
const merged: TradeTick[] = []
|
const merged: TradeTick[] = []
|
||||||
for (const t of [...incoming, ...quote.trades]) {
|
for (const t of [...incoming, ...quote.trades]) {
|
||||||
@ -97,6 +104,7 @@ export function applyWsTick(quote: QuoteData, data: BaiduWsTickData): void {
|
|||||||
if (merged.length >= MAX_TRADES) break
|
if (merged.length >= MAX_TRADES) break
|
||||||
}
|
}
|
||||||
quote.trades = merged
|
quote.trades = merged
|
||||||
|
return added
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -34,7 +34,7 @@
|
|||||||
:value="c.id"
|
:value="c.id"
|
||||||
/>
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
<el-button title="合约设置" @click="contractSettingsVisible = true">合约</el-button>
|
<el-button title="自选设置" @click="contractSettingsVisible = true">自选</el-button>
|
||||||
</div>
|
</div>
|
||||||
<SettingsDialog v-model="settingsVisible" />
|
<SettingsDialog v-model="settingsVisible" />
|
||||||
<ContractSettingsDialog v-model="contractSettingsVisible" />
|
<ContractSettingsDialog v-model="contractSettingsVisible" />
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
<el-dialog
|
<el-dialog
|
||||||
:model-value="modelValue"
|
:model-value="modelValue"
|
||||||
title="合约设置"
|
title="合约设置"
|
||||||
width="760px"
|
width="860px"
|
||||||
destroy-on-close
|
destroy-on-close
|
||||||
@update:model-value="emit('update:modelValue', $event)"
|
@update:model-value="emit('update:modelValue', $event)"
|
||||||
>
|
>
|
||||||
@ -25,6 +25,11 @@
|
|||||||
<el-table-column prop="variety" label="品种" width="70" />
|
<el-table-column prop="variety" label="品种" width="70" />
|
||||||
<el-table-column prop="exchange" label="交易所" width="90" />
|
<el-table-column prop="exchange" label="交易所" width="90" />
|
||||||
<el-table-column prop="largeOrderLots" label="大单阈值" width="88" />
|
<el-table-column prop="largeOrderLots" label="大单阈值" width="88" />
|
||||||
|
<el-table-column label="大单提醒" width="88">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ row.largeOrderAlertLots > 0 ? row.largeOrderAlertLots : '关' }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
<el-table-column prop="keywords" label="关键字" min-width="120" show-overflow-tooltip />
|
<el-table-column prop="keywords" label="关键字" min-width="120" show-overflow-tooltip />
|
||||||
<el-table-column label="操作" width="140" fixed="right">
|
<el-table-column label="操作" width="140" fixed="right">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
@ -74,6 +79,16 @@
|
|||||||
controls-position="right"
|
controls-position="right"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
<el-form-item label="大单提醒手数">
|
||||||
|
<el-input-number
|
||||||
|
v-model="form.largeOrderAlertLots"
|
||||||
|
:min="0"
|
||||||
|
:max="999999"
|
||||||
|
:step="10"
|
||||||
|
controls-position="right"
|
||||||
|
/>
|
||||||
|
<div class="form-tip">现手大于该值时弹窗提醒;填 0 关闭</div>
|
||||||
|
</el-form-item>
|
||||||
<el-form-item label="合约关键字">
|
<el-form-item label="合约关键字">
|
||||||
<el-input
|
<el-input
|
||||||
v-model="form.keywords"
|
v-model="form.keywords"
|
||||||
@ -119,6 +134,7 @@ const form = reactive({
|
|||||||
variety: '',
|
variety: '',
|
||||||
exchange: '',
|
exchange: '',
|
||||||
largeOrderLots: DEFAULT_CONTRACT.largeOrderLots,
|
largeOrderLots: DEFAULT_CONTRACT.largeOrderLots,
|
||||||
|
largeOrderAlertLots: DEFAULT_CONTRACT.largeOrderAlertLots,
|
||||||
keywords: '',
|
keywords: '',
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -128,6 +144,8 @@ function resetForm(seed?: Partial<ContractItem>) {
|
|||||||
form.variety = seed?.variety || ''
|
form.variety = seed?.variety || ''
|
||||||
form.exchange = seed?.exchange || ''
|
form.exchange = seed?.exchange || ''
|
||||||
form.largeOrderLots = seed?.largeOrderLots || DEFAULT_CONTRACT.largeOrderLots
|
form.largeOrderLots = seed?.largeOrderLots || DEFAULT_CONTRACT.largeOrderLots
|
||||||
|
form.largeOrderAlertLots =
|
||||||
|
seed?.largeOrderAlertLots ?? DEFAULT_CONTRACT.largeOrderAlertLots
|
||||||
form.keywords = seed?.keywords || ''
|
form.keywords = seed?.keywords || ''
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -136,6 +154,7 @@ function startAdd() {
|
|||||||
resetForm({
|
resetForm({
|
||||||
exchange: DEFAULT_CONTRACT.exchange,
|
exchange: DEFAULT_CONTRACT.exchange,
|
||||||
largeOrderLots: DEFAULT_CONTRACT.largeOrderLots,
|
largeOrderLots: DEFAULT_CONTRACT.largeOrderLots,
|
||||||
|
largeOrderAlertLots: DEFAULT_CONTRACT.largeOrderAlertLots,
|
||||||
})
|
})
|
||||||
formVisible.value = true
|
formVisible.value = true
|
||||||
}
|
}
|
||||||
@ -188,6 +207,7 @@ function saveForm() {
|
|||||||
variety: form.variety.trim(),
|
variety: form.variety.trim(),
|
||||||
exchange: form.exchange.trim(),
|
exchange: form.exchange.trim(),
|
||||||
largeOrderLots: form.largeOrderLots,
|
largeOrderLots: form.largeOrderLots,
|
||||||
|
largeOrderAlertLots: form.largeOrderAlertLots,
|
||||||
keywords: form.keywords.trim(),
|
keywords: form.keywords.trim(),
|
||||||
}
|
}
|
||||||
if (!payload.code) {
|
if (!payload.code) {
|
||||||
@ -233,4 +253,10 @@ function saveForm() {
|
|||||||
.toolbar {
|
.toolbar {
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form-tip {
|
||||||
|
margin-top: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -12,6 +12,7 @@ export const DEFAULT_CONTRACT: Omit<ContractItem, 'id'> = {
|
|||||||
name: '玻璃2609',
|
name: '玻璃2609',
|
||||||
exchange: '郑商所',
|
exchange: '郑商所',
|
||||||
largeOrderLots: 100,
|
largeOrderLots: 100,
|
||||||
|
largeOrderAlertLots: 0,
|
||||||
keywords: '',
|
keywords: '',
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -28,6 +29,9 @@ function normalizeItem(raw: Partial<ContractItem> & { code?: string }): Contract
|
|||||||
const lots = Number(raw.largeOrderLots)
|
const lots = Number(raw.largeOrderLots)
|
||||||
const largeOrderLots =
|
const largeOrderLots =
|
||||||
Number.isFinite(lots) && lots > 0 ? Math.floor(lots) : DEFAULT_CONTRACT.largeOrderLots
|
Number.isFinite(lots) && lots > 0 ? Math.floor(lots) : DEFAULT_CONTRACT.largeOrderLots
|
||||||
|
const alertLots = Number(raw.largeOrderAlertLots)
|
||||||
|
const largeOrderAlertLots =
|
||||||
|
Number.isFinite(alertLots) && alertLots > 0 ? Math.floor(alertLots) : 0
|
||||||
const keywords = String(raw.keywords ?? '').trim()
|
const keywords = String(raw.keywords ?? '').trim()
|
||||||
return {
|
return {
|
||||||
id: String(raw.id || createId()),
|
id: String(raw.id || createId()),
|
||||||
@ -36,6 +40,7 @@ function normalizeItem(raw: Partial<ContractItem> & { code?: string }): Contract
|
|||||||
name,
|
name,
|
||||||
exchange,
|
exchange,
|
||||||
largeOrderLots,
|
largeOrderLots,
|
||||||
|
largeOrderAlertLots,
|
||||||
keywords,
|
keywords,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,6 +23,7 @@ import {
|
|||||||
getPreviousTradingDate,
|
getPreviousTradingDate,
|
||||||
isWsTradingSession,
|
isWsTradingSession,
|
||||||
} from '../utils/tradingDate'
|
} from '../utils/tradingDate'
|
||||||
|
import { notifyLargeOrders } from '../utils/largeOrderAlert'
|
||||||
import type { WsConnectionStatus } from '../api/baidu/ws'
|
import type { WsConnectionStatus } from '../api/baidu/ws'
|
||||||
|
|
||||||
/** K 线周期(日/周/月) */
|
/** K 线周期(日/周/月) */
|
||||||
@ -139,7 +140,8 @@ export const useQuotaStore = defineStore('quota', () => {
|
|||||||
const product = msg.data.product
|
const product = msg.data.product
|
||||||
// Mutate in place — replacing quote.value forces every chart/UI to rebuild.
|
// Mutate in place — replacing quote.value forces every chart/UI to rebuild.
|
||||||
if (product === 'tick') {
|
if (product === 'tick') {
|
||||||
applyWsTick(quote.value, msg.data as BaiduWsTickData)
|
const added = applyWsTick(quote.value, msg.data as BaiduWsTickData)
|
||||||
|
notifyLargeOrders(added, activeContract())
|
||||||
} else if (product === 'snapshot') {
|
} else if (product === 'snapshot') {
|
||||||
applyWsSnapshot(quote.value, msg.data as BaiduWsSnapshotData)
|
applyWsSnapshot(quote.value, msg.data as BaiduWsSnapshotData)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -161,6 +161,10 @@ export interface ContractItem {
|
|||||||
* 用于分时成交「大单分析」饼图。
|
* 用于分时成交「大单分析」饼图。
|
||||||
*/
|
*/
|
||||||
largeOrderLots: number
|
largeOrderLots: number
|
||||||
|
/**
|
||||||
|
* 大单提醒手数:WS 实时成交现手大于该值时弹出提醒;0 表示关闭。
|
||||||
|
*/
|
||||||
|
largeOrderAlertLots: number
|
||||||
/**
|
/**
|
||||||
* 合约专属关键字(持仓/止损/看点等)。
|
* 合约专属关键字(持仓/止损/看点等)。
|
||||||
* AI 分析时追加在全局「额外关键字」之后。
|
* AI 分析时追加在全局「额外关键字」之后。
|
||||||
|
|||||||
23
src/utils/largeOrderAlert.ts
Normal file
23
src/utils/largeOrderAlert.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { ElNotification } from 'element-plus'
|
||||||
|
import type { ContractItem, TradeTick } from '../types'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 对 WS 新成交中超过「大单提醒」阈值的笔弹出 Notification。
|
||||||
|
* largeOrderAlertLots 为 0 时关闭提醒。
|
||||||
|
*/
|
||||||
|
export function notifyLargeOrders(trades: TradeTick[], contract: ContractItem): void {
|
||||||
|
const threshold = contract.largeOrderAlertLots
|
||||||
|
if (!threshold || threshold <= 0 || trades.length === 0) return
|
||||||
|
|
||||||
|
for (const t of trades) {
|
||||||
|
if (t.volume <= threshold) continue
|
||||||
|
const sideLabel = t.side === 'B' ? '买' : '卖'
|
||||||
|
ElNotification({
|
||||||
|
title: `大单提醒 · ${contract.name || contract.code}`,
|
||||||
|
message: `${t.time} ${sideLabel} ${t.volume}手 @ ${t.price.toFixed(2)}`,
|
||||||
|
type: t.side === 'B' ? 'error' : 'success',
|
||||||
|
duration: 4500,
|
||||||
|
position: 'top-right',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user