feat: 重构 TradingView 数据源实现,更新数据源类名为 RWADatafeed;优化 TradingViewChart 组件的使用方式

This commit is contained in:
2025-12-30 02:03:45 +07:00
parent 4a313f2af0
commit bdd2812ae9
3 changed files with 62 additions and 29 deletions

View File

@@ -13,16 +13,19 @@ import type {
SubscribeBarsCallback, SubscribeBarsCallback,
} from "#/datafeed-api"; } from "#/datafeed-api";
const { VITE_TRADINGVIEW_DATA_API_URL } = useEnv();
/** /**
* 自定义 TradingView Datafeed 实现 * 自定义 TradingView Datafeed 实现
* 用于从后端 API 获取 K 线数据 * 用于从后端 API 获取 K 线数据
*/ */
export class CustomDatafeed implements IDatafeedChartApi, IExternalDatafeed { export class RWADatafeed extends Datafeeds.UDFCompatibleDatafeed {
private apiUrl: string; private apiUrl: string = VITE_TRADINGVIEW_DATA_API_URL;
private updateInterval: number; private updateInterval: number;
private subscribers: Map<string, { callback: SubscribeBarsCallback; resolution: ResolutionString }> = new Map(); private subscribers: Map<string, { callback: SubscribeBarsCallback; resolution: ResolutionString }> = new Map();
constructor(apiUrl: string, updateInterval = 1000) { constructor(apiUrl: string, updateInterval = 1000) {
super(apiUrl);
this.apiUrl = apiUrl; this.apiUrl = apiUrl;
this.updateInterval = updateInterval; this.updateInterval = updateInterval;
} }
@@ -31,7 +34,7 @@ export class CustomDatafeed implements IDatafeedChartApi, IExternalDatafeed {
* 初始化配置 * 初始化配置
*/ */
onReady(callback: OnReadyCallback): void { onReady(callback: OnReadyCallback): void {
console.log("[CustomDatafeed]: onReady called"); console.log("[RWADatafeed]: onReady called");
const config: DatafeedConfiguration = { const config: DatafeedConfiguration = {
// 支持的交易所 // 支持的交易所
@@ -73,7 +76,7 @@ export class CustomDatafeed implements IDatafeedChartApi, IExternalDatafeed {
symbolType: string, symbolType: string,
onResult: SearchSymbolsCallback, onResult: SearchSymbolsCallback,
): void { ): void {
console.log("[CustomDatafeed]: searchSymbols", userInput, exchange, symbolType); console.log("[RWADatafeed]: searchSymbols", userInput, exchange, symbolType);
// TODO: 调用后端 API 搜索品种 // TODO: 调用后端 API 搜索品种
// const results = await client.api.market.search.get({ query: { keyword: userInput, exchange, type: symbolType } }) // const results = await client.api.market.search.get({ query: { keyword: userInput, exchange, type: symbolType } })
@@ -101,7 +104,7 @@ export class CustomDatafeed implements IDatafeedChartApi, IExternalDatafeed {
onResolve: ResolveCallback, onResolve: ResolveCallback,
onError: (reason: string) => void, onError: (reason: string) => void,
): void { ): void {
console.log("[CustomDatafeed]: resolveSymbol", symbolName); console.log("[RWADatafeed]: resolveSymbol", symbolName);
// TODO: 调用后端 API 获取品种信息 // TODO: 调用后端 API 获取品种信息
// const symbolInfo = await client.api.market.symbol[symbolName].get() // const symbolInfo = await client.api.market.symbol[symbolName].get()
@@ -152,7 +155,7 @@ export class CustomDatafeed implements IDatafeedChartApi, IExternalDatafeed {
onError: (reason: string) => void, onError: (reason: string) => void,
): void { ): void {
const { from, to, firstDataRequest } = periodParams; const { from, to, firstDataRequest } = periodParams;
console.log("[CustomDatafeed]: getBars", { console.log("[RWADatafeed]: getBars", {
symbol: symbolInfo.name, symbol: symbolInfo.name,
resolution, resolution,
from: new Date(from * 1000).toISOString(), from: new Date(from * 1000).toISOString(),
@@ -203,7 +206,7 @@ export class CustomDatafeed implements IDatafeedChartApi, IExternalDatafeed {
listenerGuid: string, listenerGuid: string,
onResetCacheNeededCallback: () => void, onResetCacheNeededCallback: () => void,
): void { ): void {
console.log("[CustomDatafeed]: subscribeBars", { console.log("[RWADatafeed]: subscribeBars", {
symbol: symbolInfo.name, symbol: symbolInfo.name,
resolution, resolution,
listenerGuid, listenerGuid,
@@ -231,7 +234,7 @@ export class CustomDatafeed implements IDatafeedChartApi, IExternalDatafeed {
* 取消订阅实时数据 * 取消订阅实时数据
*/ */
unsubscribeBars(listenerGuid: string): void { unsubscribeBars(listenerGuid: string): void {
console.log("[CustomDatafeed]: unsubscribeBars", listenerGuid); console.log("[RWADatafeed]: unsubscribeBars", listenerGuid);
this.subscribers.delete(listenerGuid); this.subscribers.delete(listenerGuid);
// TODO: 关闭 WebSocket 连接 // TODO: 关闭 WebSocket 连接

View File

@@ -1,16 +1,18 @@
import type { ChartingLibraryWidgetOptions } from "#/charting_library"; import type { ChartingLibraryWidgetOptions, IChartingLibraryWidget } from "#/charting_library";
import type { ResolutionString } from "#/datafeed-api"; import type { ResolutionString } from "#/datafeed-api";
import { CustomDatafeed } from "./datafeed"; import { RWADatafeed } from "./datafeed";
const { VITE_TRADINGVIEW_LIBRARY_URL, VITE_TRADINGVIEW_DATA_API_URL } = useEnv(); const { VITE_TRADINGVIEW_LIBRARY_URL, VITE_TRADINGVIEW_DATA_API_URL } = useEnv();
const datafeed = new RWADatafeed(VITE_TRADINGVIEW_DATA_API_URL, 60000);
const defaultOptions = { const defaultOptions = {
container: "tradingview_chart_container", container: "tradingview_chart_container",
locale: "zh", locale: "zh",
library_path: `${VITE_TRADINGVIEW_LIBRARY_URL}/charting_library/`, library_path: `${VITE_TRADINGVIEW_LIBRARY_URL}/charting_library/`,
datafeed: new CustomDatafeed(VITE_TRADINGVIEW_DATA_API_URL, 60000), datafeed,
symbol: "AAPL",
interval: "1D" as ResolutionString, interval: "1D" as ResolutionString,
symbol: "AAPL",
debug: true, debug: true,
autosize: true, autosize: true,
// 禁用移动端不友好的功能 // 禁用移动端不友好的功能
@@ -18,18 +20,46 @@ const defaultOptions = {
"left_toolbar", // 隐藏左侧绘图工具栏 "left_toolbar", // 隐藏左侧绘图工具栏
"timeframes_toolbar", // 隐藏底部时间框架工具栏 "timeframes_toolbar", // 隐藏底部时间框架工具栏
"volume_force_overlay", // 禁用成交量覆盖在价格图表上 "volume_force_overlay", // 禁用成交量覆盖在价格图表上
// 禁用顶部工具栏 // "header_widget", // 禁用顶部工具栏
"header_widget", "header_compare", // 禁用比较功能
"header_compare", "header_symbol_search", // 禁用符号搜索
"header_symbol_search", // "header_fullscreen_button", // 禁用全屏按钮
"header_fullscreen_button", // "header_settings", // 禁用设置按钮
"header_settings", "header_screenshot", // 禁用截图功能
"header_undo_redo", // 禁用撤销/重做按钮
"header_saveload", // 禁用保存/加载按钮
"control_bar", // 禁用底部控制栏
"edit_buttons_in_legend", // 禁用图例中的编辑按钮
"border_around_the_chart", // 移除图表边框
"main_series_scale_menu", // 禁用主序列比例菜单
"display_market_status", // 禁用市场状态显示
// "create_volume_indicator_by_default", // 默认不创建成交量指标
// "go_to_date", // 禁用跳转到日期功能
"pane_context_menu", // 禁用窗格右键菜单
// "legend_context_menu", // 禁用图例右键菜单
// "legend_inplace_edit", // 禁用图例内联编辑
// "legend_widget", // 禁用图例小部件
], ],
// 启用移动端友好的功能 // 启用移动端友好的功能
enabled_features: [ enabled_features: [
"show_zoom_and_move_buttons_on_touch", // 在触摸设备上显示缩放和移动按钮 "show_zoom_and_move_buttons_on_touch", // 在触摸设备上显示缩放和移动按钮
"adaptive_logo", // 在小屏幕设备上隐藏 TradingView logo "adaptive_logo", // 在小屏幕设备上隐藏 TradingView logo
"hide_left_toolbar_by_default", // 默认隐藏左侧工具栏
"items_favoriting", // 启用收藏功能
], ],
// 图表样式配置
overrides: {
"mainSeriesProperties.candleStyle.upColor": "#0ecb81", // 涨(绿色)
"mainSeriesProperties.candleStyle.downColor": "#f6465d", // 跌(红色)
"mainSeriesProperties.candleStyle.borderUpColor": "#0ecb81",
"mainSeriesProperties.candleStyle.borderDownColor": "#f6465d",
"mainSeriesProperties.candleStyle.wickUpColor": "#0ecb81",
"mainSeriesProperties.candleStyle.wickDownColor": "#f6465d",
},
// 时间刻度配置
time_scale: {
min_bar_spacing: 6,
},
} satisfies ChartingLibraryWidgetOptions; } satisfies ChartingLibraryWidgetOptions;
export const TradingViewChart = defineComponent({ export const TradingViewChart = defineComponent({
@@ -37,7 +67,6 @@ export const TradingViewChart = defineComponent({
props: { props: {
symbol: { symbol: {
type: String, type: String,
default: "BTCUSDT",
}, },
options: { options: {
type: Object as PropType<Partial<ChartingLibraryWidgetOptions>>, type: Object as PropType<Partial<ChartingLibraryWidgetOptions>>,
@@ -46,20 +75,25 @@ export const TradingViewChart = defineComponent({
}, },
setup(props) { setup(props) {
const el = ref<HTMLDivElement | null>(null); const el = ref<HTMLDivElement | null>(null);
const { isDark } = useTheme();
const widget = ref<IChartingLibraryWidget | null>(null);
onMounted(() => { onMounted(() => {
// eslint-disable-next-line new-cap // eslint-disable-next-line new-cap
const widget = new TradingView.widget({ widget.value = new TradingView.widget({
...defaultOptions, ...defaultOptions,
...props.options, ...props.options,
theme: isDark.value ? "dark" : "light",
container: el.value!, container: el.value!,
}); });
widget.onChartReady(() => { widget.value.onChartReady(() => {
console.log("[TradingView]: Chart is ready"); console.log("[TradingView]: Chart is ready");
}); });
console.log("widget", widget.value.activeChart);
}); });
return () => <div ref={el} class="w-full h-70" />; return () => <div ref={el} class="w-full h-100" />;
}, },
}); });

View File

@@ -33,12 +33,7 @@ const mode = ref<"buy" | "sell">("buy");
</ion-toolbar> </ion-toolbar>
</ion-header> </ion-header>
<ion-content :fullscreen="true"> <ion-content :fullscreen="true">
<TradingViewChart <div class="grid grid-cols-5 px-4">
class="mb-5" :options="{
symbol: 'BTC/USDT',
}"
/>
<div class="grid grid-cols-5">
<div class="col-span-3 space-y-2"> <div class="col-span-3 space-y-2">
<TradeSwitch v-model:active="mode" /> <TradeSwitch v-model:active="mode" />
<TradeWay /> <TradeWay />
@@ -52,7 +47,8 @@ const mode = ref<"buy" | "sell">("buy");
</div> </div>
<div class="col-span-2" /> <div class="col-span-2" />
</div> </div>
<div class="mt-6"> <TradingViewChart class="my-5" />
<div class="mt-6 px-4">
<OrdersPanel /> <OrdersPanel />
</div> </div>
</ion-content> </ion-content>