1. 程式人生 > >C++連線CTP介面實現簡單量化交易(行情、交易、k線、策略)

C++連線CTP介面實現簡單量化交易(行情、交易、k線、策略)

對於量化交易來說,量化策略和技術系統缺一不可,為了知其所以然,本文實現了一個C++連線CTP介面進行模擬交易的demo,從接收行情、下訂單、資料處理到新增策略、掛載執行交易等多個環節來看一下量化交易的最簡單流程,管中窺豹,一探究竟。大笑

準備工作

交易所介面

這裡使用上期所提供的CTP介面API,通過CTP可以連線交易所進行行情接收交易。下載地址:CTP下載

本文使用的win32版本的,linux版本用法類似。

CTP介面包含以下內容:

  • ThostFtdcTraderApi.h:C++標頭檔案,包含交易相關的指令,如報單。
  • ThostFtdcMdApi.h:C++標頭檔案,包含獲取行情相關的指令。
  • ThostFtdcUserApiStruct.h:包含了所有用到的資料結構。
  • ThostFtdcUserApiDataType.h:包含了所有用到的資料型別。
  • thosttraderapi.lib、thosttraderapi.dll:交易部分的動態連結庫和靜態連結庫。
  • thostmduserapi.lib、thostmduserapi.dll:行情部分的動態連結庫和靜態連結庫。
  • error.dtd、error.xml:包含所有可能的錯誤資訊。

整個開發包有2個核心標頭檔案包括4個核心介面
CThostFtdcMdApi介面和CThostFtdcTraderApi兩個標頭檔案,一個處理行情

,一個處理交易

(1)處理行情的CThostFtdcMdApi介面有兩個類,分別是CThostFtdcMdApi和CThostFtdcMdSpi,以Api結尾的是用來下命令的,以Spi結尾的是用來響應命令的回撥。

(2)處理交易的CThostFtdcTraderApi介面也有兩個類,分別是CThostFtdcTraderApi和CThostFtdcTraderSpi,  通過CThostFtdcTraderApi向CTP傳送操作請求,通過CThostFtdcTraderSpi接收CTP的操作響應。

期貨賬戶

要連線期貨交易所交易,需要開設自己的賬戶,實現期貨交易、銀期轉賬、保證金等功能,由於小白一般不會用實盤資金交易,所以此處推薦用上期所提供的simnow虛擬交易平臺

simnow申請一個虛擬賬戶。

SIMNOW提供兩類資料前置地址:

(1)交易時段的地址,如09:00-15:00和21:00-02:30,使用第一套地址,這些資料是真實的行情資料,只是時間上比真實的行情會有延遲30秒左右(SIMNOW從交易所接收後轉發出來的)。

(2)非交易時段地址,這時的資料是歷史行情的播放,比如昨天的資料之類的,可以用來做程式除錯。

建議選擇申請那個7x24行情的賬戶,便於開發除錯。

開發步驟

工程總覽

其中,

  • CTP的API檔案配置到工程
  • CustomMdSpi.h,CustomMdSpi.cpp是派生的行情回撥類
  • CustomTradeSpi.h,CustomTradeSpi.cpp是派生的交易回撥類
  • TickToKlineHelper.h,TickToKlineHelper.cpp是處理時序資料,轉換成K線的類
  • StrategyTrade.h,StrategyTrade.cpp是策略類
  • main.cpp是程式的入口

一個簡單的程式化交易系統需要完成的業務可以劃分為:
1.基本操作,比如登入,訂閱等;
2.行情操作,比如對行情資料的接收,儲存等
3.訂單操作,比如報單;對報單,成交狀況的查詢;報單,成交狀況的私有回報等。
4.資料監聽和處理操作,比如接收到新資料之後的統計處理,滿足統計條件後的報單處理(其實這裡就是我們的策略所在)

匯入CTP介面庫

visual studio建立工程後,首先需要將ctp的標頭檔案以及連結庫(lib和dll)目錄配置到工程

// 連結庫
#pragma comment (lib, "thostmduserapi.lib")
#pragma comment (lib, "thosttraderapi.lib")

全域性引數

連線到交易所,需要配置經紀商程式碼、帳戶名、密碼以及訂閱合約和買賣合約的相關引數

// ---- 全域性變數 ---- //
// 公共引數
TThostFtdcBrokerIDType gBrokerID = "9999";                         // 模擬經紀商程式碼
TThostFtdcInvestorIDType gInvesterID = "";                         // 投資者賬戶名
TThostFtdcPasswordType gInvesterPassword = "";                     // 投資者密碼

// 行情引數
CThostFtdcMdApi *g_pMdUserApi = nullptr;                           // 行情指標
char gMdFrontAddr[] = "tcp://180.168.146.187:10010";               // 模擬行情前置地址
char *g_pInstrumentID[] = {"TF1706", "zn1705", "cs1801", "CF705"}; // 行情合約程式碼列表,中、上、大、鄭交易所各選一種
int instrumentNum = 4;                                             // 行情合約訂閱數量
unordered_map<string, TickToKlineHelper> g_KlineHash;              // 不同合約的k線儲存表

