對于量化交易來說,量化策略和技術系統缺一不可,為了知其所以然,本文實現了一個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; 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{ public: void OnFrontConnected(); void OnFrontDisconnected(int nReason); 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; 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(); std::string instrumentKey = std::string(pDepthMarketData->InstrumentID); if (g_KlineHash.find(instrumentKey) == g_KlineHash.end())g_KlineHash[instrumentKey] = TickToKlineHelper();g_KlineHash[instrumentKey].KLineFromRealtimeData(pDepthMarketData); }
- 每個tick世間節點系統都會調用這個函數,推送具體的行情截面數據
- 可以在此處將行情寫到本地,或者做一些數據處理(例如實時K線計算,判斷是否觸發策略等)
?
交易回調類
同理,也需要繼承CThostFtdcTraderSpi來實現自己的CustomTradeSpi類,用于交易下單、報單等操作的回調
CustomTradeSpi頭文件
?
#pragma once#include "CTP_API/ThostFtdcTraderApi.h" class CustomTradeSpi : public CThostFtdcTraderSpi{public: void OnFrontConnected(); void OnRspUserLogin(CThostFtdcRspUserLoginField *pRspUserLogin, CThostFtdcRspInfoField *pRspInfo, int nRequestID, bool bIsLast); void OnRspError(CThostFtdcRspInfoField *pRspInfo, int nRequestID, bool bIsLast); 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(g_pTradeInstrumentID, gLimitPrice, 1, gTradeDirection); }}
這里把下單錄入的操作放在了持倉結果出來之后的回調里面,策略交易也簡單的放在了這里,真實的情況下,應該是由行情觸發某個策略條件開一個線程進行策略交易
?
下單操作
?
?
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; orderInsertReq.VolumeTotalOriginal = volume; orderInsertReq.TimeCondition = THOST_FTDC_TC_GFD; orderInsertReq.VolumeCondition = THOST_FTDC_VC_AV; 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); strcpy(orderActionReq.OrderRef, pOrder->OrderRef); orderActionReq.FrontID = trade_front_id; orderActionReq.SessionID = session_id; orderActionReq.ActionFlag = THOST_FTDC_AF_Delete; 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; std::cout << "=====收到報單應答=====" << std::endl; if (isMyOrder(pOrder)){ if (isTradingOrder(pOrder)){ std::cout << "--->>> 等待成交中!" << std::endl; } 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線數據結構
?
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; std::ifstream srcInFile; std::ofstream dstOutFile;srcInFile.open(sFilePath, std::ios::in);dstOutFile.open(dFilePath, std::ios::out);dstOutFile << "開盤價" << ','<< "最高價" << ','<< "最低價" << ','<< "收盤價" << ',' << "成交量" << std::endl; 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;}} 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(); 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(); 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(); getchar(); return 0;}
- CThostFtdcMdApi跟CustomMdSpi要建立關聯,CThostFtdcTraderApi跟CustomTradeSpi建立關聯,其實就是類似于函數注冊
- 配置行情和交易地址
- 行情和交易分別是不同的線程,注意線程同步
- 記得內存回收
運行結果
行情
應答日志
?
存成csv表格
?
交易
應答日志
?
?
K線數據
?
?
?
?
報單情況
用上期所的快期軟件,登錄上自己的賬號之后,從過程序下單,在這個界面里能看到實時的報單成交狀況
?
?
源碼下載
?
csdn:demo
?
github:demo
?
結語
?
本文旨在為剛接觸CTP的小白們拋磚引玉,各交易接口的深度運用還需要看官方開發文檔。
另外,對于完整的量化交易系統來說,不僅要具備行情、交易、策略模塊,事件驅動、風控、回測模塊以及底層的數據存儲、網絡并發都是需要深入鉆研的方面,金融工程的Quant Researcher可以只專注于數據的分析、策略的研發,但是對于程序員Quant Developer來說,如何設計和開發一個高并發、低延遲、功能完善與策略結合緊密的量化交易系統的確是一項需要不斷完善的工程。
?
ps:如果需要更高級和細致甚至可以用于實盤的功能,比如完整的開源交易系統,數據系統,算法交易,數據和交易接口等完備的解決方案,由于博客回復不現實,只能私信聯系啦~
支持是知識分享的動力,有問題可掃碼哦
?
總結
以上是生活随笔為你收集整理的C++连接CTP接口实现简单量化交易(行情、交易、k线、策略)的全部內容,希望文章能夠幫你解決所遇到的問題。
如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。