# React_ChardingView_socket

中文文档地址 https://b.aitrade.ga/books/tradingview/

# 文件结构

src
  |- service
  |     |- socket.js    // 链接ws,ws监听 推送 取消订阅 更新最新ticker 等的方法
  |- root.component.js  // 引用socket
  |- pages
  |    |-kLine
  |        |-index.js   // socket 订阅历史

# 常见问题

// 设置图标价格精度
const e = chart.priceFormatter();
e.format = (v) => new bigNumber(v).toFixed(1, 1);

# 方法一: npm trader-view

import {TradingView, Datafeed } from "trader-view";

# TradingView 初始化图标

// params ={symbol, resolution, from, to, firstDataRequest}
// list为数组 ep: [ {time,close,high,low,open,volume} ... ]
   initDatafeed() {
    const { symbol } = this.props;

    this.datafeed = new Datafeed({
      history: (params) => {
        return this.getHistData(
          params.symbol,
          params.resolution,
          params.from,
          params.to,
          params.firstDataRequest
        );
      },
      time: () => new Promise((resolve) => resolve(1)),
      config: () =>
        new Promise((resolve) =>
          resolve({
            supports_search: true,
            supports_group_request: false,
            supported_resolutions: [
              ...new Set(periods.map((item) => item.interval)),
            ],
            supports_marks: false,
            supports_timescale_marks: false,
            supports_time: true,
          })
        ),
      symbols: () =>
        new Promise((resolve) => resolve(createSymbolInfo(symbol))),
    });

    this.datafeed.subscribeBars = (
      symbolInfo,
      resolution,
      onRealtimeCallback,
      subscribeUID,
      onResetCacheNeededCallback
    ) => {
      this.onRealtimeCallback = onRealtimeCallback;
      this.onResetCacheNeededCallback = onResetCacheNeededCallback;
    };
    /**
     * 解析商品信息
     */
    this.datafeed.resolveSymbol = (symbol, onResolve, onError) => {
      onResolve(createSymbolInfo(symbol));
    };
    this.datafeed.getBars = (
      symbolInfo,
      resolution,
      from,
      to,
      onHistoryCallback,
      onErrorCallback,
      firstDataRequest
    ) => {
      this.onHistoryCallback = onHistoryCallback;
      this.from = from;
      this.to = to;
      const { symbol, currentPeriod } = this.props;
      const { period, interval, count } = periods[currentPeriod];
      socket.subscribe("subHis", symbol, period, from);
      // this.initWebSocket().then((res) => {
      //   res.onopen = () => {
      //     const { symbol, currentPeriod } = this.props;
      //     const { period, interval, count } = periods[currentPeriod];
      //     if (res) {
      //       res.send(
      //         subscribe.subHis(
      //           symbol,
      //           period,
      //           from,
      //           Math.ceil((to - from) / 60 / count)
      //         )
      //       );
      //     }
      //   };
      // });
    };
  }