// 交易引數
CThostFtdcTraderApi *g_pTradeUserApi = nullptr;                    // 交易指標
char gTradeFrontAddr[] = "tcp://180.168.146.187:10001";            // 模擬交易前置地址
TThostFtdcInstrumentIDType g_pTradeInstrumentID = "m1709";         // 所交易的合約程式碼
TThostFtdcDirectionType gTradeDirection = THOST_FTDC_D_Sell;       // 買賣方向
TThostFtdcPriceType gLimitPrice = 2818;                            // 交易價格


這裡只是簡單的寫一下,真實完整的交易系統中,一般用配置檔案,有使用者去定製

行情回撥類

繼承CThostFtdcMdSpi實現自己的行情回撥類CustomMdSpi,在系統執行時這些重寫的函式會被CTP的系統api回撥從而實現個性化行情

CustomMdSpi標頭檔案

#pragma once
// ---- 派生的行情類 ---- //
#include <vector>
#include "CTP_API/ThostFtdcMdApi.h"

class CustomMdSpi: public CThostFtdcMdSpi
{
	// ---- 繼承自CTP父類的回撥介面並實現 ---- //
public:
	///當客戶端與交易後臺建立起通訊連線時(還未登入前),該方法被呼叫。
	void OnFrontConnected();

	///當客戶端與交易後臺通訊連線斷開時,該方法被呼叫。當發生這個情況後,API會自動重新連線,客戶端可不做處理。
	///@param nReason 錯誤原因
	///        0x1001 網路讀失敗
	///        0x1002 網路寫失敗
	///        0x2001 接收心跳超時
	///        0x2002 傳送心跳失敗
	///        0x2003 收到錯誤報文
	void OnFrontDisconnected(int nReason);

	///心跳超時警告。當長時間未收到報文時,該方法被呼叫。
	///@param nTimeLapse 距離上次接收報文的時間
	void OnHeartBeatWarning(int nTimeLapse);

	///登入請求響應
	void OnRspUserLogin(CThostFtdcRspUserLoginField *pRspUserLogin, CThostFtdcRspInfoField *pRspInfo, int nRequestID, bool bIsLast);

	///登出請求響應
	void OnRspUserLogout(CThostFtdcUserLogoutField *pUserLogout, CThostFtdcRspInfoField *pRspInfo, int nRequestID, bool bIsLast);

	///錯誤應答
	void OnRspError(CThostFtdcRspInfoField *pRspInfo, int nRequestID, bool bIsLast);

	///訂閱行情應答
	void OnRspSubMarketData(CThostFtdcSpecificInstrumentField *pSpecificInstrument, CThostFtdcRspInfoField *pRspInfo, int nRequestID, bool bIsLast);

	///取消訂閱行情應答
	void OnRspUnSubMarketData(CThostFtdcSpecificInstrumentField *pSpecificInstrument, CThostFtdcRspInfoField *pRspInfo, int nRequestID, bool bIsLast);

	///訂閱詢價應答
	void OnRspSubForQuoteRsp(CThostFtdcSpecificInstrumentField *pSpecificInstrument, CThostFtdcRspInfoField *pRspInfo, int nRequestID, bool bIsLast);

	///取消訂閱詢價應答
	void OnRspUnSubForQuoteRsp(CThostFtdcSpecificInstrumentField *pSpecificInstrument, CThostFtdcRspInfoField *pRspInfo, int nRequestID, bool bIsLast);

	///深度行情通知
	void OnRtnDepthMarketData(CThostFtdcDepthMarketDataField *pDepthMarketData);

	///詢價通知
	void OnRtnForQuoteRsp(CThostFtdcForQuoteRspField *pForQuoteRsp);
};

都是重寫回調函式

連線應答

// 連線成功應答
void CustomMdSpi::OnFrontConnected()
{
	std::cout << "=====建立網路連線成功=====" << std::endl;
	// 開始登入
	CThostFtdcReqUserLoginField loginReq;
	memset(&loginReq, 0, sizeof(loginReq));
	strcpy(loginReq.BrokerID, gBrokerID);
	strcpy(loginReq.UserID, gInvesterID);
	strcpy(loginReq.Password, gInvesterPassword);
	static int requestID = 0; // 請求編號
	int rt = g_pMdUserApi->ReqUserLogin(&loginReq, requestID);
	if (!rt)
		std::cout << ">>>>>>傳送登入請求成功" << std::endl;
	else
		std::cerr << "--->>>傳送登入請求失敗" << std::endl;
}

登入應答

// 登入應答
void CustomMdSpi::OnRspUserLogin(
	CThostFtdcRspUserLoginField *pRspUserLogin, 
	CThostFtdcRspInfoField *pRspInfo, 
	int nRequestID, 
	bool bIsLast)
{
	bool bResult = pRspInfo && (pRspInfo->ErrorID != 0);
	if (!bResult)
	{
		std::cout << "=====賬戶登入成功=====" << std::endl;
		std::cout << "交易日: " << pRspUserLogin->TradingDay << std::endl;
		std::cout << "登入時間: " << pRspUserLogin->LoginTime << std::endl;
		std::cout << "經紀商: " << pRspUserLogin->BrokerID << std::endl;
		std::cout << "帳戶名: " << pRspUserLogin->UserID << std::endl;
		// 開始訂閱行情
		int rt = g_pMdUserApi->SubscribeMarketData(g_pInstrumentID, instrumentNum);
		if (!rt)
			std::cout << ">>>>>>傳送訂閱行情請求成功" << std::endl;
		else
			std::cerr << "--->>>傳送訂閱行情請求失敗" << std::endl;
	}
	else
		std::cerr << "返回錯誤--->>> ErrorID=" << pRspInfo->ErrorID << ", ErrorMsg=" << pRspInfo->ErrorMsg << std::endl;
}

