一、概述
很長一段時間都有一個想法,使用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
。
struct QUOTECENTER_EXPORT IQuoteCenter
{
public:
virtual ~IQuoteCenter(){}
public:
//訂閱detail
virtual void SubscribeDetail(IQuoteCall * observer, const SecurityInfo & security) = 0;
virtual void UnSubscribeDetail(IQuoteCall * observer) = 0;
.
.
.
//取消所有數據訂閱
virtual void UnSubscribe(IQuoteCall * observer) = 0;
.
.
.
};
IQuoteCall
接口類中有一個UpdateDetail
接口,通過重寫該接口即可獲取訂閱的標的行情數據,切換標的時從新訂閱即可,之前訂閱的標的會被自動取消。
股票實時行情數據需要啓動一個輪訓任務,每隔3秒去請求一次當前訂閱的所有標的數據,有了時間服務後,我們只需要拋一個任務對象和時間間隔,之後的定時觸發操作則會自動被執行。
QuoteCenter::QuoteCenter()
{
qRegisterMetaType<DetailCNItem>("DetailCNItem");
qRegisterMetaType<QList<DetailCNItem>>("QList<DetailCNItem>");
m_strTaskID = Services::TimerServiceInstance()->AddTask(&DoRequests, 3000);
.
.
.
}
DoRequests
函數比較重要,可謂之承上啓下,關鍵橋樑作用,因此這裏單獨做下說明。
首先DoRequests
是一個C函數,被行情模塊註冊到時間管理器中,該函數會在指定時間間隔後觸發一次,每次任務觸發我們都需要在主線程中構造一個任務請求工作者,並把任務執行完成後的觸發信號與行情對象的接收槽函數綁定,之後把任務對象拋給網絡請求服務即可。任務對象後續還會有更加詳細的說明,具體參看對detail請求對象說明。
void DoRequests(long long mseconds)
{
.
.
.
DetailWorker * detail = new DetailWorker(securitys);
QObject::connect(detail, &DetailWorker::Response
, static_cast<QuoteCenter *>(Quote::QuoteCenterInstance()), &QuoteCenter::OnDetailResponse);
RLNet::NetworkInstance()->AddTask(detail);
}
2、數據拉取模塊
本地數據的唯一來源就是從網絡拉取,爲了程序運行流程起見,必須要運行在工作線程中,防止阻塞UI,我們開發此演示程序是基於Qt開發框架下,所以線程創建、線程交互將會變的很簡單,具體細節接下來一步一步講解。
線程池
既然用到Qt,那麼線程池肯定也要用Qt的,這樣我們開發起來會省很多力氣,如下代碼所示,簡單的幾行代碼我們就搞出來一個線程池,我們只管往池子裏丟任務,當池子中有空閒線程時就會幫我們處理任務,是不是非常nice。
void RLNetwork::AddTask(CommonWorker * task)
{
// 添加任務
QThreadPool::globalInstance()->start(task);
}
RLNetwork::RLNetwork()
{
curl_global_init(CURL_GLOBAL_DEFAULT);
// 線程池初始化,設置最大線程池數
QThreadPool::globalInstance()->setMaxThreadCount(8);
}
RLNetwork::~RLNetwork()
{
curl_global_cleanup();
}
任務對象
有了線程池後,我們只管創建需要的task,然後丟到池子中,任務的觸發時機將交給Qt線程池進行管理,我們只需要關心任務中要幹什麼、任務結束後怎麼通知給外部即可。
任務基類
爲了減少大量重複代碼,這裏我們定義一個任務基類,基類中完成每個請求任務都需要操作的內容,然後把請求體和寫入內容封裝成接口,供子類重寫。
抽象內容包括:
- libcurl請求初始化、參數配置和清理
- 回調函數取到返回數據後整理標準字符串轉發給子類Write函數
class RLNETWORK_EXPORT CommonWorker : public QObject, public QRunnable
{
public:
CommonWorker();
~CommonWorker();
public:
virtual void run() override;
virtual void Write(char * data, std::size_t len) = 0;
virtual void DoRequest(CURL * curl) = 0;
static size_t CurlWriteCb(char *ptr, size_t size, size_t nmemb, void *userdata);
private:
};
Detail請求
說了這麼多,終於到了最關鍵的detail請求環節,如下StockListWorker
代碼所示,當Detail請求完成後,通過Response信號通知外部任務已完成,標的detail存放在了filePath指定的文件中。
對於StockListWorker
對象有以下幾點需要注意:
- 構造於主線程中,並且請求完成信號與主線程中槽函數所綁定
- DoRequest、Write和信號函數均運行於工作線程中
- 任務基類中我們設置了setAutoDelete爲true,因此所有的請求對象在執行完任務後都會析構
- 析構函數運行於工作線程中,與run函數所在線程一致
class RLNETWORK_EXPORT StockListWorker : public CommonWorker
{
Q_OBJECT
public:
StockListWorker(const QString & filePath);
~StockListWorker();
signals:
void Response(const QString & filePath);
public:
virtual void Write(char * data, std::size_t len) override;
virtual void DoRequest(CURL * curl) override;
private:
QString m_filePath;
QFile m_file;
};
DoRequest
函數是基類提供給我們重寫發送請求使用,如下代碼所示,展示了請求detail數據的過程,網絡請求我們統一使用libcurl進行完成,不使用Qt網絡庫主要是覺着不好用。
void StockListWorker::DoRequest(CURL * curl)
{
curl_easy_setopt(curl, CURLOPT_URL, StockListUrl);//準備發送request的url
CURLcode res = curl_easy_perform(curl);
if (res == CURLE_OK)
{
curl_off_t val = -1;
curl_easy_getinfo(curl, CURLINFO_NAMELOOKUP_TIME_T, &val);
emit Response(m_filePath);
}
else
{
std::cout << "curl_easy_perform() failed: " << curl_easy_strerror(res);
}
}
3、基礎服務模塊
市場服務
市場服務主要提供市場相關接口,如下代碼所示,IsTradingStatus
接口獲取當前標的所屬市場是否屬於交易狀態,根據市場狀態我們可以過濾一些無效操作,比如A股不開盤時,不需要請求detail等。
struct BASICSERVICES_EXPORT IMarketService
{
virtual ~IMarketService(){}
virtual bool IsTradingStatus(const SecurityInfo & security, long long mseconds) const = 0;
virtual bool IsTradingStatus(const QList<SecurityInfo> & securitys, long long mseconds) const = 0;
·
·
·
};
時間服務
時間管理器對於數據中心是相當重要的,因爲有了時間維度後,我們才能去定製一批時間相關的任務,比如輪訓任務、獲取當前時間等。
本文中的股票實時detail數據就需要添加了一個輪訓任務,因爲沒有長連接的加持,很多數據都需要我們自己去跟服務器要,雖然這樣會增大服務器的壓力,但是目前除過長連接外沒有其他更好的方式去完成這件事。
struct BASICSERVICES_EXPORT ITimerService
{
virtual ~ITimerService(){}
virtual QString AddTask(const std::function<void(long long)> & fun, long internal = 3000) = 0;
virtual bool HasTask(const QString &) = 0;
virtual void RemoveTask(const QString &) = 0;
virtual void ImmediatelyTask(const QString &) = 0;
virtual long long GetCurrentStamp() const = 0;
·
·
·
};
4、UI展示
訂閱股票detail
如下代碼所示,通過行情中心我們可以很簡單的去訂閱標的數據,之後通過重寫UpdateDetail
接口拿數據就行,其他的我們統一不用操心。
void HqSimple::on_pushButton_pull_clicked()
{
const QString & name = ui.comboBox->currentText();
const QString & id = ui.comboBox->currentData().toString();
const QStringList & items = id.split('_');
SecurityInfo info;
info.market = items.at(0);
info.symbol = items.at(1);
info.secType = "STK";
Quote::QuoteCenterInstance()->SubscribeDetail(this, info);
}
每一個需要訂閱行情數據的對象目前都是繼承自IQuoteCall
,或者持有一個IQuoteCall
,本篇文章包括後續系列文章都會採用第一種方案來實現數據訂閱,關於繼承和包含的優缺點及使用場景問題大家可以自行斟酌,本篇文章所講述案列數據類型較少,使用繼承足以完成目標。
struct QUOTECENTER_EXPORT IQuoteCall
{
virtual ~IQuoteCall(){}
virtual void UpdateDetail(const DetailCNItem &) = 0;
.
.
.
};
A股Detail數據定義
/*
0: 通用股份 // 名字;
1 : 5.050 // 今日開盤價
2 : 5.060 // 昨日收盤價
3 : 5.090 // 當前價格
4 : 5.110 // 今日最高價
5 : 5.030 // 今日最低價
6 : 5.090 // 競買價,即 “買一” 報價;
7 : 5.100 // 競賣價,即 “賣一” 報價;
8 : 3963000 // 成交的股票數,轉手乘 100
9 : 20106078.000// 成交金額 (元),轉萬除 10000
10 : 52800 //“買一” 申請 52800 股
11 : 5.090 //“買一” 報價;
12 : 90600 //“買二” 申請 90600 股
13 : 5.080 //“買二” 報價;
14 : 98500 //..
15 : 5.070 //..
16 : 105200 //..
17 : 5.060 //..
18 : 127900 //..
19 : 5.050 //..
20 : 104400 //“賣一” 申報 104400 股
21 : 5.100 //“賣一” 報價;
22 : 99700 //“賣二” 申報 99700 股
23 : 5.110 //“賣二” 報價;
24 : 111800 //..
25 : 5.120 //..
26 : 87500 //..
27 : 5.130 //..
28 : 73300 //..
29 : 5.140 //..
30 : 2022 - 02 - 14 // 日期
31 : 11 : 18 : 56 // 時間
*/
struct STOCKDATA_EXPORT DetailCNItem : public SecurityInfo
{
//0-9
QString name; //名字
double open; //今日開盤價
double preClose; // 昨日收盤價
double lastprice; // 當前價格
double high; // 今日最高價
double low; // 今日最低價
double bid; // 競買價,即 “買一” 報價;
double ask; // 競賣價,即 “賣一” 報價;
double volumn; // 成交的股票數,轉手乘 100
double amount; // 成交金額 (元),轉萬除 10000
struct AskBid
{
double price;//買/賣價
double volumn;//買/賣量
};
QVector<AskBid> asks;//賣五檔 //10-19
QVector<AskBid> bids;//買五檔 //20-29
//30-31
QString date; // 日期
QString time; // 時間
QString source; //原始數據
DetailCNItem();
DetailCNItem(const QString & str);
DetailCNItem(DetailCNItem && other);
void Clear();
};
刷新數據
UI數據刷新這裏就比較簡單了,文章最開始已經描述過UI數據分爲上中下三部分,上部和下部就是簡單文案設置,然後通過qss加了一些漲跌色配置,這裏就簡單展示下部分代碼。
void HqSimple::UpdateDetail(const DetailCNItem & data)
{
ui.label_open->setText(QString::number(data.open, 'f', 2));
.
.
.
ui.label_amount->setText(QString::number(data.amount, 'f', 2));
m_pListmodel->SetAskBid(data);
ui.textEdit->setText(QStringLiteral("原始數據:") + data.source);
}
盤口數據分爲6列:買檔、買價格、買數量、賣數量、賣價格和賣檔。實現起來也比較簡單,標準MVC即可搞定,代碼中表現爲QAbstractListModel
+QListView
+QStyledItemDelegate
,其中M和V都比較簡單,簡單的進行綁定之後就可以,這裏主要說下繪製界面用的QStyledItemDelegate
,其中最爲關鍵的就是paint
函數,相信用過Qt一年半載的同學都比較熟悉,代碼如下所示,繪製代碼比較簡單就不做說明了,不明白的同學進行留言或者私聊即可。
void AskBidDelegate::paint(QPainter * painter, const QStyleOptionViewItem & option, const QModelIndex & index) const
{
const DetailCNItem & detail = index.model()->data(index).value<DetailCNItem>();
//left
int y = 15;
int lwidth = option.rect.width() / 2;
const QPoint & gPos = QCursor::pos();
const QPoint & lPos = option.widget->mapFromGlobal(gPos);
int t = 0;
if (lPos.x() >= 0 && lPos.x() <= lwidth)
{
t = 1;
}
else if (lPos.x() >= lwidth && lPos.x() <= lwidth * 2)
{
t = 2;
}
{
painter->fillRect(option.rect.adjusted(0, 0, lwidth, 0)
, t == 1 && option.state.testFlag(QStyle::State_MouseOver) ? QColor(28, 109, 83) : QColor(39, 67, 62));
const DetailCNItem::AskBid & bid = detail.bids.at(index.row());
const QString & bidName = QStringLiteral("買%1").arg(index.row() + 1);
painter->setPen(QColor(Qt::white));
painter->drawText(10, option.rect.top() + y, bidName);
painter->setPen(QColor(Qt::red));
painter->drawText(60, option.rect.top() + y, PriceText(bid.price));
const QString & volumnName = PriceText(bid.volumn);
int volumW = painter->fontMetrics().width(volumnName);
painter->setPen(QColor(Qt::white));
painter->drawText(lwidth - volumW, option.rect.top() + y, volumnName);
}
//right
{
painter->fillRect(option.rect.adjusted(option.rect.width() / 2, 0, 0, 0)
, t == 2 && option.state.testFlag(QStyle::State_MouseOver) ? QColor(28, 109, 83) : QColor(68, 48, 58));
const DetailCNItem::AskBid & ask = detail.asks.at(index.row());
const QString & volumnName = PriceText(ask.volumn);
painter->setPen(QColor(Qt::white));
painter->drawText(lwidth + 10, option.rect.top() + y, volumnName);
painter->setPen(QColor(Qt::green));
painter->drawText(option.rect.width() - 98, option.rect.top() + y, PriceText(ask.price));
const QString & askName = QStringLiteral("賣%1").arg(index.row() + 1);
painter->setPen(QColor(Qt::white));
painter->drawText(option.rect.width() - 28, option.rect.top() + y, askName);
}
}
講到這裏,股票行情展示程序也差不都完成了,從數據訂閱、數據求情、數據緩存、數據回調和數據刷新大致都說了一遍,最後貼上項目工程截圖,大家可以參考。
此篇文章算是給股票行情演示系列文章開了一個頭,後續還會有更多文章出來,比如K線展示、分時圖展示等,敬請期待。。。
四、相關文章
- Qt 之股票組件 - 自選股 -- 列表可以拖拽、右鍵常用菜單
- Qt 之股票組件 - 股票檢索 -- 支持搜索結果預覽、鼠標、鍵盤操作
- QCustomplot使用分享(一) 能做什麼事
- QCustomplot使用分享(二) 源碼解讀
- QCustomplot使用分享(三) 圖
- QCustomplot使用分享(四) QCPAbstractItem
- QCustomplot使用分享(五) 佈局
- QCustomplot使用分享(六) 座標軸和網格線
- QCustomplot使用分享(七) 層(完結)
值得一看的優秀文章:
很重要--轉載聲明
-
本站文章無特別說明,皆爲原創,版權所有,轉載時請用鏈接的方式,給出原文出處。同時寫上原作者:朝十晚八 or Twowords
-
如要轉載,請原文轉載,如在轉載時修改本文,請事先告知,謝絕在轉載時通過修改本文達到有利於轉載者的目的。