this.widget = new TradingView({
  // debug: true,
  // fullscreen: true,
  custom_css_url: host + "charting_library/static/chardingView.css", // 自定义tradingview样式
  symbol: "BTC", // 商品名
  interval: "D", // 周期  1s 1m 1h 1W 1M 1Y ....
  preset: "mobile",
  container_id: "tv_chart_container",
  // datafeed JavaScript对象的实现接口,以反馈数据显示在图表上 必须项
  datafeed: this.datafeed,
  height: "100%",
  width: "100%",
  library_path: host+"charting_library/",
  locale: "zh",
  loading_screen:{
    backgroundColor: "#fff",
    foregroundColor: "#fff",
  },
  // 禁用自带的一些功能
  disabled_features: [
    "left_toolbar",
    "volume_force_overlay",
    "create_volume_indicator_by_default",
    "create_volume_indicator_by_default_once",
    "format_button_in_legend",
    "hide_left_toolbar_by_default",
    "go_to_date",
    "use_localstorage_for_settings",
    "save_chart_properties_to_local_storage",
    "main_series_scale_menu",
    "show_logo_on_all_charts" // 在多图表布局的每个图表上显示微标
    "header_settings",
    "timeframes_toolbar",
    "chart_property_page_background",
    "compare_symbol",
    "go_to_date",
    "header_chart_type",
    "header_compare",
    "header_interval_dialog_button",
    "header_screenshot",
    "header_symbol_search", // 头部搜索
    "header_widget_dom_node", // 隐藏头部组件
    "source_selection_markers", // 禁用系列和指示器的选择标记
    "header_indicators", // 图标指标
    "adaptive_logo", // 移动端隐藏logo
    "header_undo_redo", // 撤销返回
    "show_hide_button_in_legend",
    "show_interval_dialog_on_key_press",
    "snapshot_trading_drawings",
    "symbol_info",
    "border_around_the_chart",
    "remove_library_container_border",
    "header_saveload",
    "header_resolutions",
  ],
  overrides: getOverrides("white"),
  studies_overrides: {
    "volume.volume.color.0": "#eb4d5c",
    "volume.valume.color.1": "#53b987",
    "volume.volume.transparency": 70,
    "volume.options.showStudyArguments": !1,
    "MA Cross.short:plot.color": "#6b3798",
    "MA Cross.long:plot.color": "#708957",
  },
  enabled_features: ["hide_last_na_study_output"],
  charts_storage_url: "http://saveload.tradingview.com",
  charts_storage_api_version:"1.1",
  client_id: "tradingview.com",
  user_id:"public_user_id",
  theme: "Light",
  timezone: "Asia/Shannghai"
});
this.widget.onChartReady(()=>{
  const chart = this.widget.chart();
  chart.setResolution(inte, ()=>{});
  chart.setChartType(chartType);
  // 每当十字线位置改变时,图标库将会调用回调函数
  chart.crossHairMoved(({time})=>{
    if(!time){
      return;
    }
  })
})
export const getOverrides = (theme) => {
  const themes = {
    white: {
      up: "#1dc6ac",
      down: "#e8506c",
      bg: "rgba(255, 255, 255, 0)",
      grid: "#EDF1F7",
      cross: "#CAD1E7",
      border: "#EDF1F7",
      text: "#CAD1E7",
      areatop: "rgba(71, 78, 112, 0.1)",
      areadown: "rgba(71, 78, 112, 0.02)",
      line: "#EDF1F7",
    },
    black: {
      //url: "night.css",
      up: "#589065",
      down: "#ae4e54",
      bg: "#181B2A",
      grid: "#1f2943",
      cross: "#9194A3",
      border: "#4e5b85",
      text: "#61688A",
      areatop: "rgba(122, 152, 247, .1)",
      areadown: "rgba(122, 152, 247, .02)",
      line: "#737375",
    },
    mobile: {
      //url: "mobile.css",
      up: "#03C087",
      down: "#E76D42",
      bg: "#ffffff",
      grid: "#f7f8fa",
      cross: "#23283D",
      border: "#C5CFD5",
      text: "#8C9FAD",
      areatop: "rgba(71, 78, 112, 0.1)",
      areadown: "rgba(71, 78, 112, 0.02)",
      showLegend: !0,
    },
  };
  const t = themes[theme];
  return {
    volumePaneSize: "medium",
    "scalesProperties.lineColor": t.text,
    "scalesProperties.textColor": t.text,
    "paneProperties.background": t.bg,
    "paneProperties.vertGridProperties.color": t.grid,
    "paneProperties.horzGridProperties.color": t.grid,
    "paneProperties.crossHairProperties.color": t.cross,
    "paneProperties.legendProperties.showLegend": !!t.showLegend,
    "paneProperties.legendProperties.showStudyArguments": !0,
    "paneProperties.legendProperties.showStudyTitles": !0,
    "paneProperties.legendProperties.showStudyValues": !0,
    "paneProperties.legendProperties.showSeriesTitle": !0,
    "paneProperties.legendProperties.showSeriesOHLC": !0,
    "mainSeriesProperties.candleStyle.upColor": t.up,
    "mainSeriesProperties.candleStyle.downColor": t.down,
    "mainSeriesProperties.candleStyle.drawWick": !0,
    "mainSeriesProperties.candleStyle.drawBorder": !0,
    "mainSeriesProperties.candleStyle.borderColor": t.border,
    "mainSeriesProperties.candleStyle.borderUpColor": t.up,
    "mainSeriesProperties.candleStyle.borderDownColor": t.down,
    "mainSeriesProperties.candleStyle.wickUpColor": t.up,
    "mainSeriesProperties.candleStyle.wickDownColor": t.down,
    "mainSeriesProperties.candleStyle.barColorsOnPrevClose": !1,
    "mainSeriesProperties.hollowCandleStyle.upColor": t.up,
    "mainSeriesProperties.hollowCandleStyle.downColor": t.down,
    "mainSeriesProperties.hollowCandleStyle.drawWick": !0,
    "mainSeriesProperties.hollowCandleStyle.drawBorder": !0,
    "mainSeriesProperties.hollowCandleStyle.borderColor": t.border,
    "mainSeriesProperties.hollowCandleStyle.borderUpColor": t.up,
    "mainSeriesProperties.hollowCandleStyle.borderDownColor": t.down,
    "mainSeriesProperties.hollowCandleStyle.wickColor": t.line,
    "mainSeriesProperties.haStyle.upColor": t.up,
    "mainSeriesProperties.haStyle.downColor": t.down,
    "mainSeriesProperties.haStyle.drawWick": !0,
    "mainSeriesProperties.haStyle.drawBorder": !0,
    "mainSeriesProperties.haStyle.borderColor": t.border,
    "mainSeriesProperties.haStyle.borderUpColor": t.up,
    "mainSeriesProperties.haStyle.borderDownColor": t.down,
    "mainSeriesProperties.haStyle.wickColor": t.border,
    "mainSeriesProperties.haStyle.barColorsOnPrevClose": !1,
    "mainSeriesProperties.barStyle.upColor": t.up,
    "mainSeriesProperties.barStyle.downColor": t.down,
    "mainSeriesProperties.barStyle.barColorsOnPrevClose": !1,
    "mainSeriesProperties.barStyle.dontDrawOpen": !1,
    "mainSeriesProperties.lineStyle.color": t.border,
    "mainSeriesProperties.lineStyle.linewidth": 1,
    "mainSeriesProperties.lineStyle.priceSource": "close",
    "mainSeriesProperties.areaStyle.color1": t.areatop,
    "mainSeriesProperties.areaStyle.color2": t.areadown,
    "mainSeriesProperties.areaStyle.linecolor": t.border,
    "mainSeriesProperties.areaStyle.linewidth": 1,
    "mainSeriesProperties.areaStyle.priceSource": "close",
  };
};

# 设置图表背景

// chardingView.css
table.chart-markup-table {
  background-color: #fafafd;
  background: url("./images/logo.png") #fafafd no-repeat center center;
  background-size: 251px 51px;
}
export const resolveSymbol = ({
  Sec: t,
  PrzClose: c,
  PrzHigh: h,
  PrzLow: l,
  PrzOpen: o,
  Volume: v,
}) => ({
  time: t * 1000,
  close: toNumber(c),
  high: toNumber(h),
  low: toNumber(l),
  open: toNumber(o),
  volume: toNumber(v),
});