訂閱行情應答

// 訂閱行情應答
void CustomMdSpi::OnRspSubMarketData(
	CThostFtdcSpecificInstrumentField *pSpecificInstrument, 
	CThostFtdcRspInfoField *pRspInfo, 
	int nRequestID, 
	bool bIsLast)
{
	bool bResult = pRspInfo && (pRspInfo->ErrorID != 0);
	if (!bResult)
	{
		std::cout << "=====訂閱行情成功=====" << std::endl;
		std::cout << "合約程式碼: " << pSpecificInstrument->InstrumentID << std::endl;
		// 如果需要存入檔案或者資料庫,在這裡建立表頭,不同的合約單獨儲存
		char filePath[100] = {'\0'};
		sprintf(filePath, "%s_market_data.csv", pSpecificInstrument->InstrumentID);
		std::ofstream outFile;
		outFile.open(filePath, std::ios::out); // 新開檔案
		outFile << "合約程式碼" << ","
			<< "更新時間" << ","
			<< "最新價" << ","
			<< "成交量" << ","
			<< "買價一" << ","
			<< "買量一" << ","
			<< "賣價一" << ","
			<< "賣量一" << ","
			<< "持倉量" << ","
			<< "換手率"
			<< std::endl;
		outFile.close();
	}
	else
		std::cerr << "返回錯誤--->>> ErrorID=" << pRspInfo->ErrorID << ", ErrorMsg=" << pRspInfo->ErrorMsg << std::endl;
}
  • 因為是非同步介面,這裡連線、登入、訂閱行情是一步套一步來呼叫的,在執行過程中,會啟動一個行情執行緒,交易所每500ms會推送一個訂閱的行情tick資料,因此,某些介面會被連續間隔呼叫,直到連線關閉
  • 收到行情後除了存在記憶體,也可以用文字檔案或者資料庫等形式儲存起來,在這裡建立初始檔案或者建庫

深度行情通知

// 行情詳情通知
void CustomMdSpi::OnRtnDepthMarketData(CThostFtdcDepthMarketDataField *pDepthMarketData)
{
	// 列印行情,欄位較多,擷取部分
	std::cout << "=====獲得深度行情=====" << std::endl;
	std::cout << "交易日: " << pDepthMarketData->TradingDay << std::endl;
	std::cout << "交易所程式碼: " << pDepthMarketData->ExchangeID << std::endl;
	std::cout << "合約程式碼: " << pDepthMarketData->InstrumentID << std::endl;
	std::cout << "合約在交易所的程式碼: " << pDepthMarketData->ExchangeInstID << std::endl;
	std::cout << "最新價: " << pDepthMarketData->LastPrice << std::endl;
	std::cout << "數量: " << pDepthMarketData->Volume << std::endl;
	// 如果只獲取某一個合約行情,可以逐tick地存入檔案或資料庫
	char filePath[100] = {'\0'};
	sprintf(filePath, "%s_market_data.csv", pDepthMarketData->InstrumentID);
	std::ofstream outFile;
	outFile.open(filePath, std::ios::app); // 檔案追加寫入 
	outFile << pDepthMarketData->InstrumentID << "," 
		<< pDepthMarketData->UpdateTime << "." << pDepthMarketData->UpdateMillisec << "," 
		<< pDepthMarketData->LastPrice << "," 
		<< pDepthMarketData->Volume << "," 
		<< pDepthMarketData->BidPrice1 << "," 
		<< pDepthMarketData->BidVolume1 << "," 
		<< pDepthMarketData->AskPrice1 << "," 
		<< pDepthMarketData->AskVolume1 << "," 
		<< pDepthMarketData->OpenInterest << "," 
		<< pDepthMarketData->Turnover << std::endl;
	outFile.close();

	// 計算實時k線
	std::string instrumentKey = std::string(pDepthMarketData->InstrumentID);
	if (g_KlineHash.find(instrumentKey) == g_KlineHash.end())
		g_KlineHash[instrumentKey] = TickToKlineHelper();
	g_KlineHash[instrumentKey].KLineFromRealtimeData(pDepthMarketData);


	// 取消訂閱行情
	//int rt = g_pMdUserApi->UnSubscribeMarketData(g_pInstrumentID, instrumentNum);
	//if (!rt)
	//	std::cout << ">>>>>>傳送取消訂閱行情請求成功" << std::endl;
	//else
	//	std::cerr << "--->>>傳送取消訂閱行情請求失敗" << std::endl;
}
  • 每個tick世間節點系統都會呼叫這個函式,推送具體的行情截面資料
  • 可以在此處將行情寫到本地,或者做一些資料處理(例如實時K線計算,判斷是否觸發策略等)

交易回撥類

同理,也需要繼承CThostFtdcTraderSpi來實現自己的CustomTradeSpi類,用於交易下單、報單等操作的回撥

CustomTradeSpi標頭檔案

#pragma once
// ---- 派生的交易類 ---- //
#include "CTP_API/ThostFtdcTraderApi.h"

