Line data Source code
1 : /**
2 : * @file client_app.cpp
3 : * @brief 客户端 FIX Application 实现
4 : */
5 :
6 : #include "client_app.hpp"
7 : #include "fix/fix_tags.hpp"
8 : #include "base/logger.hpp"
9 : #include <sstream>
10 : #include <iomanip>
11 : #include <chrono>
12 :
13 : namespace fix40::client {
14 :
15 2 : ClientApp::ClientApp(std::shared_ptr<ClientState> state, const std::string& userId)
16 2 : : state_(std::move(state))
17 4 : , userId_(userId) {
18 2 : clOrdIdPrefixMs_ = std::chrono::duration_cast<std::chrono::milliseconds>(
19 4 : std::chrono::system_clock::now().time_since_epoch()).count();
20 2 : }
21 :
22 : // ============================================================================
23 : // Application 接口实现
24 : // ============================================================================
25 :
26 0 : void ClientApp::onLogon(const SessionID& sessionID) {
27 0 : LOG() << "[ClientApp] Logged on: " << sessionID.to_string();
28 0 : state_->setConnectionState(ConnectionState::LOGGED_IN);
29 0 : state_->setUserId(userId_);
30 0 : state_->addMessage("登录成功");
31 :
32 : // 登录后自动查询资金和持仓
33 0 : queryBalance();
34 0 : queryPositions();
35 0 : queryOrderHistory();
36 0 : }
37 :
38 0 : void ClientApp::onLogout(const SessionID& sessionID) {
39 0 : LOG() << "[ClientApp] Logged out: " << sessionID.to_string();
40 0 : state_->setConnectionState(ConnectionState::DISCONNECTED);
41 0 : state_->addMessage("已登出");
42 0 : }
43 :
44 1 : void ClientApp::fromApp(const FixMessage& msg, const SessionID& sessionID) {
45 1 : std::string msgType = msg.get_string(tags::MsgType);
46 1 : LOG() << "[ClientApp] Received MsgType=" << msgType << " from " << sessionID.to_string();
47 :
48 1 : if (msgType == "8") {
49 0 : handleExecutionReport(msg);
50 1 : } else if (msgType == "U2") {
51 0 : handleBalanceResponse(msg);
52 1 : } else if (msgType == "U4") {
53 0 : handlePositionResponse(msg);
54 1 : } else if (msgType == "U5") {
55 1 : handleAccountUpdate(msg);
56 0 : } else if (msgType == "U6") {
57 0 : handlePositionUpdate(msg);
58 0 : } else if (msgType == "U8") {
59 0 : handleInstrumentSearchResponse(msg);
60 0 : } else if (msgType == "U10") {
61 0 : handleOrderHistoryResponse(msg);
62 0 : } else if (msgType == "j") {
63 : // BusinessMessageReject
64 0 : std::string text = msg.has(tags::Text) ? msg.get_string(tags::Text) : "Unknown error";
65 0 : state_->setLastError(text);
66 0 : state_->addMessage("业务拒绝: " + text);
67 0 : } else {
68 0 : LOG() << "[ClientApp] Unknown message type: " << msgType;
69 : }
70 1 : }
71 :
72 2 : void ClientApp::toApp(FixMessage& msg, const SessionID& sessionID) {
73 2 : std::string msgType = msg.get_string(tags::MsgType);
74 2 : LOG() << "[ClientApp] Sending MsgType=" << msgType << " via " << sessionID.to_string();
75 2 : }
76 :
77 1 : void ClientApp::setSession(std::shared_ptr<Session> session) {
78 1 : session_ = session;
79 1 : }
80 :
81 : // ============================================================================
82 : // 业务操作
83 : // ============================================================================
84 :
85 2 : std::string ClientApp::sendNewOrder(const std::string& symbol, const std::string& side,
86 : int64_t qty, double price, const std::string& ordType) {
87 2 : auto session = session_.lock();
88 2 : if (!session) {
89 0 : state_->setLastError("未连接");
90 0 : return "";
91 : }
92 :
93 2 : std::string clOrdID = generateClOrdID();
94 :
95 2 : FixMessage msg;
96 2 : msg.set(tags::MsgType, "D");
97 2 : msg.set(tags::ClOrdID, clOrdID);
98 2 : msg.set(tags::HandlInst, "1"); // Automated execution
99 2 : msg.set(tags::Symbol, symbol);
100 2 : msg.set(tags::Side, side);
101 2 : msg.set(tags::OrderQty, static_cast<int>(qty));
102 2 : msg.set(tags::OrdType, ordType);
103 :
104 2 : if (ordType == "2") { // Limit order
105 2 : std::ostringstream oss;
106 2 : oss << std::fixed << std::setprecision(2) << price;
107 2 : msg.set(tags::Price, oss.str());
108 2 : }
109 :
110 2 : msg.set(tags::TimeInForce, "0"); // Day
111 :
112 : // TransactTime
113 2 : auto now = std::chrono::system_clock::now();
114 2 : auto time = std::chrono::system_clock::to_time_t(now);
115 2 : std::ostringstream timeOss;
116 2 : timeOss << std::put_time(std::gmtime(&time), "%Y%m%d-%H:%M:%S");
117 2 : msg.set(tags::TransactTime, timeOss.str());
118 :
119 2 : session->send_app_message(msg);
120 :
121 : // 添加到本地订单列表
122 2 : OrderInfo order;
123 2 : order.clOrdID = clOrdID;
124 2 : order.symbol = symbol;
125 2 : order.side = (side == "1") ? "BUY" : "SELL";
126 2 : order.price = price;
127 2 : order.orderQty = qty;
128 2 : order.state = OrderState::PENDING_NEW;
129 2 : state_->addOrder(order);
130 :
131 2 : state_->addMessage("下单: " + symbol + " " + order.side + " " + std::to_string(qty) + "@" + std::to_string(price));
132 :
133 2 : return clOrdID;
134 2 : }
135 :
136 0 : void ClientApp::sendCancelOrder(const std::string& origClOrdID,
137 : const std::string& symbol, const std::string& side) {
138 0 : auto session = session_.lock();
139 0 : if (!session) {
140 0 : state_->setLastError("未连接");
141 0 : return;
142 : }
143 :
144 0 : std::string clOrdID = generateClOrdID();
145 :
146 0 : FixMessage msg;
147 0 : msg.set(tags::MsgType, "F");
148 0 : msg.set(tags::ClOrdID, clOrdID);
149 0 : msg.set(tags::OrigClOrdID, origClOrdID);
150 0 : msg.set(tags::Symbol, symbol);
151 0 : msg.set(tags::Side, side);
152 0 : msg.set(tags::CxlType, "F"); // Full cancel
153 :
154 0 : auto now = std::chrono::system_clock::now();
155 0 : auto time = std::chrono::system_clock::to_time_t(now);
156 0 : std::ostringstream timeOss;
157 0 : timeOss << std::put_time(std::gmtime(&time), "%Y%m%d-%H:%M:%S");
158 0 : msg.set(tags::TransactTime, timeOss.str());
159 :
160 0 : session->send_app_message(msg);
161 0 : state_->addMessage("撤单: " + origClOrdID);
162 0 : }
163 :
164 0 : void ClientApp::queryBalance() {
165 0 : auto session = session_.lock();
166 0 : if (!session) return;
167 :
168 0 : FixMessage msg;
169 0 : msg.set(tags::MsgType, "U1");
170 0 : msg.set(tags::RequestID, std::to_string(requestIdCounter_++));
171 :
172 0 : session->send_app_message(msg);
173 0 : }
174 :
175 0 : void ClientApp::queryPositions() {
176 0 : auto session = session_.lock();
177 0 : if (!session) return;
178 :
179 0 : FixMessage msg;
180 0 : msg.set(tags::MsgType, "U3");
181 0 : msg.set(tags::RequestID, std::to_string(requestIdCounter_++));
182 :
183 0 : session->send_app_message(msg);
184 0 : }
185 :
186 0 : void ClientApp::queryOrderHistory() {
187 0 : auto session = session_.lock();
188 0 : if (!session) return;
189 :
190 0 : FixMessage msg;
191 0 : msg.set(tags::MsgType, "U9");
192 0 : msg.set(tags::RequestID, std::to_string(requestIdCounter_++));
193 0 : session->send_app_message(msg);
194 0 : }
195 :
196 0 : void ClientApp::searchInstruments(const std::string& pattern, int maxResults) {
197 0 : auto session = session_.lock();
198 0 : if (!session) return;
199 :
200 0 : FixMessage msg;
201 0 : msg.set(tags::MsgType, "U7");
202 0 : msg.set(tags::RequestID, std::to_string(requestIdCounter_++));
203 0 : msg.set(tags::SearchPattern, pattern);
204 0 : msg.set(tags::MaxResults, maxResults);
205 :
206 0 : session->send_app_message(msg);
207 0 : }
208 :
209 : // ============================================================================
210 : // 消息处理
211 : // ============================================================================
212 :
213 0 : void ClientApp::handleExecutionReport(const FixMessage& msg) {
214 0 : OrderInfo order;
215 0 : order.clOrdID = msg.get_string(tags::ClOrdID);
216 :
217 0 : if (msg.has(tags::OrderID)) {
218 0 : order.orderId = msg.get_string(tags::OrderID);
219 : }
220 0 : if (msg.has(tags::Symbol)) {
221 0 : order.symbol = msg.get_string(tags::Symbol);
222 : }
223 0 : if (msg.has(tags::Side)) {
224 0 : order.side = (msg.get_string(tags::Side) == "1") ? "BUY" : "SELL";
225 : }
226 0 : if (msg.has(tags::Price)) {
227 : try {
228 0 : order.price = std::stod(msg.get_string(tags::Price));
229 0 : } catch (...) { order.price = 0.0; }
230 : }
231 0 : if (msg.has(tags::OrderQty)) {
232 : try {
233 0 : order.orderQty = std::stoll(msg.get_string(tags::OrderQty));
234 0 : } catch (...) { order.orderQty = 0; }
235 : }
236 0 : if (msg.has(tags::CumQty)) {
237 : try {
238 0 : order.filledQty = std::stoll(msg.get_string(tags::CumQty));
239 0 : } catch (...) { order.filledQty = 0; }
240 : }
241 0 : if (msg.has(tags::AvgPx)) {
242 : try {
243 0 : order.avgPx = std::stod(msg.get_string(tags::AvgPx));
244 0 : } catch (...) { order.avgPx = 0.0; }
245 : }
246 0 : if (msg.has(tags::Text)) {
247 0 : order.text = msg.get_string(tags::Text);
248 : }
249 :
250 : // 解析订单状态
251 0 : if (msg.has(tags::OrdStatus)) {
252 0 : std::string status = msg.get_string(tags::OrdStatus);
253 0 : if (status == "0") order.state = OrderState::NEW;
254 0 : else if (status == "1") order.state = OrderState::PARTIALLY_FILLED;
255 0 : else if (status == "2") order.state = OrderState::FILLED;
256 0 : else if (status == "4") order.state = OrderState::CANCELED;
257 0 : else if (status == "8") order.state = OrderState::REJECTED;
258 0 : }
259 :
260 0 : state_->updateOrder(order.clOrdID, order);
261 :
262 : // 生成消息
263 0 : std::string stateStr;
264 0 : switch (order.state) {
265 0 : case OrderState::NEW: stateStr = "已确认"; break;
266 0 : case OrderState::PARTIALLY_FILLED: stateStr = "部分成交"; break;
267 0 : case OrderState::FILLED: stateStr = "全部成交"; break;
268 0 : case OrderState::CANCELED: stateStr = "已撤销"; break;
269 0 : case OrderState::REJECTED: stateStr = "已拒绝"; break;
270 0 : default: stateStr = "未知"; break;
271 : }
272 :
273 : // 生成消息(拒绝时显示原因)
274 0 : if (order.state == OrderState::REJECTED && !order.text.empty()) {
275 0 : state_->addMessage("订单 " + order.clOrdID + " " + stateStr + ": " + order.text);
276 0 : state_->setLastError(order.text);
277 : } else {
278 0 : state_->addMessage("订单 " + order.clOrdID + " " + stateStr);
279 : }
280 :
281 : // 成交后刷新资金和持仓
282 0 : if (order.state == OrderState::FILLED || order.state == OrderState::PARTIALLY_FILLED) {
283 0 : queryBalance();
284 0 : queryPositions();
285 : }
286 0 : }
287 :
288 0 : void ClientApp::handleBalanceResponse(const FixMessage& msg) {
289 0 : AccountInfo info;
290 :
291 0 : auto safeStod = [](const std::string& s) -> double {
292 0 : try { return std::stod(s); } catch (...) { return 0.0; }
293 : };
294 :
295 0 : if (msg.has(tags::Balance)) {
296 0 : info.balance = safeStod(msg.get_string(tags::Balance));
297 : }
298 0 : if (msg.has(tags::Available)) {
299 0 : info.available = safeStod(msg.get_string(tags::Available));
300 : }
301 0 : if (msg.has(tags::FrozenMargin)) {
302 0 : info.frozenMargin = safeStod(msg.get_string(tags::FrozenMargin));
303 : }
304 0 : if (msg.has(tags::UsedMargin)) {
305 0 : info.usedMargin = safeStod(msg.get_string(tags::UsedMargin));
306 : }
307 0 : if (msg.has(tags::PositionProfit)) {
308 0 : info.positionProfit = safeStod(msg.get_string(tags::PositionProfit));
309 : }
310 0 : if (msg.has(tags::CloseProfit)) {
311 0 : info.closeProfit = safeStod(msg.get_string(tags::CloseProfit));
312 : }
313 0 : if (msg.has(tags::DynamicEquity)) {
314 0 : info.dynamicEquity = safeStod(msg.get_string(tags::DynamicEquity));
315 : }
316 0 : if (msg.has(tags::RiskRatio)) {
317 0 : info.riskRatio = safeStod(msg.get_string(tags::RiskRatio));
318 : }
319 :
320 0 : state_->updateAccount(info);
321 0 : }
322 :
323 0 : void ClientApp::handlePositionResponse(const FixMessage& msg) {
324 : // 简化处理:从 Text 字段解析持仓
325 : // 格式: "IF2601:L10@4000.00,S5@4100.00;IC2601:L20@5000.00,S0@0.00;"
326 0 : if (!msg.has(tags::Text)) {
327 0 : state_->clearPositions();
328 0 : return;
329 : }
330 :
331 0 : std::string text = msg.get_string(tags::Text);
332 0 : std::vector<PositionInfo> positions;
333 :
334 0 : std::istringstream iss(text);
335 0 : std::string item;
336 0 : while (std::getline(iss, item, ';')) {
337 0 : if (item.empty()) continue;
338 :
339 0 : auto colonPos = item.find(':');
340 0 : if (colonPos == std::string::npos) continue;
341 :
342 0 : PositionInfo pos;
343 0 : pos.instrumentId = item.substr(0, colonPos);
344 :
345 0 : std::string rest = item.substr(colonPos + 1);
346 : // 解析 L10@4000.00,S5@4100.00
347 0 : std::istringstream restIss(rest);
348 0 : std::string part;
349 0 : while (std::getline(restIss, part, ',')) {
350 0 : if (part.empty()) continue;
351 0 : char dir = part[0];
352 0 : auto atPos = part.find('@');
353 0 : if (atPos == std::string::npos) continue;
354 :
355 : try {
356 0 : int64_t qty = std::stoll(part.substr(1, atPos - 1));
357 0 : double price = std::stod(part.substr(atPos + 1));
358 :
359 0 : if (dir == 'L') {
360 0 : pos.longPosition = qty;
361 0 : pos.longAvgPrice = price;
362 0 : } else if (dir == 'S') {
363 0 : pos.shortPosition = qty;
364 0 : pos.shortAvgPrice = price;
365 : }
366 0 : } catch (...) {
367 : // 解析失败,跳过该字段
368 0 : }
369 : }
370 :
371 0 : if (pos.longPosition > 0 || pos.shortPosition > 0) {
372 0 : positions.push_back(pos);
373 : }
374 0 : }
375 :
376 0 : state_->setPositions(positions);
377 0 : }
378 :
379 1 : void ClientApp::handleAccountUpdate(const FixMessage& msg) {
380 : // 与 handleBalanceResponse 基本相同,但不触发额外的查询。
381 : // 注意:U5 作为推送消息,可能只包含部分字段;因此这里以当前状态为基准做“合并更新”,
382 : // 避免缺失字段被默认值(例如 closeProfit=0)覆盖。
383 1 : AccountInfo info = state_->getAccount();
384 :
385 1 : auto safeStod = [](const std::string& s) -> double {
386 1 : try { return std::stod(s); } catch (...) { return 0.0; }
387 : };
388 :
389 1 : if (msg.has(tags::Balance)) {
390 1 : info.balance = safeStod(msg.get_string(tags::Balance));
391 : }
392 1 : if (msg.has(tags::Available)) {
393 0 : info.available = safeStod(msg.get_string(tags::Available));
394 : }
395 1 : if (msg.has(tags::FrozenMargin)) {
396 0 : info.frozenMargin = safeStod(msg.get_string(tags::FrozenMargin));
397 : }
398 1 : if (msg.has(tags::UsedMargin)) {
399 0 : info.usedMargin = safeStod(msg.get_string(tags::UsedMargin));
400 : }
401 1 : if (msg.has(tags::PositionProfit)) {
402 0 : info.positionProfit = safeStod(msg.get_string(tags::PositionProfit));
403 : }
404 1 : if (msg.has(tags::CloseProfit)) {
405 0 : info.closeProfit = safeStod(msg.get_string(tags::CloseProfit));
406 : }
407 1 : if (msg.has(tags::DynamicEquity)) {
408 0 : info.dynamicEquity = safeStod(msg.get_string(tags::DynamicEquity));
409 : }
410 1 : if (msg.has(tags::RiskRatio)) {
411 0 : info.riskRatio = safeStod(msg.get_string(tags::RiskRatio));
412 : }
413 :
414 1 : state_->updateAccount(info);
415 1 : }
416 :
417 0 : void ClientApp::handlePositionUpdate(const FixMessage& msg) {
418 0 : PositionInfo pos;
419 :
420 0 : auto safeStod = [](const std::string& s) -> double {
421 0 : try { return std::stod(s); } catch (...) { return 0.0; }
422 : };
423 :
424 0 : if (msg.has(tags::InstrumentID)) {
425 0 : pos.instrumentId = msg.get_string(tags::InstrumentID);
426 : }
427 0 : pos.quantitiesValid = msg.has(tags::LongPosition) || msg.has(tags::ShortPosition);
428 0 : if (msg.has(tags::LongPosition)) {
429 0 : pos.longPosition = msg.get_int(tags::LongPosition);
430 : }
431 0 : if (msg.has(tags::LongAvgPrice)) {
432 0 : pos.longAvgPrice = safeStod(msg.get_string(tags::LongAvgPrice));
433 : }
434 0 : if (msg.has(tags::ShortPosition)) {
435 0 : pos.shortPosition = msg.get_int(tags::ShortPosition);
436 : }
437 0 : if (msg.has(tags::ShortAvgPrice)) {
438 0 : pos.shortAvgPrice = safeStod(msg.get_string(tags::ShortAvgPrice));
439 : }
440 0 : if (msg.has(tags::PositionProfit)) {
441 0 : pos.profit = safeStod(msg.get_string(tags::PositionProfit));
442 : }
443 :
444 0 : state_->updatePosition(pos);
445 0 : }
446 :
447 0 : void ClientApp::handleInstrumentSearchResponse(const FixMessage& msg) {
448 0 : std::vector<std::string> results;
449 :
450 0 : if (msg.has(tags::InstrumentList)) {
451 0 : std::string list = msg.get_string(tags::InstrumentList);
452 0 : std::istringstream iss(list);
453 0 : std::string item;
454 0 : while (std::getline(iss, item, ',')) {
455 0 : if (!item.empty()) {
456 0 : results.push_back(item);
457 : }
458 : }
459 0 : }
460 :
461 0 : state_->setSearchResults(results);
462 0 : }
463 :
464 0 : void ClientApp::handleOrderHistoryResponse(const FixMessage& msg) {
465 : // Text 字段包含序列化订单列表(与 ClientState::saveOrders 的格式对齐):
466 : // clOrdID|orderId|symbol|side|price|orderQty|filledQty|avgPx|state|text|updateTime
467 0 : if (!msg.has(tags::Text)) {
468 0 : state_->clearOrders();
469 0 : state_->setLastError("订单历史响应格式无效:缺少 Text 字段");
470 0 : state_->addMessage("订单历史响应格式无效");
471 0 : return;
472 : }
473 :
474 0 : std::string text = msg.get_string(tags::Text);
475 0 : if (text.empty()) {
476 0 : state_->clearOrders();
477 0 : state_->addMessage("订单历史为空");
478 0 : return;
479 : }
480 :
481 0 : std::vector<OrderInfo> orders;
482 0 : std::istringstream iss(text);
483 0 : std::string line;
484 :
485 0 : while (std::getline(iss, line)) {
486 0 : if (line.empty()) continue;
487 :
488 0 : std::vector<std::string> fields;
489 0 : std::istringstream lineIss(line);
490 0 : std::string field;
491 0 : while (std::getline(lineIss, field, '|')) {
492 0 : fields.push_back(field);
493 : }
494 :
495 : // 协议/格式兼容策略:
496 : // - 至少需要到 state 字段(第 9 个字段)才能构造 OrderInfo;
497 : // - 如未来服务端增加字段,客户端忽略多余字段以保持兼容性。
498 0 : if (fields.size() < 9) {
499 0 : continue;
500 : }
501 :
502 0 : OrderInfo order;
503 0 : order.clOrdID = fields[0];
504 0 : order.orderId = fields[1];
505 0 : order.symbol = fields[2];
506 0 : order.side = fields[3];
507 :
508 0 : auto safeStod = [](const std::string& s) -> double {
509 0 : try { return std::stod(s); } catch (...) { return 0.0; }
510 : };
511 0 : auto safeStoll = [](const std::string& s) -> int64_t {
512 0 : try { return std::stoll(s); } catch (...) { return 0; }
513 : };
514 :
515 0 : order.price = safeStod(fields[4]);
516 0 : order.orderQty = safeStoll(fields[5]);
517 0 : order.filledQty = safeStoll(fields[6]);
518 0 : order.avgPx = safeStod(fields[7]);
519 :
520 0 : int stateVal = 0;
521 : try {
522 0 : stateVal = std::stoi(fields[8]);
523 0 : } catch (...) {
524 0 : stateVal = 0;
525 0 : }
526 0 : if (stateVal < 0 || stateVal > static_cast<int>(OrderState::REJECTED)) {
527 0 : stateVal = 0;
528 : }
529 0 : order.state = static_cast<OrderState>(stateVal);
530 :
531 0 : if (fields.size() >= 10) {
532 0 : order.text = fields[9];
533 : }
534 0 : if (fields.size() >= 11) {
535 0 : order.updateTime = fields[10];
536 : }
537 :
538 0 : if (!order.clOrdID.empty()) {
539 0 : orders.push_back(std::move(order));
540 : }
541 0 : }
542 :
543 0 : state_->setOrders(orders);
544 0 : state_->addMessage("订单历史已刷新 (" + std::to_string(orders.size()) + ")");
545 0 : }
546 :
547 2 : std::string ClientApp::generateClOrdID() {
548 2 : std::ostringstream oss;
549 : // ClOrdID 必须唯一:如果仅用自增计数器,客户端重启后计数器会从 1 重新开始,
550 : // 会导致服务端/客户端用相同 ClOrdID 覆盖历史订单(orders_ map 也会覆盖)。
551 : // 这里加入“本次运行启动时间(ms)”前缀,并使用 fetch_add 确保在 C++17 下行为明确。
552 2 : const uint64_t seq = orderIdCounter_.fetch_add(1, std::memory_order_relaxed);
553 2 : oss << userId_ << "-" << clOrdIdPrefixMs_ << "-" << std::setfill('0') << std::setw(6) << seq;
554 4 : return oss.str();
555 2 : }
556 :
557 : } // namespace fix40::client
|