簡單的股票行情展示演示(一) - 實時標的數據

原文鏈接:簡單的股票行情演示(一) - 實時標的數據

一、概述

很長一段時間都有一個想法,使用QCP去做一個行情展示小事例,一直沒有着手開發的原因主要是行情數據源的問題,畢竟穩定的數據纔是核心,加上今年5月份有了小寶寶也一直比較忙。

最近得空研究了下用C++實現股票行情展示相關內容,主要策略是通過拉取網上一些免費的開源接口數據,然後存儲到本地,在通過代碼讀取需要的日期數據進行展示。互聯網拉取行情數據方法網上隨手百度後會發現有一大堆,調取個別接口進行獲取數據也是很方便的,比如通過新浪開源獲取A股股票接口獲取實時行情數據就很簡單,瀏覽器url輸入框中輸入http://hq.sinajs.cn/list=sz002208,sh601318這段測試連接,按下回車,就會拿到list指定的兩支股票數據,效果如下圖所示。

需要特別注意:該接口拉取頻繁後,會被後臺403,所以本地需要做一些策略,儘可能減少無效拉取

image
嚐到了簡單的甜頭之後,接下來就是瘋狂百度、google,儘可能全面的整理開源的行情數據源,網上雖然文章很多,但是重複的內容特別多,講的比較好的文章有新浪股票 api股票數據 API 接口合集實時行情API,通過看這幾篇文章能大概瞭解到一些皮毛,簡單使用不成問題。總的來說提供了一個可操作的入口,數據源的問題算是暫時得到一部分解決,至於其他更完善的數據後續文章會有介紹,是由開源軟件提供,而且文檔比較詳細,之後更多的數據將會使用開源程序進行獲取

實時行情數據有了之後,接下來就是C++側代碼實現,主要分爲異步數據拉取、數據寫入本地文件、數據層讀取,回調給UI展示,本篇文章接下來的主要內容將會講解怎麼拉取數據、回調給UI等流程。

二、效果展示

如下效果圖所示,是一個簡單的多窗口程序,支持同時拉取多支股票實時行情數據並回調給UI。拿其中一個行情數據展示窗口爲例來說明,數據源是來自新浪行情API接口,測試程序UI展示總共分上中下三段,上半部分主要是股票盤口數據,展示開收盤價格、實時成交量等,中段是股票買賣五檔數據,最底下白色框中是數據源內容,也就是從互聯網接口拉取後的數據。

正常情況下測試程序只會跑一個窗口,圖示中多個窗口主要是爲了觀察方便。

image

三、實現代碼

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線程池進行管理,我們只需要關心任務中要幹什麼、任務結束後怎麼通知給外部即可。

任務基類

爲了減少大量重複代碼,這裏我們定義一個任務基類,基類中完成每個請求任務都需要操作的內容,然後把請求體和寫入內容封裝成接口,供子類重寫。

抽象內容包括:

  1. libcurl請求初始化、參數配置和清理
  2. 回調函數取到返回數據後整理標準字符串轉發給子類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對象有以下幾點需要注意:

  1. 構造於主線程中,並且請求完成信號與主線程中槽函數所綁定
  2. DoRequest、Write和信號函數均運行於工作線程中
  3. 任務基類中我們設置了setAutoDelete爲true,因此所有的請求對象在執行完任務後都會析構
  4. 析構函數運行於工作線程中,與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);
	}
}

講到這裏,股票行情展示程序也差不都完成了,從數據訂閱、數據求情、數據緩存、數據回調和數據刷新大致都說了一遍,最後貼上項目工程截圖,大家可以參考。

image

此篇文章算是給股票行情演示系列文章開了一個頭,後續還會有更多文章出來,比如K線展示、分時圖展示等,敬請期待。。。

四、相關文章

  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. 如要轉載,請原文轉載,如在轉載時修改本文,請事先告知,謝絕在轉載時通過修改本文達到有利於轉載者的目的。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章