class CustomTradeSpi : public CThostFtdcTraderSpi
{
// ---- ctp_api部分回撥介面 ---- //
public:
	///當客戶端與交易後臺建立起通訊連線時(還未登入前),該方法被呼叫。
	void OnFrontConnected();

	///登入請求響應
	void OnRspUserLogin(CThostFtdcRspUserLoginField *pRspUserLogin, CThostFtdcRspInfoField *pRspInfo, int nRequestID, bool bIsLast);

	///錯誤應答
	void OnRspError(CThostFtdcRspInfoField *pRspInfo, int nRequestID, bool bIsLast);

	///當客戶端與交易後臺通訊連線斷開時,該方法被呼叫。當發生這個情況後,API會自動重新連線,客戶端可不做處理。
	void OnFrontDisconnected(int nReason);

	///心跳超時警告。當長時間未收到報文時,該方法被呼叫。
	void OnHeartBeatWarning(int nTimeLapse);

	///登出請求響應
	void OnRspUserLogout(CThostFtdcUserLogoutField *pUserLogout, CThostFtdcRspInfoField *pRspInfo, int nRequestID, bool bIsLast);

	///投資者結算結果確認響應
	void OnRspSettlementInfoConfirm(CThostFtdcSettlementInfoConfirmField *pSettlementInfoConfirm, CThostFtdcRspInfoField *pRspInfo, int nRequestID, bool bIsLast);

	///請求查詢合約響應
	void OnRspQryInstrument(CThostFtdcInstrumentField *pInstrument, CThostFtdcRspInfoField *pRspInfo, int nRequestID, bool bIsLast);

	///請求查詢資金賬戶響應
	void OnRspQryTradingAccount(CThostFtdcTradingAccountField *pTradingAccount, CThostFtdcRspInfoField *pRspInfo, int nRequestID, bool bIsLast);

	///請求查詢投資者持倉響應
	void OnRspQryInvestorPosition(CThostFtdcInvestorPositionField *pInvestorPosition, CThostFtdcRspInfoField *pRspInfo, int nRequestID, bool bIsLast);

	///報單錄入請求響應
	void OnRspOrderInsert(CThostFtdcInputOrderField *pInputOrder, CThostFtdcRspInfoField *pRspInfo, int nRequestID, bool bIsLast);

	///報單操作請求響應
	void OnRspOrderAction(CThostFtdcInputOrderActionField *pInputOrderAction, CThostFtdcRspInfoField *pRspInfo, int nRequestID, bool bIsLast);

	///報單通知
	void OnRtnOrder(CThostFtdcOrderField *pOrder);

	///成交通知
	void OnRtnTrade(CThostFtdcTradeField *pTrade);
	
// ---- 自定義函式 ---- //
public:
	bool loginFlag; // 登陸成功的標識
	void reqOrderInsert(
		TThostFtdcInstrumentIDType instrumentID,
		TThostFtdcPriceType price,
		TThostFtdcVolumeType volume,
		TThostFtdcDirectionType direction); // 個性化報單錄入,外部呼叫
private:
	void reqUserLogin(); // 登入請求
	void reqUserLogout(); // 登出請求
	void reqSettlementInfoConfirm(); // 投資者結果確認
	void reqQueryInstrument(); // 請求查詢合約
	void reqQueryTradingAccount(); // 請求查詢資金帳戶
	void reqQueryInvestorPosition(); // 請求查詢投資者持倉
	void reqOrderInsert(); // 請求報單錄入
	
	void reqOrderAction(CThostFtdcOrderField *pOrder); // 請求報單操作
	bool isErrorRspInfo(CThostFtdcRspInfoField *pRspInfo); // 是否收到錯誤資訊
	bool isMyOrder(CThostFtdcOrderField *pOrder); // 是否我的報單回報
	bool isTradingOrder(CThostFtdcOrderField *pOrder); // 是否正在交易的報單
};

除了重寫的基類函式,還自己封裝一些主動呼叫的操作函式,比如登入登出、下單報單、查詢報單等

登入應答

void CustomTradeSpi::OnRspUserLogin(
	CThostFtdcRspUserLoginField *pRspUserLogin,
	CThostFtdcRspInfoField *pRspInfo,
	int nRequestID,
	bool bIsLast)
{
	if (!isErrorRspInfo(pRspInfo))
	{
		std::cout << "=====賬戶登入成功=====" << std::endl;
		loginFlag = true;
		std::cout << "交易日: " << pRspUserLogin->TradingDay << std::endl;
		std::cout << "登入時間: " << pRspUserLogin->LoginTime << std::endl;
		std::cout << "經紀商: " << pRspUserLogin->BrokerID << std::endl;
		std::cout << "帳戶名: " << pRspUserLogin->UserID << std::endl;
		// 儲存會話引數
		trade_front_id = pRspUserLogin->FrontID;
		session_id = pRspUserLogin->SessionID;
		strcpy(order_ref, pRspUserLogin->MaxOrderRef);

		// 投資者結算結果確認
		reqSettlementInfoConfirm();
	}
}

查詢投資者結算結果應答

