feat: 添加自定义 TradingView 数据源实现;更新 TradingViewChart 组件以支持新数据源;优化交易视图中的图表配置
This commit is contained in:
295
src/tradingview/datafeed.ts
Normal file
295
src/tradingview/datafeed.ts
Normal file
@@ -0,0 +1,295 @@
|
||||
import type {
|
||||
Bar,
|
||||
DatafeedConfiguration,
|
||||
HistoryCallback,
|
||||
IDatafeedChartApi,
|
||||
IExternalDatafeed,
|
||||
LibrarySymbolInfo,
|
||||
OnReadyCallback,
|
||||
PeriodParams,
|
||||
ResolutionString,
|
||||
ResolveCallback,
|
||||
SearchSymbolsCallback,
|
||||
SubscribeBarsCallback,
|
||||
} from "#/datafeed-api";
|
||||
|
||||
/**
|
||||
* 自定义 TradingView Datafeed 实现
|
||||
* 用于从后端 API 获取 K 线数据
|
||||
*/
|
||||
export class CustomDatafeed implements IDatafeedChartApi, IExternalDatafeed {
|
||||
private apiUrl: string;
|
||||
private updateInterval: number;
|
||||
private subscribers: Map<string, { callback: SubscribeBarsCallback; resolution: ResolutionString }> = new Map();
|
||||
|
||||
constructor(apiUrl: string, updateInterval = 1000) {
|
||||
this.apiUrl = apiUrl;
|
||||
this.updateInterval = updateInterval;
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化配置
|
||||
*/
|
||||
onReady(callback: OnReadyCallback): void {
|
||||
console.log("[CustomDatafeed]: onReady called");
|
||||
|
||||
const config: DatafeedConfiguration = {
|
||||
// 支持的交易所
|
||||
exchanges: [
|
||||
{ value: "", name: "All Exchanges", desc: "" },
|
||||
],
|
||||
// 支持的品种类型
|
||||
symbols_types: [
|
||||
{ name: "All types", value: "" },
|
||||
{ name: "Stock", value: "stock" },
|
||||
{ name: "Crypto", value: "crypto" },
|
||||
],
|
||||
// 支持的时间周期
|
||||
supported_resolutions: [
|
||||
"1" as ResolutionString,
|
||||
"5" as ResolutionString,
|
||||
"15" as ResolutionString,
|
||||
"30" as ResolutionString,
|
||||
"60" as ResolutionString,
|
||||
"240" as ResolutionString,
|
||||
"1D" as ResolutionString,
|
||||
"1W" as ResolutionString,
|
||||
"1M" as ResolutionString,
|
||||
],
|
||||
supports_marks: false,
|
||||
supports_timescale_marks: false,
|
||||
supports_time: true,
|
||||
};
|
||||
|
||||
setTimeout(() => callback(config), 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 搜索品种
|
||||
*/
|
||||
searchSymbols(
|
||||
userInput: string,
|
||||
exchange: string,
|
||||
symbolType: string,
|
||||
onResult: SearchSymbolsCallback,
|
||||
): void {
|
||||
console.log("[CustomDatafeed]: searchSymbols", userInput, exchange, symbolType);
|
||||
|
||||
// TODO: 调用后端 API 搜索品种
|
||||
// const results = await client.api.market.search.get({ query: { keyword: userInput, exchange, type: symbolType } })
|
||||
|
||||
// 模拟搜索结果
|
||||
const results = [
|
||||
{
|
||||
symbol: "AAPL",
|
||||
full_name: "NASDAQ:AAPL",
|
||||
description: "Apple Inc.",
|
||||
exchange: "NASDAQ",
|
||||
ticker: "AAPL",
|
||||
type: "stock",
|
||||
},
|
||||
];
|
||||
|
||||
onResult(results);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析品种信息
|
||||
*/
|
||||
resolveSymbol(
|
||||
symbolName: string,
|
||||
onResolve: ResolveCallback,
|
||||
onError: (reason: string) => void,
|
||||
): void {
|
||||
console.log("[CustomDatafeed]: resolveSymbol", symbolName);
|
||||
|
||||
// TODO: 调用后端 API 获取品种信息
|
||||
// const symbolInfo = await client.api.market.symbol[symbolName].get()
|
||||
|
||||
// 模拟品种信息
|
||||
const symbolInfo: LibrarySymbolInfo = {
|
||||
name: symbolName,
|
||||
ticker: symbolName,
|
||||
description: symbolName,
|
||||
type: "crypto",
|
||||
session: "24x7",
|
||||
exchange: "Binance",
|
||||
listed_exchange: "Binance",
|
||||
timezone: "Etc/UTC",
|
||||
format: "price",
|
||||
pricescale: 100,
|
||||
minmov: 1,
|
||||
has_intraday: true,
|
||||
has_daily: true,
|
||||
has_weekly_and_monthly: true,
|
||||
supported_resolutions: [
|
||||
"1" as ResolutionString,
|
||||
"5" as ResolutionString,
|
||||
"15" as ResolutionString,
|
||||
"30" as ResolutionString,
|
||||
"60" as ResolutionString,
|
||||
"240" as ResolutionString,
|
||||
"1D" as ResolutionString,
|
||||
"1W" as ResolutionString,
|
||||
"1M" as ResolutionString,
|
||||
],
|
||||
intraday_multipliers: ["1", "5", "15", "30", "60", "240"],
|
||||
volume_precision: 2,
|
||||
data_status: "streaming",
|
||||
};
|
||||
|
||||
setTimeout(() => onResolve(symbolInfo), 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取历史 K 线数据
|
||||
*/
|
||||
getBars(
|
||||
symbolInfo: LibrarySymbolInfo,
|
||||
resolution: ResolutionString,
|
||||
periodParams: PeriodParams,
|
||||
onResult: HistoryCallback,
|
||||
onError: (reason: string) => void,
|
||||
): void {
|
||||
const { from, to, firstDataRequest } = periodParams;
|
||||
console.log("[CustomDatafeed]: getBars", {
|
||||
symbol: symbolInfo.name,
|
||||
resolution,
|
||||
from: new Date(from * 1000).toISOString(),
|
||||
to: new Date(to * 1000).toISOString(),
|
||||
firstDataRequest,
|
||||
});
|
||||
|
||||
// TODO: 调用后端 API 获取 K 线数据
|
||||
// client.api.market.kline.get({
|
||||
// query: {
|
||||
// symbol: symbolInfo.name,
|
||||
// resolution,
|
||||
// from,
|
||||
// to,
|
||||
// }
|
||||
// }).then(response => {
|
||||
// const bars = response.data.map(item => ({
|
||||
// time: item.time * 1000,
|
||||
// open: item.open,
|
||||
// high: item.high,
|
||||
// low: item.low,
|
||||
// close: item.close,
|
||||
// volume: item.volume,
|
||||
// }))
|
||||
// onResult(bars, { noData: bars.length === 0 })
|
||||
// }).catch(error => {
|
||||
// onError(error.message)
|
||||
// })
|
||||
|
||||
// 模拟 K 线数据
|
||||
const bars: Bar[] = this.generateMockBars(from, to, resolution);
|
||||
|
||||
if (bars.length === 0) {
|
||||
onResult([], { noData: true });
|
||||
}
|
||||
else {
|
||||
onResult(bars, { noData: false });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 订阅实时数据
|
||||
*/
|
||||
subscribeBars(
|
||||
symbolInfo: LibrarySymbolInfo,
|
||||
resolution: ResolutionString,
|
||||
onTick: SubscribeBarsCallback,
|
||||
listenerGuid: string,
|
||||
onResetCacheNeededCallback: () => void,
|
||||
): void {
|
||||
console.log("[CustomDatafeed]: subscribeBars", {
|
||||
symbol: symbolInfo.name,
|
||||
resolution,
|
||||
listenerGuid,
|
||||
});
|
||||
|
||||
this.subscribers.set(listenerGuid, { callback: onTick, resolution });
|
||||
|
||||
// TODO: 建立 WebSocket 连接获取实时数据
|
||||
// const ws = new WebSocket(`${this.apiUrl}/ws/kline?symbol=${symbolInfo.name}&resolution=${resolution}`)
|
||||
// ws.onmessage = (event) => {
|
||||
// const data = JSON.parse(event.data)
|
||||
// const bar: Bar = {
|
||||
// time: data.time * 1000,
|
||||
// open: data.open,
|
||||
// high: data.high,
|
||||
// low: data.low,
|
||||
// close: data.close,
|
||||
// volume: data.volume,
|
||||
// }
|
||||
// onTick(bar)
|
||||
// }
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消订阅实时数据
|
||||
*/
|
||||
unsubscribeBars(listenerGuid: string): void {
|
||||
console.log("[CustomDatafeed]: unsubscribeBars", listenerGuid);
|
||||
this.subscribers.delete(listenerGuid);
|
||||
|
||||
// TODO: 关闭 WebSocket 连接
|
||||
// ws.close()
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成模拟 K 线数据(用于测试)
|
||||
*/
|
||||
private generateMockBars(from: number, to: number, resolution: ResolutionString): Bar[] {
|
||||
const bars: Bar[] = [];
|
||||
const interval = this.getIntervalInSeconds(resolution);
|
||||
let currentTime = from * 1000;
|
||||
let lastClose = 100 + Math.random() * 10;
|
||||
|
||||
while (currentTime <= to * 1000) {
|
||||
const open = lastClose;
|
||||
const change = (Math.random() - 0.5) * 5;
|
||||
const close = open + change;
|
||||
const high = Math.max(open, close) + Math.random() * 2;
|
||||
const low = Math.min(open, close) - Math.random() * 2;
|
||||
|
||||
bars.push({
|
||||
time: currentTime,
|
||||
open,
|
||||
high,
|
||||
low,
|
||||
close,
|
||||
volume: Math.random() * 1000000,
|
||||
});
|
||||
|
||||
lastClose = close;
|
||||
currentTime += interval * 1000;
|
||||
}
|
||||
|
||||
return bars;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取时间周期对应的秒数
|
||||
*/
|
||||
private getIntervalInSeconds(resolution: ResolutionString): number {
|
||||
const match = resolution.match(/^(\d+)([DWMH]?)$/);
|
||||
if (!match)
|
||||
return 60;
|
||||
|
||||
const value = Number.parseInt(match[1]);
|
||||
const unit = match[2];
|
||||
|
||||
switch (unit) {
|
||||
case "D":
|
||||
return value * 86400;
|
||||
case "W":
|
||||
return value * 604800;
|
||||
case "M":
|
||||
return value * 2592000;
|
||||
default:
|
||||
return value * 60;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user