export const createSymbolInfo =(symbol)=>({
  name: symbol,
  full_name: symbol,
  description: symbol,
  ticker: symbol,
  session: "24x7",
  exchange: "",
  listed_exchange: "",
  timezone: "Asia/Shanghai",
  format: "price",
  pricescale: 100,
  minmov: 1,
  minmov2: 2,
  has_intraday: true,
  has_no_valume: false,
  has_daily: true,
  has_weekly_and_monthly: true,
  has_empty_bars: true,
  supported_resolutions: periods.map((item)=> item.interval),
  intraday_multipliers: periods.map((item)=> item.interval),
})
export const getOverrides = (theme) => {
  const themes = {
    white: {
      //url: "day.css",
      up: "#03c087",
      down: "#ef5555",
      bg: "rgba(0, 0, 0, 0)",
      grid: "#EDF1F7",
      cross: "#CAD1E7",
      border: "#EDF1F7",
      text: "#CAD1E7",
      areatop: "rgba(71, 78, 112, 0.1)",
      areadown: "rgba(71, 78, 112, 0.02)",
      line: "#EDF1F7",
    },
    black: {
      //url: "night.css",
      up: "#589065",
      down: "#ae4e54",
      bg: "#181B2A",
      grid: "#1f2943",
      cross: "#9194A3",
      border: "#4e5b85",
      text: "#61688A",
      areatop: "rgba(122, 152, 247, .1)",
      areadown: "rgba(122, 152, 247, .02)",
      line: "#737375",
    },
    mobile: {
      //url: "mobile.css",
      up: "#03C087",
      down: "#E76D42",
      bg: "#ffffff",
      grid: "#f7f8fa",
      cross: "#23283D",
      border: "#C5CFD5",
      text: "#8C9FAD",
      areatop: "rgba(71, 78, 112, 0.1)",
      areadown: "rgba(71, 78, 112, 0.02)",
      showLegend: !0,
    },
  };
  const t = themes[theme];
  return {
    volumePaneSize: "medium",
    "scalesProperties.lineColor": t.text,
    "scalesProperties.textColor": t.text,
    "paneProperties.background": t.bg,
    "paneProperties.vertGridProperties.color": t.grid,
    "paneProperties.horzGridProperties.color": t.grid,
    "paneProperties.crossHairProperties.color": t.cross,
    "paneProperties.legendProperties.showLegend": !!t.showLegend,
    "paneProperties.legendProperties.showStudyArguments": !0,
    "paneProperties.legendProperties.showStudyTitles": !0,
    "paneProperties.legendProperties.showStudyValues": !0,
    "paneProperties.legendProperties.showSeriesTitle": !0,
    "paneProperties.legendProperties.showSeriesOHLC": !0,
    "mainSeriesProperties.candleStyle.upColor": t.up,
    "mainSeriesProperties.candleStyle.downColor": t.down,
    "mainSeriesProperties.candleStyle.drawWick": !0,
    "mainSeriesProperties.candleStyle.drawBorder": !0,
    "mainSeriesProperties.candleStyle.borderColor": t.border,
    "mainSeriesProperties.candleStyle.borderUpColor": t.up,
    "mainSeriesProperties.candleStyle.borderDownColor": t.down,
    "mainSeriesProperties.candleStyle.wickUpColor": t.up,
    "mainSeriesProperties.candleStyle.wickDownColor": t.down,
    "mainSeriesProperties.candleStyle.barColorsOnPrevClose": !1,
    "mainSeriesProperties.hollowCandleStyle.upColor": t.up,
    "mainSeriesProperties.hollowCandleStyle.downColor": t.down,
    "mainSeriesProperties.hollowCandleStyle.drawWick": !0,
    "mainSeriesProperties.hollowCandleStyle.drawBorder": !0,
    "mainSeriesProperties.hollowCandleStyle.borderColor": t.border,
    "mainSeriesProperties.hollowCandleStyle.borderUpColor": t.up,
    "mainSeriesProperties.hollowCandleStyle.borderDownColor": t.down,
    "mainSeriesProperties.hollowCandleStyle.wickColor": t.line,
    "mainSeriesProperties.haStyle.upColor": t.up,
    "mainSeriesProperties.haStyle.downColor": t.down,
    "mainSeriesProperties.haStyle.drawWick": !0,
    "mainSeriesProperties.haStyle.drawBorder": !0,
    "mainSeriesProperties.haStyle.borderColor": t.border,
    "mainSeriesProperties.haStyle.borderUpColor": t.up,
    "mainSeriesProperties.haStyle.borderDownColor": t.down,
    "mainSeriesProperties.haStyle.wickColor": t.border,
    "mainSeriesProperties.haStyle.barColorsOnPrevClose": !1,
    "mainSeriesProperties.barStyle.upColor": t.up,
    "mainSeriesProperties.barStyle.downColor": t.down,
    "mainSeriesProperties.barStyle.barColorsOnPrevClose": !1,
    "mainSeriesProperties.barStyle.dontDrawOpen": !1,
    "mainSeriesProperties.lineStyle.color": t.border,
    "mainSeriesProperties.lineStyle.linewidth": 1,
    "mainSeriesProperties.lineStyle.priceSource": "close",
    "mainSeriesProperties.areaStyle.color1": t.areatop,
    "mainSeriesProperties.areaStyle.color2": t.areadown,
    "mainSeriesProperties.areaStyle.linecolor": t.border,
    "mainSeriesProperties.areaStyle.linewidth": 1,
    "mainSeriesProperties.areaStyle.priceSource": "close",
  };
};

# websocket 链接获取数据

let _cache = {}; // 暂存所有交易对信息
initWebSocket(isFirst = false){
  this.socket = new WebSocket("wss://ss....");
  // 最基础的需要订阅k线数据,最新ticker
  this.socket.onmessage=(e)=>{
    this.onSocketMessage(e.data);
  }
}
onSocketMessage(data){
  try{
    if(!data){
      return;
    }
    if(data === "PONG"){
      return;
    }
    const _data = JSON.parse(data);
    if(_data.subj==="index"){
      if(this.onRealtimeCallback){
        const bars = Object.entries(_cache).sort((a,b)=>a[0]-b[0]);
        // 最后一条数据用最新数据
        const item = bars[bars.length-1];
        if(item){
          const bar = item[1];
          bar.close = _data.data.Prz;
          bar.time = item[0]/1;
          this.onRealtimeCallback(bar);
        }
      }
    } else if(_data.subj === "kline"){
      const bars = resolveSymbol(_data.data);
      _cache[bar.time] = bar;
      _lastBar = {...bar};
      if(this.onRealtimeCallback){
        setTimeout(()=>{
          this.onRealtimeCallback(bar);
        }, 100);
      }
    } else if(_data.data.Count){
      if(this.onHistoryCallback){
        const {
          Sec, PrzOpen, PrzClose, PrzHigh, PrzLow, Volume, Count
        } = _data.data;
        const {symbol, currentPeriod} = this.props;
        const {period, interval} = periods[currentPeriod];
        const list = [];
        for(let i=0;i<Count; i++){
          const bar = {
            time: Sec[i]*1000,
            close: toNumber(PrzClose[i]),
            high: toNumber(PrzHigh[i]),
            low: toNumber(PrzLow[i]),
            open: toNumber(PrzOpen[i]),
            volume: toNumber(Volume[i]),
          }
          list.push(bar);
          _cache[bar.time] = bar;
          if(i===Count-1){
            _lastBar = {...bar};
            console.log("last", bar.time);
          }
        }
        this.onHistoryCallback(list, {
          noData: !list.length,
          nextTime: Sec[Sec.length -1]*1000,
        });
        if(this.socket){
          this.socket.send(subscribe.sub(symbol, period));
        }
      }
    }
  }
}