void CustomTradeSpi::OnRspSettlementInfoConfirm(
	CThostFtdcSettlementInfoConfirmField *pSettlementInfoConfirm,
	CThostFtdcRspInfoField *pRspInfo,
	int nRequestID,
	bool bIsLast)
{
	if (!isErrorRspInfo(pRspInfo))
	{
		std::cout << "=====投資者結算結果確認成功=====" << std::endl;
		std::cout << "確認日期: " << pSettlementInfoConfirm->ConfirmDate << std::endl;
		std::cout << "確認時間: " << pSettlementInfoConfirm->ConfirmTime << std::endl;
		// 請求查詢合約
		reqQueryInstrument();
	}
}

查詢合約應答

void CustomTradeSpi::OnRspQryInstrument(
	CThostFtdcInstrumentField *pInstrument,
	CThostFtdcRspInfoField *pRspInfo,
	int nRequestID,
	bool bIsLast)
{
	if (!isErrorRspInfo(pRspInfo))
	{
		std::cout << "=====查詢合約結果成功=====" << std::endl;
		std::cout << "交易所程式碼: " << pInstrument->ExchangeID << std::endl;
		std::cout << "合約程式碼: " << pInstrument->InstrumentID << std::endl;
		std::cout << "合約在交易所的程式碼: " << pInstrument->ExchangeInstID << std::endl;
		std::cout << "執行價: " << pInstrument->StrikePrice << std::endl;
		std::cout << "到期日: " << pInstrument->EndDelivDate << std::endl;
		std::cout << "當前交易狀態: " << pInstrument->IsTrading << std::endl;
		// 請求查詢投資者資金賬戶
		reqQueryTradingAccount();
	}
}

查詢投資者資金帳戶應答

void CustomTradeSpi::OnRspQryTradingAccount(
	CThostFtdcTradingAccountField *pTradingAccount,
	CThostFtdcRspInfoField *pRspInfo,
	int nRequestID,
	bool bIsLast)
{
	if (!isErrorRspInfo(pRspInfo))
	{
		std::cout << "=====查詢投資者資金賬戶成功=====" << std::endl;
		std::cout << "投資者賬號: " << pTradingAccount->AccountID << std::endl;
		std::cout << "可用資金: " << pTradingAccount->Available << std::endl;
		std::cout << "可取資金: " << pTradingAccount->WithdrawQuota << std::endl;
		std::cout << "當前保證金: " << pTradingAccount->CurrMargin << std::endl;
		std::cout << "平倉盈虧: " << pTradingAccount->CloseProfit << std::endl;
		// 請求查詢投資者持倉
		reqQueryInvestorPosition();
	}
}

查詢投資者持倉應答

void CustomTradeSpi::OnRspQryInvestorPosition(
	CThostFtdcInvestorPositionField *pInvestorPosition,
	CThostFtdcRspInfoField *pRspInfo,
	int nRequestID,
	bool bIsLast)
{
	if (!isErrorRspInfo(pRspInfo))
	{
		std::cout << "=====查詢投資者持倉成功=====" << std::endl;
		if (pInvestorPosition)
		{
			std::cout << "合約程式碼: " << pInvestorPosition->InstrumentID << std::endl;
			std::cout << "開倉價格: " << pInvestorPosition->OpenAmount << std::endl;
			std::cout << "開倉量: " << pInvestorPosition->OpenVolume << std::endl;
			std::cout << "開倉方向: " << pInvestorPosition->PosiDirection << std::endl;
			std::cout << "佔用保證金:" << pInvestorPosition->UseMargin << std::endl;
		}
		else
			std::cout << "----->該合約未持倉" << std::endl;
		
		// 報單錄入請求(這裡是一部介面,此處是按順序執行)
		/*if (loginFlag)
			reqOrderInsert();*/
		if (loginFlag)
			reqOrderInsert(g_pTradeInstrumentID, gLimitPrice, 1, gTradeDirection); // 自定義一筆交易

		// 策略交易
		/*std::cout << "=====開始進入策略交易=====" << std::endl;
		while (loginFlag)
			StrategyCheckAndTrade(g_pTradeInstrumentID, this);*/
	}
}

這裡把下單錄入的操作放在了持倉結果出來之後的回撥裡面,策略交易也簡單的放在了這裡,真實的情況下,應該是由行情觸發某個策略條件開一個執行緒進行策略交易

下單操作

void CustomTradeSpi::reqOrderInsert(
	TThostFtdcInstrumentIDType instrumentID,
	TThostFtdcPriceType price,
	TThostFtdcVolumeType volume,
	TThostFtdcDirectionType direction)
{
	CThostFtdcInputOrderField orderInsertReq;
	memset(&orderInsertReq, 0, sizeof(orderInsertReq));
	///經紀公司程式碼
	strcpy(orderInsertReq.BrokerID, gBrokerID);
	///投資者程式碼
	strcpy(orderInsertReq.InvestorID, gInvesterID);
	///合約程式碼
	strcpy(orderInsertReq.InstrumentID, instrumentID);
	///報單引用
	strcpy(orderInsertReq.OrderRef, order_ref);
	///報單價格條件: 限價
	orderInsertReq.OrderPriceType = THOST_FTDC_OPT_LimitPrice;
	///買賣方向: 
	orderInsertReq.Direction = direction;
	///組合開平標誌: 開倉
	orderInsertReq.CombOffsetFlag[0] = THOST_FTDC_OF_Open;
	///組合投機套保標誌
	orderInsertReq.CombHedgeFlag[0] = THOST_FTDC_HF_Speculation;
	///價格
	orderInsertReq.LimitPrice = price;
	///數量:1
	orderInsertReq.VolumeTotalOriginal = volume;
	///有效期型別: 當日有效
	orderInsertReq.TimeCondition = THOST_FTDC_TC_GFD;
	///成交量型別: 任何數量
	orderInsertReq.VolumeCondition = THOST_FTDC_VC_AV;
	///最小成交量: 1
	orderInsertReq.MinVolume = 1;
	///觸發條件: 立即
	orderInsertReq.ContingentCondition = THOST_FTDC_CC_Immediately;
	///強平原因: 非強平
	orderInsertReq.ForceCloseReason = THOST_FTDC_FCC_NotForceClose;
	///自動掛起標誌: 否
	orderInsertReq.IsAutoSuspend = 0;
	///使用者強評標誌: 否
	orderInsertReq.UserForceClose = 0;

	static int requestID = 0; // 請求編號
	int rt = g_pTradeUserApi->ReqOrderInsert(&orderInsertReq, ++requestID);
	if (!rt)
		std::cout << ">>>>>>傳送報單錄入請求成功" << std::endl;
	else
		std::cerr << "--->>>傳送報單錄入請求失敗" << std::endl;
}

