新增分时成交大单统计

This commit is contained in:
dongzp 2026-07-21 16:43:24 +08:00
parent 7728061d00
commit 4aebee0d4d
4 changed files with 237 additions and 0 deletions

View File

@ -16,6 +16,7 @@
<aside class="side"> <aside class="side">
<OrderBook v-if="quote" :quote="quote" /> <OrderBook v-if="quote" :quote="quote" />
<TradeTape v-if="quote" :quote="quote" /> <TradeTape v-if="quote" :quote="quote" />
<LargeOrderAnalysis v-if="quote" :quote="quote" />
</aside> </aside>
</div> </div>
</section> </section>
@ -47,6 +48,7 @@ import QuoteStats from './components/quote/QuoteStats.vue'
import ChartPanel from './components/quote/ChartPanel.vue' import ChartPanel from './components/quote/ChartPanel.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 NewsList from './components/news/NewsList.vue' import NewsList from './components/news/NewsList.vue'
import PositionPanel from './components/position/PositionPanel.vue' import PositionPanel from './components/position/PositionPanel.vue'
import AiAdviceDrawer from './components/ai/AiAdviceDrawer.vue' import AiAdviceDrawer from './components/ai/AiAdviceDrawer.vue'

View File

@ -0,0 +1,226 @@
<template>
<div class="large-order card">
<div class="title">大单分析</div>
<div class="body">
<div class="legend left">
<div v-for="item in buyItems" :key="item.key" class="legend-item">
<span class="swatch" :style="{ background: item.color }" />
<div class="meta">
<div class="name">{{ item.name }}</div>
<div class="pct">{{ item.pct }}%</div>
</div>
</div>
</div>
<v-chart class="chart" :option="option" autoresize />
<div class="legend right">
<div v-for="item in sellItems" :key="item.key" class="legend-item">
<span class="swatch" :style="{ background: item.color }" />
<div class="meta">
<div class="name">{{ item.name }}</div>
<div class="pct">{{ item.pct }}%</div>
</div>
</div>
</div>
</div>
<div class="hint">大单阈值 &gt; {{ threshold }} 手</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { use } from 'echarts/core'
import { CanvasRenderer } from 'echarts/renderers'
import { PieChart } from 'echarts/charts'
import { TooltipComponent } from 'echarts/components'
import type { EChartsOption } from 'echarts'
import VChart from 'vue-echarts'
import { contractConfig } from '../../config/contract'
import type { QuoteData, TradeTick } from '../../types'
use([CanvasRenderer, PieChart, TooltipComponent])
const props = defineProps<{
quote: QuoteData
}>()
const threshold = contractConfig.largeOrderLots
const COLORS = {
largeBuy: '#f0435c',
retailBuy: '#8b4a3a',
largeSell: '#2ecc71',
retailSell: '#1a7a4c',
} as const
interface Bucket {
key: keyof typeof COLORS
name: string
color: string
value: number
pct: string
}
function aggregate(trades: TradeTick[]) {
let largeBuy = 0
let largeSell = 0
let retailBuy = 0
let retailSell = 0
for (const t of trades) {
const isLarge = t.volume > threshold
if (t.side === 'B') {
if (isLarge) largeBuy += t.volume
else retailBuy += t.volume
} else if (isLarge) largeSell += t.volume
else retailSell += t.volume
}
return { largeBuy, largeSell, retailBuy, retailSell }
}
function toItems(
buckets: ReturnType<typeof aggregate>,
): { buyItems: Bucket[]; sellItems: Bucket[]; data: Bucket[] } {
const total =
buckets.largeBuy + buckets.largeSell + buckets.retailBuy + buckets.retailSell
const pct = (v: number) => (total > 0 ? ((v / total) * 100).toFixed(0) : '0')
const buyItems: Bucket[] = [
{
key: 'largeBuy',
name: '大单主买',
color: COLORS.largeBuy,
value: buckets.largeBuy,
pct: pct(buckets.largeBuy),
},
{
key: 'retailBuy',
name: '散单主买',
color: COLORS.retailBuy,
value: buckets.retailBuy,
pct: pct(buckets.retailBuy),
},
]
const sellItems: Bucket[] = [
{
key: 'largeSell',
name: '大单主卖',
color: COLORS.largeSell,
value: buckets.largeSell,
pct: pct(buckets.largeSell),
},
{
key: 'retailSell',
name: '散单主卖',
color: COLORS.retailSell,
value: buckets.retailSell,
pct: pct(buckets.retailSell),
},
]
return { buyItems, sellItems, data: [...buyItems, ...sellItems] }
}
const stats = computed(() => toItems(aggregate(props.quote.trades)))
const buyItems = computed(() => stats.value.buyItems)
const sellItems = computed(() => stats.value.sellItems)
const option = computed((): EChartsOption => {
const data = stats.value.data.map((d) => ({
name: d.name,
value: d.value,
itemStyle: { color: d.color },
}))
return {
tooltip: {
trigger: 'item',
formatter: '{b}<br/>手数: {c}<br/>占比: {d}%',
},
series: [
{
type: 'pie',
radius: ['0%', '78%'],
center: ['50%', '50%'],
avoidLabelOverlap: true,
label: { show: false },
labelLine: { show: false },
data,
},
],
}
})
</script>
<style scoped>
.large-order {
padding: 12px;
margin-top: 12px;
}
.title {
font-size: 13px;
font-weight: 600;
margin-bottom: 8px;
}
.body {
display: grid;
grid-template-columns: 72px 1fr 72px;
align-items: center;
gap: 4px;
min-height: 150px;
}
.chart {
height: 140px;
width: 100%;
}
.legend {
display: flex;
flex-direction: column;
gap: 14px;
}
.legend-item {
display: flex;
align-items: flex-start;
gap: 6px;
}
.swatch {
width: 10px;
height: 10px;
border-radius: 2px;
flex-shrink: 0;
margin-top: 3px;
}
.meta {
min-width: 0;
}
.name {
font-size: 11px;
color: var(--text-primary);
line-height: 1.3;
white-space: nowrap;
}
.pct {
font-size: 12px;
font-weight: 600;
color: #0d9488;
font-variant-numeric: tabular-nums;
line-height: 1.3;
}
.hint {
margin-top: 4px;
font-size: 11px;
color: var(--text-secondary);
text-align: center;
}
</style>

View File

@ -8,4 +8,9 @@ export const contractConfig = {
name: '玻璃2609', name: '玻璃2609',
/** 交易所展示名 */ /** 交易所展示名 */
exchange: '郑商所', exchange: '郑商所',
/**
* 大单手数阈值:现手大于该值计为大单,否则计为散单。
* 用于分时成交「大单分析」饼图。
*/
largeOrderLots: 100,
} as const } as const

View File

@ -109,5 +109,9 @@ export const quoteMock: QuoteData = {
{ time: '11:26', price: 934.0, volume: 3, side: 'B' }, { time: '11:26', price: 934.0, volume: 3, side: 'B' },
{ time: '11:25', price: 935.0, volume: 27, side: 'S' }, { time: '11:25', price: 935.0, volume: 27, side: 'S' },
{ time: '11:25', price: 936.0, volume: 9, side: 'B' }, { time: '11:25', price: 936.0, volume: 9, side: 'B' },
{ time: '11:24', price: 936.0, volume: 5, side: 'B' },
{ time: '11:24', price: 935.0, volume: 7, side: 'S' },
{ time: '11:23', price: 935.0, volume: 45, side: 'S' },
{ time: '11:23', price: 934.0, volume: 38, side: 'B' },
], ],
} }