websock 订阅的参数

let hisPage = "1";
const subscribe = {
  sub: (symbol, period) =>{
    ++hisPage;
    return JSON.stringify({
      req: "Sub",
      rid: `${hisPage}`,
      args: [
        `kline_${period}_GMEX_CI_${symbol}`,
        `index_GMEX_CI_${symbol}`,
        '__slow__',
      ],
      expires: +new Date(),
    })
  },
  unsub: (symbol, period, all=true){
    ++hisPage;
    return JSON.stringify({
      req: "Unsub",
      rid: `${hisPage}`,
      args: all ? [
          `index_GMEX_CI_${symbol}`,
          `kline_${period}_GMEX_CI_${symbol}`,
          `GMEX_CI_${symbol}`,
        ]
        : [`GMEX_CI_${symbol}`],
      expires: +new Date(),
    });
  },
  subHis: (symbol, period, Sec, Count) => {
    ++hisPage;
    return JSON.stringify({
      req: "GetHistKLine",
      rid: `${hisPage}`,
      args: {
        Sym: `GMEX_CI_${symbol}`,
        Typ: period,
        Sec,
        Offset: 0,
        Count,
      },
      expires: +new Date(),
    });
  },
}

# 生命周期销毁的时候


# 更新 tradingview

componentWillUpdate(nextProps) {
  const { currentPeriod, symbol } = nextProps;
  const { currentPeriod: c, symbol: s } = this.props;
  const period = periods[currentPeriod];
  if (!this.widget) {
    return;
  }

  if (c !== currentPeriod) {
    this.widget.chart().setResolution(period.interval, () => { });
    this.widget.chart().setChartType(period.chartType);
    // this.onResetCacheNeededCallback && this.onResetCacheNeededCallback();
    // this.widget.chart().resetData();
    _cache = {};
    this.socket.send(subscribe.unsub(s, periods[c].period));
    // this.initSub(nextProps);
  }
}
// kLine/index.js
import React from "react";
import moment from "moment";
import { connect } from "react-redux";
import { Scoped } from "kremling";
import { TradingView, Datafeed } from "trader-view";
import {
  periods,
  studys,
  resolveSymbol,
  createSymbolInfo,
  toNumber,
  getOverrides,
} from "@/utils/tradingviewHelper";
import socket from "@/service/socket";
import Text from "@/components/text";
import styles from "./index.krem.scss";

// http://localhost:9001
const host = process.env.NODE_ENV === "development" ? "/" : "/micro/contract/";