通過過載寫了兩個函式,一個是用預設引數下單,一個可以傳參下單,比如設定合約程式碼、價格、數量等

報單操作

void CustomTradeSpi::reqOrderAction(CThostFtdcOrderField *pOrder)
{
	static bool orderActionSentFlag = false; // 是否傳送了報單
	if (orderActionSentFlag)
		return;

	CThostFtdcInputOrderActionField orderActionReq;
	memset(&orderActionReq, 0, sizeof(orderActionReq));
	///經紀公司程式碼
	strcpy(orderActionReq.BrokerID, pOrder->BrokerID);
	///投資者程式碼
	strcpy(orderActionReq.InvestorID, pOrder->InvestorID);
	///報單操作引用
	//	TThostFtdcOrderActionRefType	OrderActionRef;
	///報單引用
	strcpy(orderActionReq.OrderRef, pOrder->OrderRef);
	///請求編號
	//	TThostFtdcRequestIDType	RequestID;
	///前置編號
	orderActionReq.FrontID = trade_front_id;
	///會話編號
	orderActionReq.SessionID = session_id;
	///交易所程式碼
	//	TThostFtdcExchangeIDType	ExchangeID;
	///報單編號
	//	TThostFtdcOrderSysIDType	OrderSysID;
	///操作標誌
	orderActionReq.ActionFlag = THOST_FTDC_AF_Delete;
	///價格
	//	TThostFtdcPriceType	LimitPrice;
	///數量變化
	//	TThostFtdcVolumeType	VolumeChange;
	///使用者程式碼
	//	TThostFtdcUserIDType	UserID;
	///合約程式碼
	strcpy(orderActionReq.InstrumentID, pOrder->InstrumentID);
	static int requestID = 0; // 請求編號
	int rt = g_pTradeUserApi->ReqOrderAction(&orderActionReq, ++requestID);
	if (!rt)
		std::cout << ">>>>>>傳送報單操作請求成功" << std::endl;
	else
		std::cerr << "--->>>傳送報單操作請求失敗" << std::endl;
	orderActionSentFlag = true;
}

主要是對於未成交的訂單進行編輯或者撤銷操作

報單應答

void CustomTradeSpi::OnRtnOrder(CThostFtdcOrderField *pOrder)
{
	char str[10];
	sprintf(str, "%d", pOrder->OrderSubmitStatus);
	int orderState = atoi(str) - 48;	//報單狀態0=已經提交,3=已經接受

	std::cout << "=====收到報單應答=====" << std::endl;

	if (isMyOrder(pOrder))
	{
		if (isTradingOrder(pOrder))
		{
			std::cout << "--->>> 等待成交中!" << std::endl;
			//reqOrderAction(pOrder); // 這裡可以撤單
			//reqUserLogout(); // 登出測試
		}
		else if (pOrder->OrderStatus == THOST_FTDC_OST_Canceled)
			std::cout << "--->>> 撤單成功!" << std::endl;
	}
}

void CustomTradeSpi::OnRtnTrade(CThostFtdcTradeField *pTrade)
{
	std::cout << "=====報單成功成交=====" << std::endl;
	std::cout << "成交時間: " << pTrade->TradeTime << std::endl;
	std::cout << "合約程式碼: " << pTrade->InstrumentID << std::endl;
	std::cout << "成交價格: " << pTrade->Price << std::endl;
	std::cout << "成交量: " << pTrade->Volume << std::endl;
	std::cout << "開平倉方向: " << pTrade->Direction << std::endl;
}

等待成交進行輪詢可以選擇報單操作,成交完成後的應答

時間序列轉K線

從交易拿到的tick資料是時間序列資料,在證券交易中其實還需要根據時間序列算出一些技術指標資料,例如MACD,KDJ、K線等,這裡簡單地對資料做一下處理,寫一個TickToKlineHelper將時間序列專程K線

K線資料結構

// k線資料結構
struct KLineDataType
{
	double open_price;   // 開
	double high_price;   // 高
	double low_price;    // 低
	double close_price;  // 收
	int volume;          // 量
};


轉換函式

