新增分时成交大单统计
This commit is contained in:
parent
7728061d00
commit
4aebee0d4d
@ -16,6 +16,7 @@
|
||||
<aside class="side">
|
||||
<OrderBook v-if="quote" :quote="quote" />
|
||||
<TradeTape v-if="quote" :quote="quote" />
|
||||
<LargeOrderAnalysis v-if="quote" :quote="quote" />
|
||||
</aside>
|
||||
</div>
|
||||
</section>
|
||||
@ -47,6 +48,7 @@ import QuoteStats from './components/quote/QuoteStats.vue'
|
||||
import ChartPanel from './components/quote/ChartPanel.vue'
|
||||
import OrderBook from './components/quote/OrderBook.vue'
|
||||
import TradeTape from './components/quote/TradeTape.vue'
|
||||
import LargeOrderAnalysis from './components/quote/LargeOrderAnalysis.vue'
|
||||
import NewsList from './components/news/NewsList.vue'
|
||||
import PositionPanel from './components/position/PositionPanel.vue'
|
||||
import AiAdviceDrawer from './components/ai/AiAdviceDrawer.vue'
|
||||
|
||||
226
src/components/quote/LargeOrderAnalysis.vue
Normal file
226
src/components/quote/LargeOrderAnalysis.vue
Normal 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">大单阈值 > {{ 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>
|
||||
@ -8,4 +8,9 @@ export const contractConfig = {
|
||||
name: '玻璃2609',
|
||||
/** 交易所展示名 */
|
||||
exchange: '郑商所',
|
||||
/**
|
||||
* 大单手数阈值:现手大于该值计为大单,否则计为散单。
|
||||
* 用于分时成交「大单分析」饼图。
|
||||
*/
|
||||
largeOrderLots: 100,
|
||||
} as const
|
||||
|
||||
@ -109,5 +109,9 @@ export const quoteMock: QuoteData = {
|
||||
{ time: '11:26', price: 934.0, volume: 3, side: 'B' },
|
||||
{ time: '11:25', price: 935.0, volume: 27, side: 'S' },
|
||||
{ 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' },
|
||||
],
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user