// 暂存所以交易对信息
let _cache = {};
let _lastBar = null;
let _selectTime = "";
@connect(({ allSymbolsContract: { currentPeriod, symbol } }) => {
  return {
    currentPeriod,
    symbol,
  };
})
export default class Kline extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      show: false,
      minTime: 0, // 不在此范围内,弹层将被隐藏
      maxTime: 0, // 不在此范围内,弹层将被隐藏
      bar: {}, // 当前选中的bar
      prevBar: {}, // 当前选中bar的前一个bar
    };
  }
  /**
   * 初始订阅
   */
  async initSub({ symbol, currentPeriod }) {
    const { period, interval, count } = periods[currentPeriod];
    await socket.subscribe("subHis", symbol, period, this.from);
  }

  /**
   * 监听WebSocket响应
   * @param msg string
   */
  onSocketMessage = (data) => {
    if (data.subj === "index") {
      // 更新最后一条数据
      if (this.onRealtimeCallback) {
        const bars = Object.entries(_cache).sort((a, b) => a[0] - b[0]); //;
        const item = bars[bars.length - 1];
        if (item) {
          const bar = item[1];
          bar.close = data.data.Prz;
          bar.time = item[0] / 1;
          this.onRealtimeCallback(bar);
        }
      }
    } else if (data.subj === "kline") {
      const bar = resolveSymbol(data.data);
      // 存入缓存
      _cache[bar.time] = bar;
      _lastBar = { ...bar };
      // console.log("kline", _lastBar);
      console.log("put", bar.time);
      if (this.onRealtimeCallback) {
        setTimeout(() => {
          this.onRealtimeCallback(bar);
        }, 100);
      }
    } else if (data.data.Count) {
      if (this.onHistoryCallback) {
        const {
          Sec,
          PrzOpen,
          PrzClose,
          PrzHigh,
          PrzLow,
          Volume,
          Count,
        } = data.data;

        const { symbol, currentPeriod } = this.props;
        const { period, interval } = periods[currentPeriod];
        const list = [];

        for (let i = 0; i < Count; i++) {
          const bar = {
            time: Sec[i] * 1000,
            close: toNumber(PrzClose[i]),
            high: toNumber(PrzHigh[i]),
            low: toNumber(PrzLow[i]),
            open: toNumber(PrzOpen[i]),
            volume: toNumber(Volume[i]),
          };
          list.push(bar);
          // 存入缓存
          _cache[bar.time] = bar;
          if (i === Count - 1) {
            _lastBar = { ...bar };
            console.log("last", bar.time);
          }
        }

        this.onHistoryCallback(list, {
          noData: !list.length,
          nextTime: Sec[Sec.length - 1] * 1000,
        });

        socket.subscribe("sub", symbol, period);
      }
    }
  };

  /**
   * 初始化 JS API
   */
  initDatafeed() {
    const { symbol } = this.props;

    this.datafeed = new Datafeed({
      history: (params) => {
        return this.getHistData(
          params.symbol,
          params.resolution,
          params.from,
          params.to,
          params.firstDataRequest
        );
      },
      time: () => new Promise((resolve) => resolve(1)),
      config: () =>
        new Promise((resolve) =>
          resolve({
            supports_search: true,
            supports_group_request: false,
            supported_resolutions: [
              ...new Set(periods.map((item) => item.interval)),
            ],
            supports_marks: false,
            supports_timescale_marks: false,
            supports_time: true,
          })
        ),
      symbols: () =>
        new Promise((resolve) => resolve(createSymbolInfo(symbol))),
    });

    this.datafeed.subscribeBars = (
      symbolInfo,
      resolution,
      onRealtimeCallback,
      subscribeUID,
      onResetCacheNeededCallback
    ) => {
      this.onRealtimeCallback = onRealtimeCallback;
      this.onResetCacheNeededCallback = onResetCacheNeededCallback;
    };
    /**
     * 解析商品信息
     */
    this.datafeed.resolveSymbol = (symbol, onResolve, onError) => {
      onResolve(createSymbolInfo(symbol));
    };
    this.datafeed.getBars = (
      symbolInfo,
      resolution,
      from,
      to,
      onHistoryCallback,
      onErrorCallback,
      firstDataRequest
    ) => {
      this.onHistoryCallback = onHistoryCallback;
      this.from = from;
      this.to = to;
      const { symbol, currentPeriod } = this.props;
      const { period, interval, count } = periods[currentPeriod];
      socket.subscribe("subHis", symbol, period, from);
      // this.initWebSocket().then((res) => {
      //   res.onopen = () => {
      //     const { symbol, currentPeriod } = this.props;
      //     const { period, interval, count } = periods[currentPeriod];
      //     if (res) {
      //       res.send(
      //         subscribe.subHis(
      //           symbol,
      //           period,
      //           from,
      //           Math.ceil((to - from) / 60 / count)
      //         )
      //       );
      //     }
      //   };
      // });
    };
  }
  async getHistData() {
    const list = await new Promise();
    return {
      bars: [],
      meta: { noData: false },
    };
  }
  /**
   * 初始化图表
   */
  initTradingView = () => {
    if (!this.datafeed) {
      return;
    }
    const { currentPeriod, symbol } = this.props;
    const { time: interval, chartType, interval: inte } = periods[
      currentPeriod
    ];
    const localeMap = {
      "zh-CN": "zh",
      "ko-KR": "ko",
      "en-US": "en",
    };
    this.widget = new TradingView({
      // debug: true, // uncomment this line to see Library errors and warnings in the console
      // fullscreen: true,
      custom_css_url: host + "charting_library/static/style.css",
      symbol,
      interval,
      preset: "mobile",
      container_id: "tv_chart_container",
      datafeed: this.datafeed,
      height: "100%",
      width: "100%",
      library_path: host + "charting_library/",
      locale: "zh",
      loading_screen: {
        // backgroundColor: "#fff",
        // foregroundColor: "#fff"
      },
      disabled_features: [
        "left_toolbar",
        "volume_force_overlay",
        "create_volume_indicator_by_default",
        "create_volume_indicator_by_default_once",
        "format_button_in_legend",
        "hide_left_toolbar_by_default",
        "go_to_date",
        "use_localstorage_for_settings",
        "save_chart_properties_to_local_storage",
        "main_series_scale_menu",
        "show_logo_on_all_charts",
        "header_settings",
        "timeframes_toolbar",
        "chart_property_page_background",
        "timeframes_toolbar",
        "compare_symbol",
        "go_to_date",
        "header_chart_type", // k线样式
        "header_compare",
        "header_interval_dialog_button",
        "header_screenshot", // 截图
        "header_symbol_search",
        "header_undo_redo",
        "show_hide_button_in_legend",
        "show_interval_dialog_on_key_press",
        "snapshot_trading_drawings",
        "symbol_info",
        "border_around_the_chart",
        "remove_library_container_border",
        "header_saveload",
        "header_resolutions",
      ],
      overrides: getOverrides("white"),
      studies_overrides: {
        "volume.volume.color.0": "#eb4d5c",
        "volume.volume.color.1": "#53b987",
        "volume.volume.transparency": 70,
        "volume.options.showStudyArguments": !1,
        "MA Cross.short:plot.color": "#6B3798",
        "MA Cross.long:plot.color": "#708957",
      },
      enabled_features: ["hide_last_na_study_output"],
      charts_storage_url: "http://saveload.tradingview.com",
      charts_storage_api_version: "1.1",
      client_id: "tradingview.com",
      user_id: "public_user_id",
      theme: "Light",
      timezone: "Asia/Shanghai",
    });

    this.widget.onChartReady(() => {
      // 辅助线
      const chart = this.widget.chart();
      chart.setResolution(inte, () => {});
      chart.setChartType(chartType);

      // 每当十字线位置改变时,图表库将会调用回调函数。
      chart.crossHairMoved(({ time }) => {
        if (!time) {
          return;
        }
        const bars = Object.entries(_cache).sort((a, b) => a[0] - b[0]); //;
        const index = bars.findIndex((item) => item[1].time === time * 1000);
        const bar = bars[index]?.[1];
        // console.log("111", time, bar, _cache);
        if (bar) {
          const { minTime, maxTime } = this.state;
          let show = true;
          if (minTime && maxTime) {
            show = bar.time <= maxTime && bar.time >= minTime;
          }
          this.setState(
            {
              show,
              bar: bar,
              prevBar: bars[index - 1]?.[1] || {},
              minTime: show ? bars[index - 2]?.[1]?.time || 0 : 0,
              maxTime: show ? bars[index + 2]?.[1]?.time || 0 : 0,
            },
            () => {
              _selectTime = time;
            }
          );
        } else {
          this.setState({
            show: false,
          });
        }
      });

      for (let i = 0, l = studys.length; i < l; i++) {
        chart.createStudy(...studys[i]);
      }
    });
  };
  componentWillUnmount() {
    const { currentPeriod, symbol } = this.props;
    socket.unsubscribe(symbol, currentPeriod);
  }
  componentWillReceiveProps(nextProps) {
    const { currentPeriod, symbol } = nextProps;
    const { currentPeriod: c, symbol: s } = this.props;
    const period = periods[currentPeriod];
    if (!this.widget) {
      return;
    }

    if (c !== currentPeriod) {
      this.widget.chart().setResolution(period.interval, () => {});
      this.widget.chart().setChartType(period.chartType);
      _cache = {};
      socket.unsubscribe(s, periods[c].period);
    }
  }

  async componentDidMount() {
    // onmessage事件
    socket.onMessage(this.onSocketMessage);
    this.initDatafeed();
    // this.initWebSocket();
    this.initTradingView();
  }

  render() {
    const {
      show,
      prevBar,
      bar: { time, close, high, low, open, volume },
    } = this.state;
    const pClose = prevBar.close || 0;
    const num = close - pClose;
    const percent = ((close - pClose) / close) * 100;
    return (
      <Scoped css={styles}>
        <div className="contract-home-kline">
          {show ? (
            <ul className="float-panle">
              <li>
                <Text type="date" format="YYYY-MM-DD HH:mm">
                  {time}
                </Text>
              </li>
              <li>
                <div>开:</div>
                <Text type="number">{open}</Text>
              </li>
              <li>
                <div>高:</div>
                <Text type="number">{high}</Text>
              </li>
              <li>
                <div>低:</div>
                <Text type="number">{low}</Text>
              </li>
              <li>
                <div>收:</div>
                <Text type="number">{close}</Text>
              </li>
              <li>
                <div>涨跌额: </div>
                <span className={num >= 0 ? "green" : "red"}>
                  {num >= 0 ? "+" : ""}
                  <Text type="number">{num}</Text>
                </span>
              </li>
              <li>
                <div>涨跌幅: </div>
                <span className={percent >= 0 ? "green" : "red"}>
                  {percent >= 0 ? "+" : ""}
                  <Text type="number">{percent}</Text>%
                </span>
              </li>
            </ul>
          ) : null}
          <div
            className="kline-percentage"
            ref={(ref) => (this.percentage = ref)}
          >
            111%
          </div>
          <div id="tv_chart_container"></div>
        </div>
      </Scoped>
    );
  }
}
import React from "react";
import moment from "moment";
import { connect } from "react-redux";
import { Scoped } from "kremling";
import { TradingView, Datafeed } from "trader-view";
import {
  periods,
  studys,
  resolveSymbol,
  createSymbolInfo,
  toNumber,
  getOverrides,
} from "@/utils/tradingviewHelper";
import socket from "@/service/socket";
import Text from "@/components/text";
import styles from "./index.krem.scss";