void TickToKlineHelper::KLineFromLocalData(const std::string &sFilePath, const std::string &dFilePath)
{
	// 先清理殘留資料
	m_priceVec.clear();
	m_volumeVec.clear();
	m_KLineDataArray.clear();

	std::cout << "開始轉換tick到k線..." << std::endl;
	// 預設讀取的tick資料表有4個欄位:合約程式碼、更新時間、最新價、成交量
	std::ifstream srcInFile;
	std::ofstream dstOutFile;
	srcInFile.open(sFilePath, std::ios::in);
	dstOutFile.open(dFilePath, std::ios::out);
	dstOutFile << "開盤價" << ','
		<< "最高價" << ','
		<< "最低價" << ','
		<< "收盤價" << ',' 
		<< "成交量" << std::endl;

	// 一遍解析檔案一邊計算k線資料,1分鐘k線每次讀取60 * 2 = 120行資料
	std::string lineStr;
	bool isFirstLine = true;
	while (std::getline(srcInFile, lineStr))
	{
		if (isFirstLine)
		{
			// 跳過第一行表頭
			isFirstLine = false;
			continue;
		}
		std::istringstream ss(lineStr);
		std::string fieldStr;
		int count = 4;
		while (std::getline(ss, fieldStr, ','))
		{
			count--;
			if (count == 1)
				m_priceVec.push_back(std::atof(fieldStr.c_str()));
			else if (count == 0)
			{
				m_volumeVec.push_back(std::atoi(fieldStr.c_str()));
				break;
			}
		}

		// 計算k線

		if (m_priceVec.size() == kDataLineNum)
		{
			KLineDataType k_line_data;
			k_line_data.open_price = m_priceVec.front();
			k_line_data.high_price = *std::max_element(m_priceVec.cbegin(), m_priceVec.cend());
			k_line_data.low_price = *std::min_element(m_priceVec.cbegin(), m_priceVec.cend());
			k_line_data.close_price = m_priceVec.back();
			// 成交量的真實的演算法是當前區間最後一個成交量減去上去一個區間最後一個成交量
			k_line_data.volume = m_volumeVec.back() - m_volumeVec.front(); 
			//m_KLineDataArray.push_back(k_line_data); // 此處可以存到記憶體
			
			dstOutFile << k_line_data.open_price << ','
				<< k_line_data.high_price << ','
				<< k_line_data.low_price << ','
				<< k_line_data.close_price << ','
				<< k_line_data.volume << std::endl;

			m_priceVec.clear();
			m_volumeVec.clear();
		}
	}

	srcInFile.close();
	dstOutFile.close();

	std::cout << "k線生成成功" << std::endl;
}

void TickToKlineHelper::KLineFromRealtimeData(CThostFtdcDepthMarketDataField *pDepthMarketData)
{
	m_priceVec.push_back(pDepthMarketData->LastPrice);
	m_volumeVec.push_back(pDepthMarketData->Volume);
	if (m_priceVec.size() == kDataLineNum)
	{
		KLineDataType k_line_data;
		k_line_data.open_price = m_priceVec.front();
		k_line_data.high_price = *std::max_element(m_priceVec.cbegin(), m_priceVec.cend());
		k_line_data.low_price = *std::min_element(m_priceVec.cbegin(), m_priceVec.cend());
		k_line_data.close_price = m_priceVec.back();
		// 成交量的真實的演算法是當前區間最後一個成交量減去上去一個區間最後一個成交量
		k_line_data.volume = m_volumeVec.back() - m_volumeVec.front();
		m_KLineDataArray.push_back(k_line_data); // 此處可以存到記憶體

		m_priceVec.clear();
		m_volumeVec.clear();
	}
}

  • 可以從本地檔案中讀取行情資料,進行離線轉換,也可以在接受到行情時進行實時計算
  • 基本思想是,針對每個合約程式碼,建立字典,維持一個行情陣列,當時間間隔達到要求(例如分鐘、分時、分日)時計算該時段的開、高、低、收、成交量等資料存入K線陣列
  • 最低時間單位的K線計算出來之後,高時間間隔的K線資料可以根據低時間間隔的K線計算出來(例如,算出了分鐘K,那麼分時K就根據分鐘K來算)
  • 本例子中只是實現了一個大概的原理,非常不精確,僅供參考

策略交易

量化交易系統最終是需要將編寫的策略程式碼掛載到系統中進行策略交易的,這裡做了一個簡單的實現

StrategyTrade.h

#pragma once
// ---- 簡單策略交易的類 ---- //

#include <functional>
#include "CTP_API/ThostFtdcUserApiStruct.h"
#include "TickToKlineHelper.h"
#include "CustomTradeSpi.h"

typedef void(*reqOrderInsertFun)(
	TThostFtdcInstrumentIDType instrumentID,
	TThostFtdcPriceType price,
	TThostFtdcVolumeType volume,
	TThostFtdcDirectionType direction);

using ReqOrderInsertFunctionType = std::function<
	void(TThostFtdcInstrumentIDType instrumentID,
	TThostFtdcPriceType price,
	TThostFtdcVolumeType volume,
	TThostFtdcDirectionType direction)>;

void StrategyCheckAndTrade(TThostFtdcInstrumentIDType instrumentID, CustomTradeSpi *customTradeSpi);


StrategyTrade.cpp

#include <vector>
#include <string>
#include <unordered_map>
#include <thread>
#include <mutex>
#include "StrategyTrade.h"
#include "CustomTradeSpi.h"

