對於量化交易來說,量化策略和技術系統缺一不可,爲了知其所以然,本文實現了一個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:如果需要更高級和細緻甚至可以用於實盤的功能,比如完整的開源交易系統,數據系統,算法交易,數據和交易接口等完備的解決方案,由於博客回覆不現實,只能私信聯繫啦~