// http://localhost:9001
const host = process.env.NODE_ENV === "development" ? "/" : "/micro/contract/";

// 暂存所以交易对信息
let _cache = {};
let _lastBar = null;
let _selectTime = "";
@connect(({ allSymbolsContract: { currentPeriod, symbol } }) => {
  return {
    currentPeriod,
    symbol,
  };
})
export default class Kline extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      show: false,
      minTime: 0, // 不在此范围内,弹层将被隐藏
      maxTime: 0, // 不在此范围内,弹层将被隐藏
      bar: {}, // 当前选中的bar
      prevBar: {}, // 当前选中bar的前一个bar
    };
  }
  /**
   * 初始订阅
   */
  async initSub({ symbol, currentPeriod }) {
    const { period, interval, count } = periods[currentPeriod];
    await socket.subscribe("subHis", symbol, period, this.from);
  }

  /**
   * 监听WebSocket响应
   * @param msg string
   */
  onSocketMessage = (data) => {
    if (data.subj === "index") {
      // 更新最后一条数据
      if (this.onRealtimeCallback) {
        const bars = Object.entries(_cache).sort((a, b) => a[0] - b[0]); //;
        const item = bars[bars.length - 1];
        if (item) {
          const bar = item[1];
          bar.close = data.data.Prz;
          bar.time = item[0] / 1;
          this.onRealtimeCallback(bar);
        }
      }
    } else if (data.subj === "kline") {
      const bar = resolveSymbol(data.data);
      // 存入缓存
      _cache[bar.time] = bar;
      _lastBar = { ...bar };
      // console.log("kline", _lastBar);
      console.log("put", bar.time);
      if (this.onRealtimeCallback) {
        setTimeout(() => {
          this.onRealtimeCallback(bar);
        }, 100);
      }
    } else if (data.data.Count) {
      if (this.onHistoryCallback) {
        const {
          Sec,
          PrzOpen,
          PrzClose,
          PrzHigh,
          PrzLow,
          Volume,
          Count,
        } = data.data;

        const { symbol, currentPeriod } = this.props;
        const { period, interval } = periods[currentPeriod];
        const list = [];

        for (let i = 0; i < Count; i++) {
          const bar = {
            time: Sec[i] * 1000,
            close: toNumber(PrzClose[i]),
            high: toNumber(PrzHigh[i]),
            low: toNumber(PrzLow[i]),
            open: toNumber(PrzOpen[i]),
            volume: toNumber(Volume[i]),
          };
          list.push(bar);
          // 存入缓存
          _cache[bar.time] = bar;
          if (i === Count - 1) {
            _lastBar = { ...bar };
            console.log("last", bar.time);
          }
        }

        this.onHistoryCallback(list, {
          noData: !list.length,
          nextTime: Sec[Sec.length - 1] * 1000,
        });

        socket.subscribe("sub", symbol, period);
      }
    }
  };

  /**
   * 初始化 JS API
   */
  initDatafeed() {
    const { symbol } = this.props;

    this.datafeed = new Datafeed({
      history: (params) => {
        return this.getHistData(
          params.symbol,
          params.resolution,
          params.from,
          params.to,
          params.firstDataRequest
        );
      },
      time: () => new Promise((resolve) => resolve(1)),
      config: () =>
        new Promise((resolve) =>
          resolve({
            supports_search: true,
            supports_group_request: false,
            supported_resolutions: [
              ...new Set(periods.map((item) => item.interval)),
            ],
            supports_marks: false,
            supports_timescale_marks: false,
            supports_time: true,
          })
        ),
      symbols: () =>
        new Promise((resolve) => resolve(createSymbolInfo(symbol))),
    });

    this.datafeed.subscribeBars = (
      symbolInfo,
      resolution,
      onRealtimeCallback,
      subscribeUID,
      onResetCacheNeededCallback
    ) => {
      this.onRealtimeCallback = onRealtimeCallback;
      this.onResetCacheNeededCallback = onResetCacheNeededCallback;
    };
    /**
     * 解析商品信息
     */
    this.datafeed.resolveSymbol = (symbol, onResolve, onError) => {
      onResolve(createSymbolInfo(symbol));
    };
    this.datafeed.getBars = (
      symbolInfo,
      resolution,
      from,
      to,
      onHistoryCallback,
      onErrorCallback,
      firstDataRequest
    ) => {
      this.onHistoryCallback = onHistoryCallback;
      this.from = from;
      this.to = to;
      const { symbol, currentPeriod } = this.props;
      const { period, interval, count } = periods[currentPeriod];
      socket.subscribe("subHis", symbol, period, from);
      // this.initWebSocket().then((res) => {
      //   res.onopen = () => {
      //     const { symbol, currentPeriod } = this.props;
      //     const { period, interval, count } = periods[currentPeriod];
      //     if (res) {
      //       res.send(
      //         subscribe.subHis(
      //           symbol,
      //           period,
      //           from,
      //           Math.ceil((to - from) / 60 / count)
      //         )
      //       );
      //     }
      //   };
      // });
    };
  }
  async getHistData() {
    const list = await new Promise();
    return {
      bars: [],
      meta: { noData: false },
    };
  }
  /**
   * 初始化图表
   */
  initTradingView = () => {
    if (!this.datafeed) {
      return;
    }
    const { currentPeriod, symbol } = this.props;
    const { time: interval, chartType, interval: inte } = periods[
      currentPeriod
    ];
    const localeMap = {
      "zh-CN": "zh",
      "ko-KR": "ko",
      "en-US": "en",
    };
    this.widget = new TradingView({
      // debug: true, // uncomment this line to see Library errors and warnings in the console
      // fullscreen: true,
      custom_css_url: host + "charting_library/static/style.css",
      symbol,
      interval,
      preset: "mobile",
      container_id: "tv_chart_container",
      datafeed: this.datafeed,
      height: "100%",
      width: "100%",
      library_path: host + "charting_library/",
      locale: "zh",
      loading_screen: {
        // backgroundColor: "#fff",
        // foregroundColor: "#fff"
      },
      disabled_features: [
        "header_fullscreen_button", // 全屏
        "property_pages",  // 禁用所有属性页
        "left_toolbar",
        "volume_force_overlay",  // 防止重叠
        "create_volume_indicator_by_default",
        "create_volume_indicator_by_default_once",
        "format_button_in_legend",
        "hide_left_toolbar_by_default",
        "go_to_date",
        "use_localstorage_for_settings",
        "save_chart_properties_to_local_storage",
        "main_series_scale_menu",
        "show_logo_on_all_charts",
        "header_settings",  // 设置
        "timeframes_toolbar", // 下面的时间
        "chart_property_page_background",
        "compare_symbol",
        "go_to_date",
        "header_chart_type",
        "header_compare", // 图标对比
        "header_interval_dialog_button",
        "header_screenshot",
        "header_symbol_search",
        "header_undo_redo",
        "show_hide_button_in_legend",
        "show_interval_dialog_on_key_press",
        "snapshot_trading_drawings",
        "symbol_info",
        "border_around_the_chart",
        "remove_library_container_border",
        "header_saveload",
        "header_resolutions", // 头部时间s
      ],
      overrides: getOverrides("white"),
      studies_overrides: {
        "volume.volume.color.0": "#eb4d5c",
        "volume.volume.color.1": "#53b987",
        "volume.volume.transparency": 70,
        "volume.options.showStudyArguments": !1,
        "MA Cross.short:plot.color": "#6B3798",
        "MA Cross.long:plot.color": "#708957",
      },
      enabled_features: ["hide_last_na_study_output"],
      charts_storage_url: "http://saveload.tradingview.com",
      charts_storage_api_version: "1.1",
      client_id: "tradingview.com",
      user_id: "public_user_id",
      theme: "Light",
      timezone: "Asia/Shanghai",
    });

    this.widget.onChartReady(() => {
      // 辅助线
      const chart = this.widget.chart();
      chart.setResolution(inte, () => {});
      chart.setChartType(chartType);

      // 每当十字线位置改变时,图表库将会调用回调函数。
      chart.crossHairMoved(({ time }) => {
        if (!time) {
          return;
        }
        const bars = Object.entries(_cache).sort((a, b) => a[0] - b[0]); //;
        const index = bars.findIndex((item) => item[1].time === time * 1000);
        const bar = bars[index]?.[1];
        // console.log("111", time, bar, _cache);
        if (bar) {
          const { minTime, maxTime } = this.state;
          let show = true;
          if (minTime && maxTime) {
            show = bar.time <= maxTime && bar.time >= minTime;
          }
          this.setState(
            {
              show,
              bar: bar,
              prevBar: bars[index - 1]?.[1] || {},
              minTime: show ? bars[index - 2]?.[1]?.time || 0 : 0,
              maxTime: show ? bars[index + 2]?.[1]?.time || 0 : 0,
            },
            () => {
              _selectTime = time;
            }
          );
        } else {
          this.setState({
            show: false,
          });
        }
      });

      for (let i = 0, l = studys.length; i < l; i++) {
        chart.createStudy(...studys[i]);
      }
    });
  };
  componentWillUnmount() {
    const { currentPeriod, symbol } = this.props;
    socket.unsubscribe(symbol, currentPeriod);
  }
  componentWillReceiveProps(nextProps) {
    const { currentPeriod, symbol } = nextProps;
    const { currentPeriod: c, symbol: s } = this.props;
    const period = periods[currentPeriod];
    if (!this.widget) {
      return;
    }

    if (c !== currentPeriod) {
      this.widget.chart().setResolution(period.interval, () => {});
      this.widget.chart().setChartType(period.chartType);
      _cache = {};
      socket.unsubscribe(s, periods[c].period);
    }
  }

  async componentDidMount() {
    // onmessage事件
    socket.onMessage(this.onSocketMessage);
    this.initDatafeed();
    // this.initWebSocket();
    this.initTradingView();
  }

  render() {
    const {
      show,
      prevBar,
      bar: { time, close, high, low, open, volume },
    } = this.state;
    const pClose = prevBar.close || 0;
    const num = close - pClose;
    const percent = ((close - pClose) / close) * 100;
    return (
      <Scoped css={styles}>
          <div
            className="kline-percentage"
            ref={(ref) => (this.percentage = ref)}
          >
            111%
          </div>
          <div id="tv_chart_container"></div>
        </div>
      </Scoped>
    );
  }
}
// service/socket.js
import { tickerSymbol, periods } from "@/utils/tradingviewHelper";
let ws = "wss://XXX/market";
let index = 1;
let socket = null;
let onMessageFn = null;
let dispatch = null;
let state = null;
let timer = null;
let subQueue = []; // 订阅队列