extern std::unordered_map<std::string, TickToKlineHelper> g_KlineHash;

// 執行緒互斥量
std::mutex marketDataMutex;

void StrategyCheckAndTrade(TThostFtdcInstrumentIDType instrumentID, CustomTradeSpi *customTradeSpi)
{
	// 加鎖
	std::lock_guard<std::mutex> lk(marketDataMutex);
	TickToKlineHelper tickToKlineObject = g_KlineHash.at(std::string(instrumentID));
	// 策略
	std::vector<double> priceVec = tickToKlineObject.m_priceVec;
	if (priceVec.size() >= 3)
	{
		int len = priceVec.size();
		// 最後連續三個上漲就買開倉,反之就賣開倉,這裡暫時用最後一個價格下單
		if (priceVec[len - 1] > priceVec[len - 2] && priceVec[len - 2] > priceVec[len - 3])
			customTradeSpi->reqOrderInsert(instrumentID, priceVec[len - 1], 1, THOST_FTDC_D_Buy);
		else if (priceVec[len - 1] < priceVec[len - 2] && priceVec[len - 2] < priceVec[len - 3])
			customTradeSpi->reqOrderInsert(instrumentID, priceVec[len - 1], 1, THOST_FTDC_D_Buy);
	}
}

  • 基本思想,針對指定合約,判斷如果連續三個上漲就買開倉,連續三個下跌就賣開倉,價格都是用最新價
  • 因為行情和交易是分開的執行緒,涉及到執行緒競爭,所以在實際下單時需要加入互斥鎖,執行緒同步
  • 策略如何被行情觸發然後交易其實需要用事件驅動來做的,這裡沒有實現T_T

入口

main.cpp

int main()
{
	// 賬號密碼
	cout << "請輸入賬號: ";
	scanf("%s", gInvesterID);
	cout << "請輸入密碼: ";
	scanf("%s", gInvesterPassword);

	// 初始化行情執行緒
	cout << "初始化行情..." << endl;
	g_pMdUserApi = CThostFtdcMdApi::CreateFtdcMdApi();   // 建立行情例項
	CThostFtdcMdSpi *pMdUserSpi = new CustomMdSpi;       // 建立行情回撥例項
	g_pMdUserApi->RegisterSpi(pMdUserSpi);               // 註冊事件類
	g_pMdUserApi->RegisterFront(gMdFrontAddr);           // 設定行情前置地址
	g_pMdUserApi->Init();                                // 連線執行
	


	// 初始化交易執行緒
	cout << "初始化交易..." << endl;
	g_pTradeUserApi = CThostFtdcTraderApi::CreateFtdcTraderApi(); // 建立交易例項
	//CThostFtdcTraderSpi *pTradeSpi = new CustomTradeSpi;
	CustomTradeSpi *pTradeSpi = new CustomTradeSpi;               // 建立交易回撥例項
	g_pTradeUserApi->RegisterSpi(pTradeSpi);                      // 註冊事件類
	g_pTradeUserApi->SubscribePublicTopic(THOST_TERT_RESTART);    // 訂閱公共流
	g_pTradeUserApi->SubscribePrivateTopic(THOST_TERT_RESTART);   // 訂閱私有流
	g_pTradeUserApi->RegisterFront(gTradeFrontAddr);              // 設定交易前置地址
	g_pTradeUserApi->Init();                                      // 連線執行
		

	// 等到執行緒退出
	g_pMdUserApi->Join();
	delete pMdUserSpi;
	g_pMdUserApi->Release();

	g_pTradeUserApi->Join();
	delete pTradeSpi;
	g_pTradeUserApi->Release();

	// 轉換本地k線資料
	//TickToKlineHelper tickToKlineHelper;
	//tickToKlineHelper.KLineFromLocalData("market_data.csv", "K_line_data.csv");
	
	getchar();
	return 0;
}
  • CThostFtdcMdApi跟CustomMdSpi要建立關聯,CThostFtdcTraderApi跟CustomTradeSpi建立關聯,其實就是類似於函式註冊
  • 配置行情和交易地址
  • 行情和交易分別是不同的執行緒,注意執行緒同步
  • 記得記憶體回收

執行結果

行情

應答日誌

存成csv表格

交易

應答日誌

K線資料

報單情況

用上期所的快期軟體,登入上自己的賬號之後,從過程式下單,在這個介面裡能看到實時的報單成交狀況

原始碼下載

csdn:demo

github:demo

結語

本文旨在為剛接觸CTP的小白們拋磚引玉,各交易介面的深度運用還需要看官方開發文件。

另外,對於完整的量化交易系統來說,不僅要具備行情、交易、策略模組,事件驅動、風控、回測模組以及底層的資料儲存、網路併發都是需要深入鑽研的方面,金融工程的Quant Researcher可以只專注於資料的分析、策略的研發,但是對於程式設計師Quant Developer來說,如何設計和開發一個高併發、低延遲、功能完善與策略結合緊密的量化交易系統的確是一項需要不斷完善的工程。

ps:如果需要更高階和細緻甚至可以用於實盤的功能,比如完整的開源交易系統,資料系統,演算法交易,資料和交易介面等完備的解決方案,由於部落格回覆不現實,只能私信聯絡啦~

支援是知識分享的動力,有問題可掃碼哦