新增分时成交大单统计
This commit is contained in:
parent
7728061d00
commit
4aebee0d4d
@ -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'
|
||||||
|
|||||||
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',
|
name: '玻璃2609',
|
||||||
/** 交易所展示名 */
|
/** 交易所展示名 */
|
||||||
exchange: '郑商所',
|
exchange: '郑商所',
|
||||||
|
/**
|
||||||
|
* 大单手数阈值:现手大于该值计为大单,否则计为散单。
|
||||||
|
* 用于分时成交「大单分析」饼图。
|
||||||
|
*/
|
||||||
|
largeOrderLots: 100,
|
||||||
} as const
|
} as const
|
||||||
|
|||||||
@ -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' },
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user