export const subscribeTypeMap = {
  sub: (symbol, period) =>
    JSON.stringify({
      req: "Sub",
      rid: `${++index}`,
      args: [`kline_${period}_GMEX_CI_${symbol}`],
      expires: +new Date(),
    }),
  unSub: (symbol, period, all = true) =>
    JSON.stringify({
      req: "UnSub",
      rid: `${++index}`,
      args: all
        ? [`kline_${period}_GMEX_CI_${symbol}`, `GMEX_CI_${symbol}`]
        : [`GMEX_CI_${symbol}`],
      expires: +new Date(),
    }),
  subHis: (symbol, period, Sec) =>
    JSON.stringify({
      req: "GetHistKLine",
      rid: `${++index}`,
      args: {
        Sym: `GMEX_CI_${symbol}`,
        Typ: period,
        Sec: Sec,
        Offset: 0,
        Count: 10000000,
      },
      expires: +new Date(),
    }),
};

const throwErr = () => {
  throw "先执行service/socket.js的run方法";
};
/**
 * 提供给外界使用
 * @param {*} fn
 */
export const onMessage = (fn) => {
  if (socket) {
    onMessageFn = fn;
  } else {
    throwErr();
  }
};

/**
 * 启动,初始化
 */
export const run = (d, s) =>
  new Promise(function (resolve, reject) {
    if (socket) {
      const { readyState, OPEN, CLOSED, CLOSING } = socket;
      if (readyState === OPEN) {
        resolve(socket);
      } else if (readyState === CLOSING || readyState === CLOSED) {
        socket = new WebSocket(ws);
      }
    } else {
      socket = new WebSocket(ws);
    }
    // 给重连时使用
    dispatch = d;
    state = s;
    // 进入正常逻辑
    const {
      allSymbolsContract: { currentPeriod, symbol },
    } = state;
    const { period } = periods[currentPeriod];

    socket.onmessage = (e) => {
      try {
        const data = JSON.parse(e.data);
        // 对外提供onmessage方法
        if (onMessageFn) {
          onMessageFn(data);
        }
        // 存储数据
        if (data.subj === "index") {
          dispatch({
            type: "global/updateState",
            state: {
              kline: tickerSymbol(data.data),
            },
          });
        }
      } catch (err) {
        console.error(err);
      }
    };

    /**
     * 链接出错
     */
    socket.onerror = (e) => {
      reject(e);
    };
    /**
     * 订阅k线
     */
    socket.onopen = () => {
      // 链接打开成功
      resolve(socket);
      /**
       * 大部分页面都会使用,所以全局开启订阅
       */
      socket.send(
        JSON.stringify({
          req: "Sub",
          rid: `${++index}`,
          expires: Date.now() + 500 + 30000,
          args: ["index_GMEX_CI_BTC", "__slow__"],
        })
      );
    };
  });

