11.6 报单时序竞争¶
在交易中,不同服务器上程序发出的请求(报单)到达交易所有先后顺序,高频交
易对这个先后顺序非常敏感。根据日志文件,统计不同服务器上,在相同时
间点相同合约(股票代码)的报单到达交易所的先后比例。
(1) 每台服务器可能有多份日志文件,每份日志文件的文件名由 PID、MachineID 和日期构
成,例如“5169_MachineE_2025-01-10.log”。MachineID 用于区分不同的服务器。
(2) 程序每笔交易会先发送请求(报单)到交易所,然后收到交易所的应答(报单回报)。
所以每份日志文件中有两类日志,“vendorSendOrderImpl”和“notifyOrder”。其中,
“vendorSendOrderImpl”是报单发送日志,“notifyOrder”是报单回报日志,两类日志中
都包含 OrderRef 字段用于标识同一报单。
注意,不同服务器上的 OrderRef 可能重复,但互相间毫无关系,只有同一个机器上的
相同 OrderRef 才表示同一笔订单。
(3) 以“vendorSendOrderImpl”的日志时间作为报单发单时间,InstrumentID 字段代表报单
交易的合约;以“notifyOrder”日志中的 ExchangeTimestamp 字段作为报单到达交易所
的时间(该时间类似 UnixTime),ExchangeTimestamp 值比较小的合约先到达交易所。
(4) 对于不同服务器上发单时间差值在 200ms 内的相同合约报单,可以认为是相同时间
点的相同合约报单。
注意:相同时间点下,同一台服务器上程序可能在同一个合约上连续报单,即同时有多
笔报单。同一服务器相同时间点上自身的相同合约报单比较没有意义。
(5) 实际实盘时存在会存在各种特殊情况,拓展延伸可挖掘更多数据信息。
示例:
输入:多份日志文件
输出:服务器的报单数量(一次完整的请求及回报记为一次报单数量),相同时间点报单数
量,不同服务器间的先后比例
示例日志文件名
3394_MachineD_2025-01-10.log
21963_MachineA_2025-01-10.log
示例日志文件数据条目
[01/10 09:30:00.525277035] [ info ] [ 3394/3601 ] [notifyOrder] InstrumentID:A Direction:0 OffsetFlag:0 HedgeFlag:1 ConnectionSelectionType:1 OrderRef:2 OrderType:0 ConnectionID:1 RealConnectionID:1 ErrorNo:0 ExchangeRef:0 ExchangeTimestamp:22372 OrderStatus:1 OrderLocalID:5
[01/10 09:30:00.525277421] [ info ] [ 3394/3601 ] [notifyOrder] InstrumentID:A Direction:0 OffsetFlag:0 HedgeFlag:1 ConnectionSelectionType:1 OrderRef:2 OrderType:0 ConnectionID:1 RealConnectionID:1 ErrorNo:0 ExchangeRef:0 ExchangeTimestamp:22372 OrderStatus:2 OrderLocalID:5
#include <algorithm>
#include <cmath>
#include <ctime>
#include <filesystem>
#include <fstream>
#include <iomanip>
#include <iostream>
#include <map>
#include <set>
#include <sstream>
#include <string>
#include <vector>
using namespace std;
// Structure to represent a log entry
struct LogEntry {
string machineID;
long long logTimeMs; // Log time in milliseconds
string logType; // "vendorSendOrderImpl" or "notifyOrder"
int orderRef;
string instrumentID;
long long exchangeTimestamp; // Only for notifyOrder
};
// Structure to represent an order
struct Order {
string machineID;
int orderRef;
long long sendTimeMs; // Time from vendorSendOrderImpl
string instrumentID;
long long exchangeTimestamp; // Time from notifyOrder
bool hasNotify; // Whether it has received notifyOrder
};
// Get all .log files from a directory
vector<string> getLogFilesFromDir(const string& dirPath) {
vector<string> logFiles;
try {
namespace fs = std::filesystem;
if (fs::exists(dirPath) && fs::is_directory(dirPath)) {
for (const auto& entry : fs::directory_iterator(dirPath)) {
if (entry.is_regular_file()) {
string filename = entry.path().filename().string();
if (filename.length() >= 4 &&
filename.substr(filename.length() - 4) == ".log") {
string fullPath = entry.path().string();
logFiles.push_back(fullPath);
}
}
}
}
} catch (const exception& e) {
cerr << "Error reading directory " << dirPath << ": " << e.what() << endl;
}
return logFiles;
}
class LogAnalyzer {
private:
map<pair<string, int>, Order> orders; // Key: (machineID, orderRef)
vector<LogEntry> allEntries;
map<string, int> machineOrderCount; // Total orders per machine
public:
// Parse a single log file
void parseLogFile(const string& filename) {
ifstream file(filename);
if (!file.is_open()) {
cerr << "Cannot open file: " << filename << endl;
return;
}
// Extract MachineID from filename (format: PID_MachineID_Date.log)
string machineID = extractMachineID(filename);
if (machineID.empty()) {
cerr << "Invalid filename format: " << filename << endl;
return;
}
string line;
while (getline(file, line)) {
parseLogLine(line, machineID);
}
file.close();
}
// Extract MachineID from filename
string extractMachineID(const string& filename) {
// Expected format: PID_MachineID_Date.log
size_t lastSlash = filename.find_last_of("/\\");
string fname =
(lastSlash != string::npos) ? filename.substr(lastSlash + 1) : filename;
size_t firstUnderscore = fname.find('_');
size_t secondUnderscore = fname.find('_', firstUnderscore + 1);
if (firstUnderscore != string::npos && secondUnderscore != string::npos) {
return fname.substr(firstUnderscore + 1,
secondUnderscore - firstUnderscore - 1);
}
return "";
}
// Parse a single line of log
void parseLogLine(const string& line, const string& machineID) {
// Format: [MM/DD HH:MM:SS.nnnnnnnnn] [ info ] [ PID/TID ] [logType]
// fields...
if (line.find("[vendorSendOrderImpl]") != string::npos) {
parseSendOrder(line, machineID);
} else if (line.find("[notifyOrder]") != string::npos) {
parseNotifyOrder(line, machineID);
}
}
// Parse vendorSendOrderImpl log
void parseSendOrder(const string& line, const string& machineID) {
// Extract time
long long timeMs = extractTimeMs(line);
// Extract OrderRef and InstrumentID
int orderRef = extractIntField(line, "OrderRef:");
string instrumentID = extractStringField(line, "InstrumentID:");
if (orderRef != -1 && !instrumentID.empty()) {
auto key = make_pair(machineID, orderRef);
if (orders.find(key) == orders.end()) {
Order order;
order.machineID = machineID;
order.orderRef = orderRef;
order.sendTimeMs = timeMs;
order.instrumentID = instrumentID;
order.exchangeTimestamp = 0;
order.hasNotify = false;
orders[key] = order;
} else {
orders[key].sendTimeMs = timeMs;
orders[key].instrumentID = instrumentID;
}
}
}
// Parse notifyOrder log
void parseNotifyOrder(const string& line, const string& machineID) {
// Extract OrderRef and ExchangeTimestamp
int orderRef = extractIntField(line, "OrderRef:");
long long exchangeTs = extractLongField(line, "ExchangeTimestamp:");
string instrumentID = extractStringField(line, "InstrumentID:");
if (orderRef != -1 && exchangeTs != -1) {
auto key = make_pair(machineID, orderRef);
if (orders.find(key) == orders.end()) {
Order order;
order.machineID = machineID;
order.orderRef = orderRef;
order.sendTimeMs = 0;
order.instrumentID = instrumentID;
order.exchangeTimestamp = exchangeTs;
order.hasNotify = true;
orders[key] = order;
} else {
orders[key].exchangeTimestamp = exchangeTs;
orders[key].hasNotify = true;
}
}
}
// Extract time in milliseconds from log timestamp
long long extractTimeMs(const string& line) {
size_t startPos = line.find('[');
if (startPos == string::npos) return -1;
startPos = line.find('[', startPos + 1);
if (startPos == string::npos) return -1;
size_t endPos = line.find(']', startPos);
if (endPos == string::npos) return -1;
string timeStr = line.substr(startPos + 1, endPos - startPos - 1);
// Format: MM/DD HH:MM:SS.nnnnnnnnn
int month, day, hour, minute, second, nanos;
sscanf(timeStr.c_str(), "%d/%d %d:%d:%d.%d", &month, &day, &hour, &minute,
&second, &nanos);
long long ms =
(hour * 3600 + minute * 60 + second) * 1000 + nanos / 1000000;
return ms;
}
// Extract integer field
int extractIntField(const string& line, const string& fieldName) {
size_t pos = line.find(fieldName);
if (pos == string::npos) return -1;
pos += fieldName.length();
size_t endPos = line.find(' ', pos);
if (endPos == string::npos) endPos = line.length();
string value = line.substr(pos, endPos - pos);
try {
return stoi(value);
} catch (...) {
return -1;
}
}
// Extract long field
long long extractLongField(const string& line, const string& fieldName) {
size_t pos = line.find(fieldName);
if (pos == string::npos) return -1;
pos += fieldName.length();
size_t endPos = line.find(' ', pos);
if (endPos == string::npos) endPos = line.length();
string value = line.substr(pos, endPos - pos);
try {
return stoll(value);
} catch (...) {
return -1;
}
}
// Extract string field
string extractStringField(const string& line, const string& fieldName) {
size_t pos = line.find(fieldName);
if (pos == string::npos) return "";
pos += fieldName.length();
size_t endPos = line.find(' ', pos);
if (endPos == string::npos) endPos = line.length();
return line.substr(pos, endPos - pos);
}
// Analyze and output statistics
void analyzeAndOutput() {
// Count complete orders per machine
map<string, int> completeOrdersPerMachine;
vector<Order> completeOrders;
for (auto& orderPair : orders) {
Order& order = orderPair.second;
if (order.hasNotify && order.sendTimeMs > 0) {
completeOrdersPerMachine[order.machineID]++;
completeOrders.push_back(order);
}
}
// Output 1: Order count per server
cout << "=== Server Order Count ===" << endl;
for (auto& machPair : completeOrdersPerMachine) {
cout << machPair.first << ": " << machPair.second << " orders" << endl;
}
cout << endl;
// Group orders by instrument first
map<string, vector<Order>> ordersByInstrument;
for (auto& order : completeOrders) {
ordersByInstrument[order.instrumentID].push_back(order);
}
// Find orders at the same time point (within 200ms, different servers)
vector<vector<Order>> sameTimePointGroups;
for (auto& instPair : ordersByInstrument) {
vector<Order>& instOrders = instPair.second;
// Sort by sendTimeMs
sort(instOrders.begin(), instOrders.end(),
[](const Order& a, const Order& b) {
return a.sendTimeMs < b.sendTimeMs;
});
// Use sliding window to find orders within 200ms
for (size_t i = 0; i < instOrders.size(); ++i) {
vector<Order> timeGroup;
timeGroup.push_back(instOrders[i]);
// Find all orders within 200ms window
for (size_t j = i + 1; j < instOrders.size(); ++j) {
if (instOrders[j].sendTimeMs - instOrders[i].sendTimeMs <= 200) {
timeGroup.push_back(instOrders[j]);
} else {
break;
}
}
// Check if there are orders from different machines
if (timeGroup.size() > 1) {
set<string> machines;
for (auto& order : timeGroup) {
machines.insert(order.machineID);
}
// Only keep groups with orders from different machines
if (machines.size() > 1) {
sameTimePointGroups.push_back(timeGroup);
}
}
}
}
// Count unique orders in same time point groups (avoid duplicate counting)
set<pair<string, int>> countedOrders; // (machineID, orderRef)
int sameTimePointCount = 0;
for (auto& group : sameTimePointGroups) {
for (auto& order : group) {
auto key = make_pair(order.machineID, order.orderRef);
if (countedOrders.find(key) == countedOrders.end()) {
countedOrders.insert(key);
sameTimePointCount++;
}
}
}
// Output 2: Same time point orders count
cout << "=== Same Time Point Orders ===" << endl;
cout << "Same time point (within 200ms, same instrument, different "
"servers) order count: "
<< sameTimePointCount << endl;
cout << endl;
// Output 3: Order arrival ratio between servers
cout << "=== Order Arrival Ratio Between Servers ===" << endl;
outputArrivalRatio(sameTimePointGroups);
}
// Output arrival order ratio
void outputArrivalRatio(const vector<vector<Order>>& groups) {
// (machine1, machine2) -> (machine1_first_count, machine2_first_count)
map<pair<string, string>, pair<int, int>> ratios;
for (auto& group : groups) {
if (group.size() < 2) continue;
set<string> machines;
for (auto& order : group) {
machines.insert(order.machineID);
}
if (machines.size() < 2) continue;
// Sort by exchange timestamp to find arrival order
vector<Order> sortedOrders = group;
sort(sortedOrders.begin(), sortedOrders.end(),
[](const Order& a, const Order& b) {
return a.exchangeTimestamp < b.exchangeTimestamp;
});
// For each pair of orders from different machines, count precedence
for (size_t i = 0; i < sortedOrders.size(); ++i) {
for (size_t j = i + 1; j < sortedOrders.size(); ++j) {
string m1 = sortedOrders[i].machineID;
string m2 = sortedOrders[j].machineID;
if (m1 != m2) {
// sortedOrders[i] arrives first (smaller ExchangeTimestamp)
// Normalize machine order (lexicographically smaller first)
string machineA = (m1 < m2) ? m1 : m2;
string machineB = (m1 < m2) ? m2 : m1;
auto key = make_pair(machineA, machineB);
// Check which machine actually arrived first
if (sortedOrders[i].machineID == machineA) {
ratios[key].first++; // machineA arrives first
} else {
ratios[key].second++; // machineB arrives first
}
}
}
}
}
// Output ratios
if (ratios.empty()) {
cout << "No orders from different servers at same time point." << endl;
} else {
for (auto& ratio : ratios) {
int total = ratio.second.first + ratio.second.second;
if (total > 0) {
double ratio1 = (double)ratio.second.first / total * 100.0;
double ratio2 = (double)ratio.second.second / total * 100.0;
cout << ratio.first.first << " vs " << ratio.first.second << ": "
<< ratio.first.first << " " << fixed << setprecision(2) << ratio1
<< "%, " << ratio.first.second << " " << ratio2 << "%" << endl;
}
}
}
}
};
int main(int argc, char* argv[]) {
LogAnalyzer analyzer;
namespace fs = std::filesystem;
vector<string> logFiles;
// If no arguments provided, use default "LogFile" directory
if (argc < 2) {
string defaultDir = "LogFile";
if (fs::exists(defaultDir) && fs::is_directory(defaultDir)) {
cout << "Reading log files from directory: " << defaultDir << endl;
vector<string> files = getLogFilesFromDir(defaultDir);
logFiles.insert(logFiles.end(), files.begin(), files.end());
} else {
// Try current directory
cout << "Reading log files from current directory..." << endl;
vector<string> files = getLogFilesFromDir(".");
logFiles.insert(logFiles.end(), files.begin(), files.end());
}
} else {
// Process command line arguments
for (int i = 1; i < argc; i++) {
string path = argv[i];
if (fs::exists(path)) {
if (fs::is_directory(path)) {
// It's a directory, scan for .log files
cout << "Reading log files from directory: " << path << endl;
vector<string> files = getLogFilesFromDir(path);
logFiles.insert(logFiles.end(), files.begin(), files.end());
} else if (fs::is_regular_file(path)) {
// It's a file, check if it's a .log file
if (path.length() >= 4 && path.substr(path.length() - 4) == ".log") {
logFiles.push_back(path);
} else {
cerr << "Warning: " << path << " is not a .log file, skipping."
<< endl;
}
}
} else {
cerr << "Warning: " << path << " does not exist, skipping." << endl;
}
}
}
if (logFiles.empty()) {
cerr << "Error: No log files found!" << endl;
cerr << "Usage: " << argv[0]
<< " [directory_or_file1] [directory_or_file2] ..." << endl;
cerr << "If no arguments provided, will search in 'LogFile' directory or "
"current directory."
<< endl;
return 1;
}
cout << "Found " << logFiles.size() << " log file(s) to process." << endl;
cout << endl;
// Parse all log files
for (const auto& logFile : logFiles) {
analyzer.parseLogFile(logFile);
}
// Analyze and output results
analyzer.analyzeAndOutput();
return 0;
}
