K线接口对接
This commit is contained in:
parent
5393aa60be
commit
5708ab2b14
66
_t.mjs
Normal file
66
_t.mjs
Normal file
@ -0,0 +1,66 @@
|
||||
import { writeFileSync } from 'fs'
|
||||
|
||||
const L = {
|
||||
intraday: '\u5206\u65f6',
|
||||
day: '\u65e5K',
|
||||
week: '\u5468K',
|
||||
month: '\u6708K',
|
||||
avg: '\u5747\u4ef7',
|
||||
volume: '\u6210\u4ea4\u91cf',
|
||||
}
|
||||
|
||||
const content = `<template>
|
||||
<div class="chart-panel card" v-loading="klineLoading">
|
||||
<el-tabs v-model="period" class="tabs" @tab-change="onTabChange">
|
||||
<el-tab-pane :label="L.intraday" name="intraday" />
|
||||
<el-tab-pane :label="L.day" name="day" />
|
||||
<el-tab-pane :label="L.week" name="week" />
|
||||
<el-tab-pane :label="L.month" name="month" />
|
||||
</el-tabs>
|
||||
<v-chart class="chart" :option="option" autoresize />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { use } from 'echarts/core'
|
||||
import { CanvasRenderer } from 'echarts/renderers'
|
||||
import { LineChart, BarChart, CandlestickChart } from 'echarts/charts'
|
||||
import {
|
||||
GridComponent,
|
||||
TooltipComponent,
|
||||
DataZoomComponent,
|
||||
LegendComponent,
|
||||
} from 'echarts/components'
|
||||
import type { EChartsOption } from 'echarts'
|
||||
import VChart from 'vue-echarts'
|
||||
import type { Candle, QuoteData } from '../../types'
|
||||
import type { KlinePeriod } from '../../composables/useQuote'
|
||||
|
||||
use([
|
||||
CanvasRenderer,
|
||||
LineChart,
|
||||
BarChart,
|
||||
CandlestickChart,
|
||||
GridComponent,
|
||||
TooltipComponent,
|
||||
DataZoomComponent,
|
||||
LegendComponent,
|
||||
])
|
||||
|
||||
/** Tab / series labels — keep as JS unicode to avoid file encoding corruption */
|
||||
const L = {
|
||||
intraday: '\\u5206\\u65f6',
|
||||
day: '\\u65e5K',
|
||||
week: '\\u5468K',
|
||||
month: '\\u6708K',
|
||||
avg: '\\u5747\\u4ef7',
|
||||
volume: '\\u6210\\u4ea4\\u91cf',
|
||||
}
|
||||
|
||||
// Fix: the above would be literal backslash-u. Write real chars via template:
|
||||
void 0
|
||||
</script>
|
||||
`
|
||||
writeFileSync('src/components/quote/ChartPanel.vue', 'placeholder')
|
||||
console.log('skip')
|
||||
@ -7,7 +7,12 @@
|
||||
<section class="quote-section">
|
||||
<QuoteStats v-if="quote" :quote="quote" />
|
||||
<div class="quote-grid">
|
||||
<ChartPanel v-if="quote" :quote="quote" />
|
||||
<ChartPanel
|
||||
v-if="quote"
|
||||
:quote="quote"
|
||||
:kline-loading="klineLoading"
|
||||
:load-kline="loadKline"
|
||||
/>
|
||||
<aside class="side">
|
||||
<OrderBook v-if="quote" :quote="quote" />
|
||||
<TradeTape v-if="quote" :quote="quote" />
|
||||
@ -50,7 +55,7 @@ import { useNews } from './composables/useNews'
|
||||
import { usePositions } from './composables/usePositions'
|
||||
import { useAiAdvice } from './composables/useAiAdvice'
|
||||
|
||||
const { data: quote } = useQuote()
|
||||
const { data: quote, klineLoading, loadKline } = useQuote()
|
||||
const { data: news, loading: newsLoading, error: newsError, refresh: refreshNews } = useNews()
|
||||
const { data: positions } = usePositions()
|
||||
const { data: advice, loading: adviceLoading, refresh: refreshAdvice } = useAiAdvice()
|
||||
|
||||
42
src/api/baidu/mapKline.ts
Normal file
42
src/api/baidu/mapKline.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import type { Candle } from '../../types'
|
||||
import type { BaiduKlineResult } from './types'
|
||||
|
||||
function toNum(value: string | undefined, fallback = 0): number {
|
||||
if (value == null || value === '' || value === '--') return fallback
|
||||
const cleaned = value.replace(/[+,%]/g, '').trim()
|
||||
const n = Number(cleaned)
|
||||
return Number.isFinite(n) ? n : fallback
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 K 线 marketData 字符串。
|
||||
* 字段顺序(keys):timestamp, time, open, close, volume, high, low, ...
|
||||
* K 线之间用 `;` 分隔,字段用 `,` 分隔。
|
||||
*/
|
||||
export function parseKlineMarketData(marketData: string): Candle[] {
|
||||
if (!marketData) return []
|
||||
|
||||
const candles: Candle[] = []
|
||||
const rows = marketData.split(';').filter(Boolean)
|
||||
|
||||
for (const row of rows) {
|
||||
const parts = row.split(',')
|
||||
// timestamp, time, open, close, volume, high, low
|
||||
if (parts.length < 7) continue
|
||||
|
||||
candles.push({
|
||||
date: parts[1] ?? '',
|
||||
open: toNum(parts[2]),
|
||||
close: toNum(parts[3]),
|
||||
volume: toNum(parts[4]),
|
||||
high: toNum(parts[5]),
|
||||
low: toNum(parts[6]),
|
||||
})
|
||||
}
|
||||
|
||||
return candles
|
||||
}
|
||||
|
||||
export function mapBaiduKlineToCandles(result: BaiduKlineResult): Candle[] {
|
||||
return parseKlineMarketData(result.newMarketData?.marketData ?? '')
|
||||
}
|
||||
@ -59,10 +59,7 @@ function parseTrades(result: BaiduQuotationResult): TradeTick[] {
|
||||
}))
|
||||
}
|
||||
|
||||
export function mapBaiduQuotationToQuote(
|
||||
result: BaiduQuotationResult,
|
||||
fallback?: QuoteData,
|
||||
): QuoteData {
|
||||
export function mapBaiduQuotationToQuote(result: BaiduQuotationResult): QuoteData {
|
||||
const op = result.pankouinfos?.origin_pankou
|
||||
const cur = result.cur
|
||||
const basic = result.basicinfos
|
||||
@ -120,7 +117,8 @@ export function mapBaiduQuotationToQuote(
|
||||
buyRatio,
|
||||
sellRatio,
|
||||
intraday: parseIntraday(result),
|
||||
candles: fallback?.candles ?? { day: [], week: [], month: [] },
|
||||
// K 线按需加载,映射分时时不带入 mock/旧缓存
|
||||
candles: { day: [], week: [], month: [] },
|
||||
orderBook: { asks, bids },
|
||||
trades: parseTrades(result),
|
||||
}
|
||||
|
||||
@ -1,10 +1,22 @@
|
||||
import { baiduHttp } from '../http'
|
||||
import type { BaiduQuotationResponse, BaiduQuotationResult } from './types'
|
||||
import type {
|
||||
BaiduKlineResponse,
|
||||
BaiduKlineResult,
|
||||
BaiduKlineType,
|
||||
BaiduQuotationResponse,
|
||||
BaiduQuotationResult,
|
||||
} from './types'
|
||||
|
||||
export interface GetStockQuotationParams {
|
||||
code: string
|
||||
}
|
||||
|
||||
export interface GetStockKlineParams {
|
||||
code: string
|
||||
/** 1=日K,2=周K,3=月K */
|
||||
ktype: BaiduKlineType
|
||||
}
|
||||
|
||||
/**
|
||||
* 百度财经 — 期货盘口 / 分时 / 行情快照
|
||||
* 仅页面打开时拉取一次;实时更新后续对接 WebSocket。
|
||||
@ -52,3 +64,42 @@ export async function getStockQuotation(
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* 百度财经 — 期货 K 线(日/周/月)
|
||||
* marketData 为 `;` 分隔的 K 线,每根内字段用 `,` 分隔。
|
||||
*/
|
||||
export async function getStockKline(
|
||||
params: GetStockKlineParams,
|
||||
): Promise<BaiduKlineResult> {
|
||||
const { data } = await baiduHttp.get<BaiduKlineResponse>(
|
||||
'/selfselect/getstockquotation',
|
||||
{
|
||||
params: {
|
||||
all: 1,
|
||||
code: params.code,
|
||||
isIndex: false,
|
||||
isBk: false,
|
||||
isBlock: false,
|
||||
isFutures: true,
|
||||
isStock: false,
|
||||
newFormat: 1,
|
||||
ktype: params.ktype,
|
||||
market_type: 'ab',
|
||||
group: 'quotation_futures_kline',
|
||||
finClientType: 'pc',
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
if (data.ResultCode !== '0') {
|
||||
throw new Error(`K线接口失败: ResultCode=${data.ResultCode}`)
|
||||
}
|
||||
|
||||
const result = data.Result
|
||||
if (!result?.newMarketData?.marketData) {
|
||||
throw new Error('K线接口返回空 marketData')
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@ -102,6 +102,26 @@ export interface BaiduQuotationResponse {
|
||||
Result: BaiduQuotationResult | BaiduQuotationResult[]
|
||||
}
|
||||
|
||||
/** K 线周期:1=日K,2=周K,3=月K */
|
||||
export type BaiduKlineType = 1 | 2 | 3
|
||||
|
||||
/** 百度财经 — 期货 K 线 newMarketData(marketData 为 ; 分隔的 CSV 串) */
|
||||
export interface BaiduKlineNewMarketData {
|
||||
headers: string[]
|
||||
keys: string[]
|
||||
marketData: string
|
||||
}
|
||||
|
||||
export interface BaiduKlineResult {
|
||||
newMarketData: BaiduKlineNewMarketData
|
||||
}
|
||||
|
||||
export interface BaiduKlineResponse {
|
||||
QueryID: string
|
||||
ResultCode: string
|
||||
Result: BaiduKlineResult
|
||||
}
|
||||
|
||||
/** 百度财经 — 期货相关新闻单条 */
|
||||
export interface BaiduFuturesNewsItem {
|
||||
loc: string
|
||||
|
||||
@ -1,11 +1,10 @@
|
||||
<template>
|
||||
<div class="chart-panel card">
|
||||
<el-tabs v-model="period" class="tabs">
|
||||
<el-tab-pane label="分时" name="intraday" />
|
||||
<el-tab-pane label="五日" name="five" />
|
||||
<el-tab-pane label="日K" name="day" />
|
||||
<el-tab-pane label="周K" name="week" />
|
||||
<el-tab-pane label="月K" name="month" />
|
||||
<div class="chart-panel card" v-loading="klineLoading">
|
||||
<el-tabs v-model="period" class="tabs" @tab-change="onTabChange">
|
||||
<el-tab-pane :label="TAB.intraday" name="intraday" />
|
||||
<el-tab-pane :label="TAB.day" name="day" />
|
||||
<el-tab-pane :label="TAB.week" name="week" />
|
||||
<el-tab-pane :label="TAB.month" name="month" />
|
||||
</el-tabs>
|
||||
<v-chart class="chart" :option="option" autoresize />
|
||||
</div>
|
||||
@ -25,6 +24,7 @@ import {
|
||||
import type { EChartsOption } from 'echarts'
|
||||
import VChart from 'vue-echarts'
|
||||
import type { Candle, QuoteData } from '../../types'
|
||||
import type { KlinePeriod } from '../../composables/useQuote'
|
||||
|
||||
use([
|
||||
CanvasRenderer,
|
||||
@ -37,7 +37,17 @@ use([
|
||||
LegendComponent,
|
||||
])
|
||||
|
||||
type ChartPeriod = 'intraday' | 'five' | 'day' | 'week' | 'month'
|
||||
/** ASCII-safe unicode escapes ? avoids Windows encoding corruption */
|
||||
const TAB = {
|
||||
intraday: '\u5206\u65f6',
|
||||
day: '\u65e5K',
|
||||
week: '\u5468K',
|
||||
month: '\u6708K',
|
||||
avg: '\u5747\u4ef7',
|
||||
volume: '\u6210\u4ea4\u91cf',
|
||||
}
|
||||
|
||||
type ChartPeriod = 'intraday' | KlinePeriod
|
||||
|
||||
interface ColorParam {
|
||||
dataIndex?: number
|
||||
@ -45,22 +55,27 @@ interface ColorParam {
|
||||
|
||||
const props = defineProps<{
|
||||
quote: QuoteData
|
||||
klineLoading?: boolean
|
||||
loadKline: (period: KlinePeriod) => Promise<Candle[]>
|
||||
}>()
|
||||
|
||||
const period = ref<ChartPeriod>('intraday')
|
||||
|
||||
const option = computed((): EChartsOption => {
|
||||
if (period.value === 'intraday' || period.value === 'five') {
|
||||
return buildIntradayOption(props.quote, period.value === 'five')
|
||||
async function onTabChange(name: string | number) {
|
||||
if (name === 'day' || name === 'week' || name === 'month') {
|
||||
await props.loadKline(name)
|
||||
}
|
||||
const key = period.value === 'day' ? 'day' : period.value === 'week' ? 'week' : 'month'
|
||||
return buildCandleOption(props.quote.candles[key])
|
||||
}
|
||||
|
||||
const option = computed((): EChartsOption => {
|
||||
if (period.value === 'intraday') {
|
||||
return buildIntradayOption(props.quote)
|
||||
}
|
||||
return buildCandleOption(props.quote.candles[period.value])
|
||||
})
|
||||
|
||||
function buildIntradayOption(quote: QuoteData, fiveDay: boolean): EChartsOption {
|
||||
const points = fiveDay
|
||||
? [...quote.intraday, ...quote.intraday.map((p) => ({ ...p, price: p.price + 1.5 }))]
|
||||
: quote.intraday
|
||||
function buildIntradayOption(quote: QuoteData): EChartsOption {
|
||||
const points = quote.intraday
|
||||
const times = points.map((p) => p.time)
|
||||
const prices = points.map((p) => p.price)
|
||||
const avgs = points.map((p) => p.avg)
|
||||
@ -69,7 +84,7 @@ function buildIntradayOption(quote: QuoteData, fiveDay: boolean): EChartsOption
|
||||
|
||||
return {
|
||||
animation: false,
|
||||
legend: { data: ['分时', '均价'], top: 0, textStyle: { fontSize: 11 } },
|
||||
legend: { data: [TAB.intraday, TAB.avg], top: 0, textStyle: { fontSize: 11 } },
|
||||
tooltip: { trigger: 'axis' },
|
||||
axisPointer: { link: [{ xAxisIndex: 'all' }] },
|
||||
grid: [
|
||||
@ -121,7 +136,7 @@ function buildIntradayOption(quote: QuoteData, fiveDay: boolean): EChartsOption
|
||||
dataZoom: [{ type: 'inside', xAxisIndex: [0, 1] }],
|
||||
series: [
|
||||
{
|
||||
name: '分时',
|
||||
name: TAB.intraday,
|
||||
type: 'line',
|
||||
data: prices,
|
||||
showSymbol: false,
|
||||
@ -143,7 +158,7 @@ function buildIntradayOption(quote: QuoteData, fiveDay: boolean): EChartsOption
|
||||
yAxisIndex: 0,
|
||||
},
|
||||
{
|
||||
name: '均价',
|
||||
name: TAB.avg,
|
||||
type: 'line',
|
||||
data: avgs,
|
||||
showSymbol: false,
|
||||
@ -152,7 +167,7 @@ function buildIntradayOption(quote: QuoteData, fiveDay: boolean): EChartsOption
|
||||
yAxisIndex: 0,
|
||||
},
|
||||
{
|
||||
name: '成交量',
|
||||
name: TAB.volume,
|
||||
type: 'bar',
|
||||
data: vols,
|
||||
itemStyle: {
|
||||
|
||||
@ -1,21 +1,43 @@
|
||||
import { onMounted, ref } from 'vue'
|
||||
import type { QuoteData } from '../types'
|
||||
import type { Candle, QuoteData } from '../types'
|
||||
import { quoteMock } from '../mocks/quote'
|
||||
import { contractConfig } from '../config/contract'
|
||||
import { getStockQuotation } from '../api/baidu/quotation'
|
||||
import { getStockKline, getStockQuotation } from '../api/baidu/quotation'
|
||||
import { mapBaiduQuotationToQuote } from '../api/baidu/mapQuote'
|
||||
import { mapBaiduKlineToCandles } from '../api/baidu/mapKline'
|
||||
import type { BaiduKlineType } from '../api/baidu/types'
|
||||
|
||||
export type KlinePeriod = 'day' | 'week' | 'month'
|
||||
|
||||
const KTYPE_MAP: Record<KlinePeriod, BaiduKlineType> = {
|
||||
day: 1,
|
||||
week: 2,
|
||||
month: 3,
|
||||
}
|
||||
|
||||
export function useQuote() {
|
||||
const data = ref<QuoteData>(structuredClone(quoteMock))
|
||||
const loading = ref(false)
|
||||
const klineLoading = ref(false)
|
||||
const error = ref<unknown>(null)
|
||||
|
||||
/** 已加载过的周期,避免重复请求 */
|
||||
const loadedKlines = ref<Record<KlinePeriod, boolean>>({
|
||||
day: false,
|
||||
week: false,
|
||||
month: false,
|
||||
})
|
||||
|
||||
async function refresh() {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const result = await getStockQuotation({ code: contractConfig.code })
|
||||
data.value = mapBaiduQuotationToQuote(result, data.value)
|
||||
const quote = mapBaiduQuotationToQuote(result)
|
||||
// 分时刷新时清空 K 线缓存,点击 Tab 再拉
|
||||
quote.candles = { day: [], week: [], month: [] }
|
||||
loadedKlines.value = { day: false, week: false, month: false }
|
||||
data.value = quote
|
||||
} catch (e) {
|
||||
error.value = e
|
||||
console.error('[useQuote] 拉取行情失败', e)
|
||||
@ -24,10 +46,36 @@ export function useQuote() {
|
||||
}
|
||||
}
|
||||
|
||||
// 页面打开时请求一次;实时数据后续对接 WebSocket
|
||||
/** 按需加载日/周/月 K 线;已加载则跳过 */
|
||||
async function loadKline(period: KlinePeriod): Promise<Candle[]> {
|
||||
if (loadedKlines.value[period]) {
|
||||
return data.value.candles[period]
|
||||
}
|
||||
|
||||
klineLoading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const result = await getStockKline({
|
||||
code: contractConfig.code,
|
||||
ktype: KTYPE_MAP[period],
|
||||
})
|
||||
const candles = mapBaiduKlineToCandles(result)
|
||||
data.value.candles[period] = candles
|
||||
loadedKlines.value[period] = true
|
||||
return candles
|
||||
} catch (e) {
|
||||
error.value = e
|
||||
console.error(`[useQuote] 拉取${period}K线失败`, e)
|
||||
throw e
|
||||
} finally {
|
||||
klineLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 页面打开时只请求分时/盘口;K 线点击 Tab 再拉
|
||||
onMounted(() => {
|
||||
void refresh()
|
||||
})
|
||||
|
||||
return { data, loading, error, refresh }
|
||||
return { data, loading, klineLoading, error, refresh, loadKline }
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user