/**
 * 连接成功后,发送订阅队列
 * 调用之前必需保证连接成功
 */
const send = (resolve) => {
  for (let subString of subQueue) {
    socket.send(subString);
  }
  resolve(socket);
  // 订阅成功后清除对列
  subQueue = [];
};
export const subscribe = (sub, ...args) =>
  new Promise(function (resolve, reject) {
    // 订阅的字符串
    subQueue.push(subscribeTypeMap[sub] ? subscribeTypeMap[sub](...args) : sub);
    if (socket) {
      const { readyState, OPEN, CLOSED, CLOSING } = socket;

      // 连接成功
      if (readyState === OPEN) {
        send(resolve);
      } else if (readyState === CLOSED || readyState === CLOSING) {
        // 连接关闭
        if (dispatch && state) {
          run(dispatch, state)
            .then(() => {
              send(resolve);
            })
            .catch(reject);
        } else {
          throwErr();
        }
      } else {
        // 链接中
        timer = setInterval(() => {
          /**
           * 开启定时器检测状态
           * 连接成功后关闭定时器并订阅所有订阅
           */
          const { OPEN, readyState } = socket;
          if (OPEN === readyState) {
            clearInterval(timer);
            timer = null;
            send(resolve);
          }
        }, 100);
      }
    } else {
      throwErr();
    }
  });

/**
 * 取消订阅,在使用之前保证连接成功
 * @param {*} sub
 * @param  {...any} args
 */
export const unsubscribe = (...args) => {
  if (socket) {
    const subString = subscribeTypeMap.unSub(...args);
    socket.send(subString);
  } else {
    throwErr();
  }
};
export default { run, onMessage, subscribe, unsubscribe };
// root.component.js
async componentDidMount() {
  const { store } = this.props;
  await socket.run(store.dispatch, store.getState());
}