• 简单的股票行情演示(一) - 实时标的数据


    原文链接:简单的股票行情演示(一) - 实时标的数据

    一、概述

    很长一段时间都有一个想法,使用QCP去做一个行情展示小事例,一直没有着手开发的原因主要是行情数据源的问题,毕竟稳定的数据才是核心,加上今年5月份有了小宝宝也一直比较忙。

    最近得空研究了下用C++实现股票行情展示相关内容,主要策略是通过拉取网上一些免费的开源接口数据,然后存储到本地,在通过代码读取需要的日期数据进行展示。互联网拉取行情数据方法网上随手百度后会发现有一大堆,调取个别接口进行获取数据也是很方便的,比如通过新浪开源获取A股股票接口获取实时行情数据就很简单,浏览器url输入框中输入http://hq.sinajs.cn/list=sz002208,sh601318这段测试连接,按下回车,就会拿到list指定的两支股票数据,效果如下图所示。

    需要特别注意:该接口拉取频繁后,会被后台403,所以本地需要做一些策略,尽可能减少无效拉取


    尝到了简单的甜头之后,接下来就是疯狂百度、google,尽可能全面的整理开源的行情数据源,网上虽然文章很多,但是重复的内容特别多,讲的比较好的文章有新浪股票 api股票数据 API 接口合集实时行情API,通过看这几篇文章能大概了解到一些皮毛,简单使用不成问题。总的来说提供了一个可操作的入口,数据源的问题算是暂时得到一部分解决,至于其他更完善的数据后续文章会有介绍,是由开源软件提供,而且文档比较详细,之后更多的数据将会使用开源程序进行获取

    实时行情数据有了之后,接下来就是C++侧代码实现,主要分为异步数据拉取、数据写入本地文件、数据层读取,回调给UI展示,本篇文章接下来的主要内容将会讲解怎么拉取数据、回调给UI等流程。

    二、效果展示

    如下效果图所示,是一个简单的多窗口程序,支持同时拉取多支股票实时行情数据并回调给UI。拿其中一个行情数据展示窗口为例来说明,数据源是来自新浪行情API接口,测试程序UI展示总共分上中下三段,上半部分主要是股票盘口数据,展示开收盘价格、实时成交量等,中段是股票买卖五档数据,最底下白色框中是数据源内容,也就是从互联网接口拉取后的数据。

    正常情况下测试程序只会跑一个窗口,图示中多个窗口主要是为了观察方便。

    三、实现代码

    1、行情数据中心

    要想实现数据复用,并减少Server压力,数据中心是必不可少的模块,举一个简单例子,当UI界面上展示的两支股票相同时,那么数据中心只会维护一支股票数据,并实时更新然后同步给两份UI界面。

    如下代码所示,为行情中心接口类,其中展示了如何去订阅股票详情数据和取消订阅,IQuoteCall这是订阅者唯一标识,每一个想要获取数据的对象都应该是一个IQuoteCall、或者持有一个IQuoteCall

    1. struct QUOTECENTER_EXPORT IQuoteCenter
    2. {
    3. public:
    4.  virtual ~IQuoteCenter(){}
    5. public:
    6.  //订阅detail
    7.  virtual void SubscribeDetail(IQuoteCall * observer, const SecurityInfo & security) = 0;
    8.  virtual void UnSubscribeDetail(IQuoteCall * observer) = 0;
    9.     .
    10.     .
    11.     .
    12.  //取消所有数据订阅
    13.  virtual void UnSubscribe(IQuoteCall * observer) = 0;
    14.  .
    15.  .
    16.  .
    17. };

    IQuoteCall接口类中有一个UpdateDetail接口,通过重写该接口即可获取订阅的标的行情数据,切换标的时从新订阅即可,之前订阅的标的会被自动取消。

    股票实时行情数据需要启动一个轮训任务,每隔3秒去请求一次当前订阅的所有标的数据,有了时间服务后,我们只需要抛一个任务对象和时间间隔,之后的定时触发操作则会自动被执行。

    1. QuoteCenter::QuoteCenter()
    2. {
    3.  qRegisterMetaType<DetailCNItem>("DetailCNItem");
    4.  qRegisterMetaType<QList<DetailCNItem>>("QList");
    5.  m_strTaskID = Services::TimerServiceInstance()->AddTask(&DoRequests, 3000);
    6.     .
    7.     .
    8.     .
    9. }

    DoRequests函数比较重要,可谓之承上启下,关键桥梁作用,因此这里单独做下说明。

    首先DoRequests是一个C函数,被行情模块注册到时间管理器中,该函数会在指定时间间隔后触发一次,每次任务触发我们都需要在主线程中构造一个任务请求工作者,并把任务执行完成后的触发信号与行情对象的接收槽函数绑定,之后把任务对象抛给网络请求服务即可。任务对象后续还会有更加详细的说明,具体参看对detail请求对象说明。

    1. void DoRequests(long long mseconds)
    2. {
    3.  .
    4.  .
    5.  .
    6.  DetailWorker * detail = new DetailWorker(securitys);
    7.  QObject::connect(detail, &DetailWorker::Response
    8.   , static_cast<QuoteCenter *>(Quote::QuoteCenterInstance()), &QuoteCenter::OnDetailResponse);
    9.  RLNet::NetworkInstance()->AddTask(detail);
    10. }

    2、数据拉取模块

    本地数据的唯一来源就是从网络拉取,为了程序运行流程起见,必须要运行在工作线程中,防止阻塞UI,我们开发此演示程序是基于Qt开发框架下,所以线程创建、线程交互将会变的很简单,具体细节接下来一步一步讲解。

    线程池

    既然用到Qt,那么线程池肯定也要用Qt的,这样我们开发起来会省很多力气,如下代码所示,简单的几行代码我们就搞出来一个线程池,我们只管往池子里丢任务,当池子中有空闲线程时就会帮我们处理任务,是不是非常nice。

    1. void RLNetwork::AddTask(CommonWorker * task)
    2. {
    3.  // 添加任务
    4.  QThreadPool::globalInstance()->start(task);
    5. }
    6. RLNetwork::RLNetwork()
    7. {
    8.  curl_global_init(CURL_GLOBAL_DEFAULT);
    9.  // 线程池初始化,设置最大线程池数
    10.  QThreadPool::globalInstance()->setMaxThreadCount(8);
    11. }
    12. RLNetwork::~RLNetwork()
    13. {
    14.  curl_global_cleanup();
    15. }

    任务对象

    有了线程池后,我们只管创建需要的task,然后丢到池子中,任务的触发时机将交给Qt线程池进行管理,我们只需要关心任务中要干什么、任务结束后怎么通知给外部即可。

    任务基类

    为了减少大量重复代码,这里我们定义一个任务基类,基类中完成每个请求任务都需要操作的内容,然后把请求体和写入内容封装成接口,供子类重写。

    抽象内容包括:

    1. libcurl请求初始化、参数配置和清理
    2. 回调函数取到返回数据后整理标准字符串转发给子类Write函数
    1. class RLNETWORK_EXPORT CommonWorker : public QObject, public QRunnable
    2. {
    3. public:
    4.  CommonWorker();
    5.  ~CommonWorker();
    6. public:
    7.  virtual void run() override;
    8.  virtual void Write(char * data, std::size_t len) 0;
    9.  virtual void DoRequest(CURL * curl) 0;
    10.  static size_t  CurlWriteCb(char *ptr, size_t size, size_t nmemb, void *userdata);
    11. private:
    12. };

    Detail请求

    说了这么多,终于到了最关键的detail请求环节,如下StockListWorker代码所示,当Detail请求完成后,通过Response信号通知外部任务已完成,标的detail存放在了filePath指定的文件中。

    对于StockListWorker对象有以下几点需要注意:

    1. 构造于主线程中,并且请求完成信号与主线程中槽函数所绑定
    2. DoRequest、Write和信号函数均运行于工作线程中
    3. 任务基类中我们设置了setAutoDelete为true,因此所有的请求对象在执行完任务后都会析构
    4. 析构函数运行于工作线程中,与run函数所在线程一致
    1. class RLNETWORK_EXPORT StockListWorker : public CommonWorker
    2. {
    3.  Q_OBJECT
    4. public:
    5.  StockListWorker(const QString & filePath);
    6.  ~StockListWorker();
    7. signals:
    8.  void Response(const QString & filePath);
    9. public:
    10.  virtual void Write(char * data, std::size_t len) override;
    11.  virtual void DoRequest(CURL * curl) override;
    12. private:
    13.  QString m_filePath;
    14.  QFile m_file;
    15. };

    DoRequest函数是基类提供给我们重写发送请求使用,如下代码所示,展示了请求detail数据的过程,网络请求我们统一使用libcurl进行完成,不使用Qt网络库主要是觉着不好用。

    1. void StockListWorker::DoRequest(CURL * curl)
    2. {
    3.  curl_easy_setopt(curl, CURLOPT_URL, StockListUrl);//准备发送request的url
    4.  CURLcode res = curl_easy_perform(curl);
    5.  if (res == CURLE_OK)
    6.  {
    7.   curl_off_t val = -1;
    8.   curl_easy_getinfo(curl, CURLINFO_NAMELOOKUP_TIME_T, &val);
    9.   emit Response(m_filePath);
    10.  }
    11.  else
    12.  {
    13.   std::cout << "curl_easy_perform() failed: " << curl_easy_strerror(res);
    14.  }
    15. }

    3、基础服务模块

    市场服务

    市场服务主要提供市场相关接口,如下代码所示,IsTradingStatus接口获取当前标的所属市场是否属于交易状态,根据市场状态我们可以过滤一些无效操作,比如A股不开盘时,不需要请求detail等。

    1. struct BASICSERVICES_EXPORT IMarketService
    2. {
    3.  virtual ~IMarketService(){}
    4.  virtual bool IsTradingStatus(const SecurityInfo & security, long long msecondsconst = 0;
    5.  virtual bool IsTradingStatus(const QList & securitys, long long msecondsconst = 0;
    6.  ·
    7.  ·
    8.  ·
    9. };

    时间服务

    时间管理器对于数据中心是相当重要的,因为有了时间维度后,我们才能去定制一批时间相关的任务,比如轮训任务、获取当前时间等。

    本文中的股票实时detail数据就需要添加了一个轮训任务,因为没有长连接的加持,很多数据都需要我们自己去跟服务器要,虽然这样会增大服务器的压力,但是目前除过长连接外没有其他更好的方式去完成这件事。

    1. struct BASICSERVICES_EXPORT ITimerService
    2. {
    3.  virtual ~ITimerService(){}
    4.  virtual QString AddTask(const std::function<void(long long)> & fun, long internal3000) = 0;
    5.  virtual bool HasTask(const QString &) = 0;
    6.  virtual void RemoveTask(const QString &) = 0;
    7.  virtual void ImmediatelyTask(const QString &) = 0;
    8.  virtual long long GetCurrentStamp() const = 0;
    9.  ·
    10.  ·
    11.  ·
    12. };

    4、UI展示

    订阅股票detail

    如下代码所示,通过行情中心我们可以很简单的去订阅标的数据,之后通过重写UpdateDetail接口拿数据就行,其他的我们统一不用操心。

    1. void HqSimple::on_pushButton_pull_clicked()
    2. {
    3.  const QString & name = ui.comboBox->currentText();
    4.  const QString & id = ui.comboBox->currentData().toString();
    5.  const QStringList & items = id.split('_');
    6.  SecurityInfo info;
    7.  info.market = items.at(0);
    8.  info.symbol = items.at(1);
    9.  info.secType = "STK";
    10.  Quote::QuoteCenterInstance()->SubscribeDetail(this, info);
    11. }

    每一个需要订阅行情数据的对象目前都是继承自IQuoteCall,或者持有一个IQuoteCall,本篇文章包括后续系列文章都会采用第一种方案来实现数据订阅,关于继承和包含的优缺点及使用场景问题大家可以自行斟酌,本篇文章所讲述案列数据类型较少,使用继承足以完成目标。

    1. struct QUOTECENTER_EXPORT IQuoteCall
    2. {
    3. virtual ~IQuoteCall(){}
    4. virtual void UpdateDetail(const DetailCNItem &) = 0;
    5. .
    6. .
    7. .
    8. };

    A股Detail数据定义

    1. /*
    2. 0: 通用股份 // 名字;
    3. 1 : 5.050 // 今日开盘价
    4. 2 : 5.060 // 昨日收盘价
    5. 3 : 5.090 // 当前价格
    6. 4 : 5.110 // 今日最高价
    7. 5 : 5.030 // 今日最低价
    8. 6 : 5.090 // 竞买价,即 “买一” 报价;
    9. 7 : 5.100 // 竞卖价,即 “卖一” 报价;
    10. 8 : 3963000 // 成交的股票数,转手乘 100
    11. 9 : 20106078.000// 成交金额 (元),转万除 10000
    12. 10 : 52800 //“买一” 申请 52800
    13. 11 : 5.090 //“买一” 报价;
    14. 12 : 90600 //“买二” 申请 90600
    15. 13 : 5.080 //“买二” 报价;
    16. 14 : 98500 //..
    17. 15 : 5.070 //..
    18. 16 : 105200 //..
    19. 17 : 5.060 //..
    20. 18 : 127900 //..
    21. 19 : 5.050 //..
    22. 20 : 104400 //“卖一” 申报 104400
    23. 21 : 5.100 //“卖一” 报价;
    24. 22 : 99700 //“卖二” 申报 99700
    25. 23 : 5.110 //“卖二” 报价;
    26. 24 : 111800 //..
    27. 25 : 5.120 //..
    28. 26 : 87500 //..
    29. 27 : 5.130 //..
    30. 28 : 73300 //..
    31. 29 : 5.140 //..
    32. 30 : 2022 - 02 - 14 // 日期
    33. 31 : 11 : 18 : 56 // 时间
    34. */
    35. struct STOCKDATA_EXPORT DetailCNItem : public SecurityInfo
    36. {
    37.  //0-9
    38.  QString name;  //名字
    39.  double open;  //今日开盘价
    40.  double preClose; // 昨日收盘价
    41.  double lastprice; // 当前价格
    42.  double high;  // 今日最高价
    43.  double low;   // 今日最低价
    44.  double bid;   // 竞买价,即 “买一” 报价;
    45.  double ask;   // 竞卖价,即 “卖一” 报价;
    46.  double volumn;  // 成交的股票数,转手乘 100
    47.  double amount;  // 成交金额 (元),转万除 10000
    48.  
    49.  struct AskBid
    50.  {
    51.   double price;///卖价
    52.   double volumn;///卖量
    53.  };
    54.  QVector<AskBid> asks;//卖五档 //10-19
    55.  QVector<AskBid> bids;//买五档 //20-29
    56.  
    57.  //30-31
    58.  QString date// 日期
    59.  QString time// 时间
    60.  QString source//原始数据
    61.  DetailCNItem();
    62.  DetailCNItem(const QString & str);
    63.  DetailCNItem(DetailCNItem && other);
    64.  void Clear();
    65. };

    刷新数据

    UI数据刷新这里就比较简单了,文章最开始已经描述过UI数据分为上中下三部分,上部和下部就是简单文案设置,然后通过qss加了一些涨跌色配置,这里就简单展示下部分代码。

    1. void HqSimple::UpdateDetail(const DetailCNItem & data)
    2. {
    3.  ui.label_open->setText(QString::number(data.open'f'2));
    4.  .
    5.  .
    6.  .
    7.  ui.label_amount->setText(QString::number(data.amount, 'f'2));
    8.  m_pListmodel->SetAskBid(data);
    9.  ui.textEdit->setText(QStringLiteral("原始数据:") + data.source);
    10. }

    盘口数据分为6列:买档、买价格、买数量、卖数量、卖价格和卖档。实现起来也比较简单,标准MVC即可搞定,代码中表现为QAbstractListModel+QListView+QStyledItemDelegate,其中M和V都比较简单,简单的进行绑定之后就可以,这里主要说下绘制界面用的QStyledItemDelegate,其中最为关键的就是paint函数,相信用过Qt一年半载的同学都比较熟悉,代码如下所示,绘制代码比较简单就不做说明了,不明白的同学进行留言或者私聊即可。

    1. void AskBidDelegate::paint(QPainter * painter, const QStyleOptionViewItem & option, const QModelIndex & index) const
    2. {
    3.  const DetailCNItem & detail = index.model()->data(index).value<DetailCNItem>();
    4.  //left
    5.  int y = 15;
    6.  int lwidth = option.rect.width() / 2;
    7.  const QPoint & gPos = QCursor::pos();
    8.  const QPoint & lPos = option.widget->mapFromGlobal(gPos);
    9.  int t = 0;
    10.  if (lPos.x() >= 0 && lPos.x() <= lwidth)
    11.  {
    12.   t = 1;
    13.  }
    14.  else if (lPos.x() >= lwidth && lPos.x() <= lwidth * 2)
    15.  {
    16.   t = 2;
    17.  }
    18.  {
    19.   painter->fillRect(option.rect.adjusted(00, lwidth, 0)
    20.    , t == 1 && option.state.testFlag(QStyle::State_MouseOver) ? QColor(2810983) : QColor(396762));
    21.   const DetailCNItem::AskBid & bid = detail.bids.at(index.row());
    22.   const QString & bidName = QStringLiteral("买%1").arg(index.row() + 1);
    23.   painter->setPen(QColor(Qt::white));
    24.   painter->drawText(10, option.rect.top() + y, bidName);
    25.   painter->setPen(QColor(Qt::red));
    26.   painter->drawText(60, option.rect.top() + y, PriceText(bid.price));
    27.   const QString & volumnName = PriceText(bid.volumn);
    28.   int volumW = painter->fontMetrics().width(volumnName);
    29.   painter->setPen(QColor(Qt::white));
    30.   painter->drawText(lwidth - volumW, option.rect.top() + y, volumnName);
    31.  }
    32.  //right
    33.  {
    34.   painter->fillRect(option.rect.adjusted(option.rect.width() / 2000)
    35.    , t == 2 && option.state.testFlag(QStyle::State_MouseOver) ? QColor(2810983) : QColor(684858));
    36.   const DetailCNItem::AskBid & ask = detail.asks.at(index.row());
    37.   const QString & volumnName = PriceText(ask.volumn);
    38.   painter->setPen(QColor(Qt::white));
    39.   painter->drawText(lwidth + 10, option.rect.top() + y, volumnName);
    40.   painter->setPen(QColor(Qt::green));
    41.   painter->drawText(option.rect.width() - 98, option.rect.top() + y, PriceText(ask.price));
    42.   const QString & askName = QStringLiteral("卖%1").arg(index.row() + 1);
    43.   painter->setPen(QColor(Qt::white));
    44.   painter->drawText(option.rect.width() - 28, option.rect.top() + y, askName);
    45.  }
    46. }

    讲到这里,股票行情展示程序也差不都完成了,从数据订阅、数据求情、数据缓存、数据回调和数据刷新大致都说了一遍,最后贴上项目工程截图,大家可以参考。

    四、相关文章

    1. Qt 之股票组件 - 自选股 -- 列表可以拖拽、右键常用菜单
    2. Qt 之股票组件 - 股票检索 -- 支持搜索结果预览、鼠标、键盘操作
    3. QCustomplot使用分享(一) 能做什么事
    4. QCustomplot使用分享(二) 源码解读
    5. QCustomplot使用分享(三) 图
    6. QCustomplot使用分享(四) QCPAbstractItem
    7. QCustomplot使用分享(五) 布局
    8. QCustomplot使用分享(六) 坐标轴和网格线
    9. QCustomplot使用分享(七) 层(完结)

    值得一看的优秀文章:

    1. 财联社-产品展示
    2. 广联达-产品展示
    3. Qt定制控件列表
    4. 牛逼哄哄的Qt库

    如果您觉得文章不错,不妨给个打赏,写作不易,感谢各位的支持。您的支持是我最大的动力,谢谢!!!
     


    很重要--转载声明

    1. 本站文章无特别说明,皆为原创,版权所有,转载时请用链接的方式,给出原文出处。同时写上原作者:朝十晚八 or Twowords

    2. 如要转载,请原文转载,如在转载时修改本文,请事先告知,谢绝在转载时通过修改本文达到有利于转载者的目的。


  • 相关阅读:
    ArrayList,LinkedList和Vector的区别
    数据库三范式
    基于本体和 Web3.0 洞悉网络威胁情报“盲盒”
    Easyx基本使用(三)
    Java进阶——Java深入学习的笔记汇总 & JVM底层、多线程、类加载 ...
    爆火的治愈系插画工具又来了,额度居然有18w,根本花不完?
    以客户为中心 | 云畅科技优质服务,再获政企客户肯定!
    第一个汇编程序
    QUdpSocket Class
    干掉 “重复代码”,这三种方式绝了!
  • 原文地址:https://blog.csdn.net/qq_30392343/article/details/127999993