diff --git a/DataService/src/AuthService.cpp b/DataService/src/AuthService.cpp index 3e7c470..7ae408e 100644 --- a/DataService/src/AuthService.cpp +++ b/DataService/src/AuthService.cpp @@ -130,7 +130,7 @@ bool AuthService::authenticate( std::shared_ptrwrite(logon.get()); - if (!ret) { + if ( ret != eprosima::fastdds::dds::RETCODE_OK ) { LOG4CXX_ERROR(logger, "Error Publishing to DDS :" << ss_logon.str()); } @@ -146,17 +146,18 @@ bool AuthService::authenticate( std::shared_ptrDATS_Destination()); logout.DATS_Destination(logon->DATS_Source()); + logout.DATS_SourceUser("AUTH"); logout.DATS_DestinationUser(logon->RawData()); logout.Text(textOut); std::stringstream ss_logout; LogoutLogger::log(ss_logout, logout); - LOG4CXX_INFO(logger, "Auth Service Logout : %s\n" << ss_logout.str()); + LOG4CXX_INFO(logger, "Auth Service Logout : " << ss_logout.str()); int ret = m_logout_dw->write(&logout); - if (!ret) { + if ( ret != eprosima::fastdds::dds::RETCODE_OK ) { LOG4CXX_ERROR(logger, "Logout write returned error : " << ret ); } } diff --git a/DataService/src/MarketDataService.cpp b/DataService/src/MarketDataService.cpp index 6a7f381..83d5363 100644 --- a/DataService/src/MarketDataService.cpp +++ b/DataService/src/MarketDataService.cpp @@ -182,7 +182,7 @@ bool MarketDataService::processMarketDataRequest( const MarketDataRequestPtr& ma std::cout << "Publishing Full Market Data Snapshot : " << marketDataSnapshotFullRefresh.Symbol() << std::endl; int ret = _market_data_shapshot_full_refresh_dw->write(&marketDataSnapshotFullRefresh); - if (!ret) { + if ( ret != eprosima::fastdds::dds::RETCODE_OK ) { LOG4CXX_ERROR(logger, "Market Data Snapshot Data Write returned : " << ret ); } diff --git a/DataService/src/OrderMassStatusRequestService.cpp b/DataService/src/OrderMassStatusRequestService.cpp index ce57941..d5170d5 100644 --- a/DataService/src/OrderMassStatusRequestService.cpp +++ b/DataService/src/OrderMassStatusRequestService.cpp @@ -148,7 +148,7 @@ bool OrderMassStatusRequestService::processMassOrderStatusServiceRequest( OrderM int ret = _execution_report_dw->write( execReport.get() ); - if (!ret) { + if ( ret != eprosima::fastdds::dds::RETCODE_OK ) { LOG4CXX_ERROR(logger, "MassOrderStatusRequestDataReader/Execution Report write returned :" << ret); } diff --git a/DataService/src/RefDataService.cpp b/DataService/src/RefDataService.cpp index 02fecf1..3fe9c81 100644 --- a/DataService/src/RefDataService.cpp +++ b/DataService/src/RefDataService.cpp @@ -204,7 +204,7 @@ bool RefDataService::processRefDataRequest( const SecurityListRequestPtr& securi int ret = _security_list_dw->write(&securityList); - if (!ret) { + if ( ret != eprosima::fastdds::dds::RETCODE_OK ) { LOG4CXX_ERROR(logger, "Security List write returned :" << ret); } diff --git a/MatchingEngine/src/Market.cpp b/MatchingEngine/src/Market.cpp index c64d1d5..2db313e 100644 --- a/MatchingEngine/src/Market.cpp +++ b/MatchingEngine/src/Market.cpp @@ -135,7 +135,7 @@ void Market::publishSecurityListRequest( eprosima::fastdds::dds::DataWriter* dwr auto ret = dwr->write(&securityListRequest); - if (!ret) { + if ( ret != eprosima::fastdds::dds::RETCODE_OK ) { LOG4CXX_ERROR(logger, "SecurityListRequest write returned : " << ret); } } @@ -164,9 +164,9 @@ void Market::publishMarketDataRequest() LoggerHelper::log_info(logger,marketDataRequest, "MarketDataRequest"); - bool ret = dataWriterContainerPtr_->market_data_request_dw->write(&marketDataRequest); + auto ret = dataWriterContainerPtr_->market_data_request_dw->write(&marketDataRequest); - if (!ret) + if ( ret != eprosima::fastdds::dds::RETCODE_OK ) { LOG4CXX_ERROR(logger, "MarketDataRequest write returned : " << ret); } @@ -292,9 +292,9 @@ void Market::publishExecutionReport(DistributedATS_ExecutionReport::ExecutionRep LoggerHelper::log_debug(logger, executionReport, "ExecutionReport"); - bool ret = dataWriterContainerPtr_->execution_report_dw->write(&executionReport); + auto ret = dataWriterContainerPtr_->execution_report_dw->write(&executionReport); - if (!ret) + if ( ret != eprosima::fastdds::dds::RETCODE_OK ) { LOG4CXX_ERROR(logger, "Execution Report write returned : " << ret); } @@ -305,9 +305,9 @@ void Market::publishOrderMassCancelReport(DistributedATS_OrderMassCancelReport:: LoggerHelper::log_debug(logger, orderMassCancelReport, "OrderMassCancelReport"); - bool ret = dataWriterContainerPtr_->order_mass_cancel_report_dw->write(&orderMassCancelReport); + auto ret = dataWriterContainerPtr_->order_mass_cancel_report_dw->write(&orderMassCancelReport); - if (!ret) + if ( ret != eprosima::fastdds::dds::RETCODE_OK ) { LOG4CXX_ERROR(logger, "Order Mass Cancel Report write returned: " << ret); } @@ -320,9 +320,9 @@ void Market::publishOrderCancelReject( LoggerHelper::log_debug(logger, orderCancelReject, "OrderCancelReject"); - bool ret = dataWriterContainerPtr_->order_cancel_reject_dw->write(&orderCancelReject); + auto ret = dataWriterContainerPtr_->order_cancel_reject_dw->write(&orderCancelReject); - if (ret) + if ( ret != eprosima::fastdds::dds::RETCODE_OK ) { LOG4CXX_ERROR(logger, "Order Cancel Reject write returned: " << ret); } diff --git a/MatchingEngine/src/Order.cpp b/MatchingEngine/src/Order.cpp index dddb87d..2da0f87 100644 --- a/MatchingEngine/src/Order.cpp +++ b/MatchingEngine/src/Order.cpp @@ -131,7 +131,7 @@ void Order::onCancelRejected(const char *reason) LoggerHelper::log_debug(logger,orderCancelReject, "OrderCancelReject"); - int ret = dataWriterContainerPtr_->order_cancel_reject_dw->write(&orderCancelReject); + auto ret = dataWriterContainerPtr_->order_cancel_reject_dw->write(&orderCancelReject); if (ret != eprosima::fastdds::dds::RETCODE_OK) { LOG4CXX_ERROR(logger, "OrderCancelReject write returned :" << ret); @@ -188,7 +188,7 @@ void Order::onReplaceRejected(const char *reason) DistributedATS_OrderCancelReject::OrderCancelReject>( logger, orderCancelReject, "OrderCancelReject"); - int ret = dataWriterContainerPtr_->order_cancel_reject_dw->write(&orderCancelReject); + auto ret = dataWriterContainerPtr_->order_cancel_reject_dw->write(&orderCancelReject); if (ret != eprosima::fastdds::dds::RETCODE_OK) { LOG4CXX_ERROR(logger, "OrdereplaceRejected write returned :" << ret); diff --git a/MatchingEngine/src/PriceDepthPublisherService.cpp b/MatchingEngine/src/PriceDepthPublisherService.cpp index db74c2a..36efd95 100644 --- a/MatchingEngine/src/PriceDepthPublisherService.cpp +++ b/MatchingEngine/src/PriceDepthPublisherService.cpp @@ -127,7 +127,7 @@ int PriceDepthPublisherService::service() MarketDataIncrementalRefreshLogger::log( ss, chunkedIncrementalMarketDataRefresh); - int ret = _market_data_incremental_refresh_dw->write( + auto ret = _market_data_incremental_refresh_dw->write( &chunkedIncrementalMarketDataRefresh); if (ret != eprosima::fastdds::dds::RETCODE_OK) { diff --git a/MiscClients/cpp_ws_reactjs/fix_ws_proxy/CMakeLists.txt b/MiscClients/cpp_ws_reactjs/fix_ws_proxy/CMakeLists.txt index 9337830..e98bebe 100644 --- a/MiscClients/cpp_ws_reactjs/fix_ws_proxy/CMakeLists.txt +++ b/MiscClients/cpp_ws_reactjs/fix_ws_proxy/CMakeLists.txt @@ -31,3 +31,5 @@ target_link_libraries(fix_ws_proxy Boost::json Boost::chrono ) + +install(TARGETS fix_ws_proxy) diff --git a/MiscClients/cpp_ws_reactjs/fix_ws_proxy/fix_application.cpp b/MiscClients/cpp_ws_reactjs/fix_ws_proxy/fix_application.cpp index 092e688..1015a62 100644 --- a/MiscClients/cpp_ws_reactjs/fix_ws_proxy/fix_application.cpp +++ b/MiscClients/cpp_ws_reactjs/fix_ws_proxy/fix_application.cpp @@ -6,61 +6,94 @@ void fix_application::toAdmin(FIX::Message& message, const FIX::SessionID& sessionID) { const FIX::Header& header = message.getHeader(); + const FIX::MsgType& msgType = FIELD_GET_REF(header, MsgType); - const FIX::MsgType& msgType = FIELD_GET_REF( header, MsgType ); + // Convert FIX message to JSON and add common fields + auto json_obj = distributed_ats::fix_json::fix_to_json(message); + json_obj["session_qualifier"] = sessionID.getSessionQualifier(); + json_obj["data_type"] = "FIX"; + json_obj["success"] = true; + auto json_str = boost::json::serialize(json_obj); - if ( msgType == FIX::MsgType_Logon ) - { + if (msgType == FIX::MsgType_Logon) { if (auto session = _ws_session.lock()) { auto ws_logon = session->get_pending_logon(); - - std::cout << std::endl << "Sending Logon : " << sessionID << "Pending Logon : " << ws_logon << std::endl; - + + std::cout << "\nSending Logon: " << sessionID + << " | Pending Logon: " << ws_logon << std::endl; + + // Set Username FIX::Username userName(sessionID.getSenderCompID()); message.setField(userName); - + + // Set Password from WebSocket logon FIX::Password password; ws_logon.getField(password); - message.setField(password); - - std::cout << "Sending Logon : " << message.toString() << std::endl; + + // Print JSON log + std::cout << "Sending Logon JSON: " << json_str << std::endl; } - } else if ( msgType == FIX::MsgType_Logout ) - { - std::cout << "Logout : " << message << std::endl; - } + } + else if (msgType == FIX::MsgType_Logout) { + std::cout << "Logout JSON: " << json_str << std::endl; + } + + // Optional: log via QuickFIX + if (auto fix_session = FIX::Session::lookupSession(sessionID)) { + if (auto log = fix_session->getLog()) { + log->onEvent(json_str); + } + } } + void fix_application::fromAdmin(const FIX::Message& message, const FIX::SessionID& sessionId) throw(FIX::FieldNotFound, FIX::IncorrectDataFormat, FIX::IncorrectTagValue, FIX::RejectLogon) { - if (auto session = _ws_session.lock()) { - - auto json_obj = distributed_ats::fix_json::fix_to_json(message); - - json_obj["session_qualifier"] = sessionId.getSessionQualifier(); - json_obj["data_type"] = "FIX"; - json_obj["success"] = true; - - auto json_str = boost::json::serialize(json_obj); - - session->send_string(json_str); - } + // Convert FIX message to JSON + auto json_obj = distributed_ats::fix_json::fix_to_json(message); + json_obj["session_qualifier"] = sessionId.getSessionQualifier(); + json_obj["data_type"] = "FIX"; + json_obj["success"] = true; + + auto json_str = boost::json::serialize(json_obj); + + // Send JSON to WebSocket session + if (auto session = _ws_session.lock()) { + session->send_string(json_str); + } + + // Log the JSON message using QuickFIX logger + if (auto fix_session = FIX::Session::lookupSession(sessionId)) { + if (auto log = fix_session->getLog()) { + log->onEvent(json_str); // Log as a general event + // If you prefer, you could also log as incoming FIX message: + // log->onIncoming(message, sessionId); + } + } } void fix_application::fromApp(const FIX::Message& message, const FIX::SessionID& sessionId) throw(FIX::FieldNotFound, FIX::IncorrectDataFormat, FIX::IncorrectTagValue, FIX::UnsupportedMessageType) { - if (auto session = _ws_session.lock()) { - - auto json_obj = distributed_ats::fix_json::fix_to_json(message); - - json_obj["session_qualifier"] = sessionId.getSessionQualifier(); - json_obj["data_type"] = "FIX"; - json_obj["success"] = true; - - auto json_str = boost::json::serialize(json_obj); - - session->send_string(json_str); - } + // Convert FIX message to JSON + auto json_obj = distributed_ats::fix_json::fix_to_json(message); + json_obj["session_qualifier"] = sessionId.getSessionQualifier(); + json_obj["data_type"] = "FIX"; + json_obj["success"] = true; + + auto json_str = boost::json::serialize(json_obj); + + // Send to your WebSocket session + if (auto session = _ws_session.lock()) { + session->send_string(json_str); + } + + // Log the JSON message using QuickFIX logger + if (auto fix_session = FIX::Session::lookupSession(sessionId)) { + if (auto log = fix_session->getLog()) { + log->onEvent(json_str); // Log as an event + // log->onIncoming(message, sessionId); + } + } } diff --git a/MiscClients/cpp_ws_reactjs/fix_ws_proxy/fix_application.h b/MiscClients/cpp_ws_reactjs/fix_ws_proxy/fix_application.h index 56dcf9b..2f794f1 100644 --- a/MiscClients/cpp_ws_reactjs/fix_ws_proxy/fix_application.h +++ b/MiscClients/cpp_ws_reactjs/fix_ws_proxy/fix_application.h @@ -12,10 +12,7 @@ class fix_application : public FIX::Application { public: - fix_application(const std::weak_ptr& ws_session) : _ws_session(ws_session) - { - //std::cout << "Pending Logon : " << _ws_logon << std::endl; - } + fix_application(const std::weak_ptr& ws_session) : _ws_session(ws_session) {} void onCreate(const FIX::SessionID& id) override { std::cout << "Created: " << id << std::endl; diff --git a/MiscClients/cpp_ws_reactjs/fix_ws_proxy/fix_ws_proxy.cpp b/MiscClients/cpp_ws_reactjs/fix_ws_proxy/fix_ws_proxy.cpp index 28f96f5..a04a3a5 100644 --- a/MiscClients/cpp_ws_reactjs/fix_ws_proxy/fix_ws_proxy.cpp +++ b/MiscClients/cpp_ws_reactjs/fix_ws_proxy/fix_ws_proxy.cpp @@ -56,9 +56,12 @@ int main(int argc, char** argv) { if (!fix_client_config.empty()) std::cout << "Using FIX client config file: " << fix_client_config << "\n"; + + auto fix_session_setttings = std::make_shared(fix_client_config); + asio::io_context ioc{1}; // number of ws threads - std::make_shared(ioc, tcp::endpoint{address, port})->run(); + std::make_shared(ioc, tcp::endpoint{address, port}, fix_session_setttings)->run(); std::cout << "WebSocket echo server listening on port " << port << "\n"; ioc.run(); diff --git a/MiscClients/cpp_ws_reactjs/fix_ws_proxy/fixproxy.ini b/MiscClients/cpp_ws_reactjs/fix_ws_proxy/fixproxy.ini index 79605fe..b8689e2 100644 --- a/MiscClients/cpp_ws_reactjs/fix_ws_proxy/fixproxy.ini +++ b/MiscClients/cpp_ws_reactjs/fix_ws_proxy/fixproxy.ini @@ -14,4 +14,4 @@ TargetCompID=FIX_GWY_1 SocketConnectHost=127.0.0.1 SocketConnectPort=15001 HeartBtInt=30 -DataDictionary=FIX44.xml +DataDictionary=/Users/mkipnis/git/DistributedATS/FIXGateway/spec/FIX44.xml diff --git a/MiscClients/cpp_ws_reactjs/fix_ws_proxy/ws_server.cpp b/MiscClients/cpp_ws_reactjs/fix_ws_proxy/ws_server.cpp index 3e9d37f..7658b80 100644 --- a/MiscClients/cpp_ws_reactjs/fix_ws_proxy/ws_server.cpp +++ b/MiscClients/cpp_ws_reactjs/fix_ws_proxy/ws_server.cpp @@ -37,7 +37,6 @@ void Session::on_read(beast::error_code ec, std::size_t bytes_transferred) { boost::json::object replay; if ( !FIX::Session::doesSessionExist(qualified_id) ) { - settings_ = std::make_shared("/Users/mkipnis/Documents/oms/oms/fixproxy.ini"); settings_->set(qualified_id, FIX::Dictionary()); pending_logon_ = fix_message; @@ -45,7 +44,9 @@ void Session::on_read(beast::error_code ec, std::size_t bytes_transferred) { storeFactory_ = std::make_shared(); logFactory_ = std::make_shared( *settings_ ); - std::cout << "FIX Message : " << fix_message << std::endl; + auto json_obj = distributed_ats::fix_json::fix_to_json(fix_message); + auto json_str = boost::json::serialize(json_obj); + std::cout << "FIX Message : " << json_str << std::endl; application_ = std::make_shared(shared_from_this()); socket_initator_ = std::make_shared( *application_, *storeFactory_, *settings_, *logFactory_ ); @@ -55,7 +56,12 @@ void Session::on_read(beast::error_code ec, std::size_t bytes_transferred) { replay["success"] = true; } else { - std::cout << "About to send: " << fix_message << std::endl; + //std::cout << "About to send: " << fix_message << std::endl; + + auto json_obj = distributed_ats::fix_json::fix_to_json(fix_message); + auto json_str = boost::json::serialize(json_obj); + std::cout << "FIX Message : " << json_str << std::endl; + replay["token"] = fix_session_qualifier_; bool success = FIX::Session::sendToTarget(fix_message, qualified_id); diff --git a/MiscClients/cpp_ws_reactjs/fix_ws_proxy/ws_server.h b/MiscClients/cpp_ws_reactjs/fix_ws_proxy/ws_server.h index 37e5244..da6b470 100644 --- a/MiscClients/cpp_ws_reactjs/fix_ws_proxy/ws_server.h +++ b/MiscClients/cpp_ws_reactjs/fix_ws_proxy/ws_server.h @@ -33,8 +33,12 @@ class fix_application; // A single WebSocket session (handles one client) class Session : public std::enable_shared_from_this { public: - explicit Session(tcp::socket socket) - : ws_(std::move(socket)) {} + explicit Session(tcp::socket socket, const std::stringstream& session_settings_stream) + : ws_(std::move(socket)) + { + std::stringstream ss(session_settings_stream.str()); + settings_ = std::make_shared(ss); + } void start() { // Accept the websocket handshake @@ -62,7 +66,6 @@ class Session : public std::enable_shared_from_this { std::string fix_session_qualifier_; std::string ws_session_qualifier_; - std::shared_ptr settings_; std::shared_ptr storeFactory_; std::shared_ptr logFactory_; @@ -172,8 +175,11 @@ class Session : public std::enable_shared_from_this { // Accepts incoming connections and launches sessions class Listener : public std::enable_shared_from_this { public: - Listener(asio::io_context& ioc, tcp::endpoint endpoint) - : ioc_(ioc), acceptor_(ioc) { + Listener(asio::io_context& ioc, tcp::endpoint endpoint, std::shared_ptr& fix_session_settings) + : ioc_(ioc), acceptor_(ioc) + { + session_settings_stream_ << *fix_session_settings; + beast::error_code ec; acceptor_.open(endpoint.protocol(), ec); @@ -196,6 +202,7 @@ class Listener : public std::enable_shared_from_this { private: asio::io_context& ioc_; tcp::acceptor acceptor_; + std::stringstream session_settings_stream_; void do_accept() { acceptor_.async_accept( @@ -208,7 +215,7 @@ class Listener : public std::enable_shared_from_this { std::cerr << "accept failed: " << ec.message() << "\n"; } else { // Create and run session - std::make_shared(std::move(socket))->start(); + std::make_shared(std::move(socket), session_settings_stream_)->start(); } do_accept(); } diff --git a/MiscClients/cpp_ws_reactjs/webtrader_reactjs_ws/package.json b/MiscClients/cpp_ws_reactjs/webtrader_reactjs_ws/package.json index c05b8ad..9ec70d3 100644 --- a/MiscClients/cpp_ws_reactjs/webtrader_reactjs_ws/package.json +++ b/MiscClients/cpp_ws_reactjs/webtrader_reactjs_ws/package.json @@ -14,7 +14,8 @@ "react-bootstrap": "^2.6.0", "react-dom": "^18.1.0", "react-scripts": "5.0.1", - "web-vitals": "^2.1.4" + "web-vitals": "^2.1.4", + "ws": "^8.18.3" }, "scripts": { "start": "react-scripts start", diff --git a/MiscClients/cpp_ws_reactjs/webtrader_reactjs_ws/src/App.js b/MiscClients/cpp_ws_reactjs/webtrader_reactjs_ws/src/App.js index e27334b..53e3128 100644 --- a/MiscClients/cpp_ws_reactjs/webtrader_reactjs_ws/src/App.js +++ b/MiscClients/cpp_ws_reactjs/webtrader_reactjs_ws/src/App.js @@ -1,236 +1,268 @@ - // DistributedATS - Mike Kipnis (c) 2022 -import logo from './logo.svg'; -import './App.css'; -import 'bootstrap/dist/css/bootstrap.min.css'; -import Login from './components/Login'; -import PositionsAndMarketData from './components/PositionsAndMarketData'; -import Ticket from './components/Ticket'; -import History from './components/History'; -import PriceLevels from './components/PriceLevels'; -import SessionStateWrapper from './components/SessionStateWrapper'; -import React from 'react'; -import { useEffect, useState, useMemo } from 'react'; -import { Container, Row, Col } from 'react-bootstrap/'; - -import helpers from './components/helpers'; +import "./App.css"; +import "bootstrap/dist/css/bootstrap.min.css"; -import WebSocketDemo from './components/WebSocketDemo'; +import React, { + useEffect, + useState, + useRef, + useCallback, +} from "react"; +import { Container, Row, Col } from "react-bootstrap/"; -const { forwardRef, useRef, useImperativeHandle } = React; +import Login from "./components/Login"; +import PositionsAndMarketData from "./components/PositionsAndMarketData"; +import Ticket from "./components/Ticket"; +import History from "./components/History"; -function App() -{ - const ticketRef = React.useRef(); - const histRef = React.useRef(); - const marketDataAndPositionsRef = React.useRef(); +import { FIXWebSocketClient } from "./websocket_fix_utils/FIXWebSocketClient"; +import { FIXMessageHandler } from "./websocket_fix_utils/FIXMessageHandler"; - const url = window.location.href; - //const url = 'http://localhost:8080'; +import { DataMan } from "./data_man/DataMan"; - const last_sequence_number = useRef(0); // sequence number between front-end and rest controller - const last_session_state = useRef(null); - const current_ticket = useRef(null); +function App() { + const ticketRef = useRef(); + const histRef = useRef(); + const marketDataAndPositionsRef = useRef(); + const fixSessionHandler = useRef(null); + const fixClient = useRef(null); + const dataMan = useRef(null); const [sessionToken, setSessionToken] = useState(null); - const [sessionState, setSessionState] = useState(null); - const [ticketState, setTicketState] = useState({instrumentName:"N/A",price:0,quantity:0,username:"",token:""}); - const [loginState, setLoginState] = useState({sessionStateCode:0,text:"Please login"}); - const [blotterData, setBlotterData] = useState([]); - const [orderCancelData, setOrderCancelData] = useState({}); - const [histData, setHistData] = useState([]); - const [priceLevels, setPriceLevels] = useState([]); + const [loginState, setLoginState] = useState({ + sessionStateCode: 0, + text: "Please login", + }); - const Logon_callback = (logon_value) => - { - const requestOptionsResults = { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(logon_value) }; - fetch(url + '/Login', requestOptionsResults) .then(res => res.json()) - .then(result => setSessionToken(result)) - .catch(err => { + const [blotterData, setBlotterData] = useState(); + const [selectedInstrument, setSelectedInstrument] = useState(); + const [lastExecReport, setLastExecReport] = useState(); + const [selectedInstrumentBlotterData, setSelectedInstrumentBlotterData] = + useState(); - var invalid_state = {}; - invalid_state["text"] = 'Unable to connect to the server'; - setLoginState(invalid_state); + const [histData, setHistData] = useState([]); - }); + function getWebSocketUrl() { + const host = window.location.hostname; // localhost, docker container, or prod host + const port = 9002; // your FIX WS port + const protocol = window.location.protocol === "https:" ? "wss" : "ws"; + return `${protocol}://${host}:${port}`; } - const Populate_ticket = ( ticket_data ) => - { - ticket_data.url = url; - if ( last_session_state.current !== null ) - { - ticket_data.token = last_session_state.current.token; - ticket_data.username = last_session_state.current.username; + // ===================================================================================== + // FIX LOGON CALLBACK (USER PRESSES LOGIN BUTTON) + // ===================================================================================== + const Logon_callback = useCallback((logonValue) => { - current_ticket.current = ticket_data; - } + if (!fixClient.current) { + const wsUrl = getWebSocketUrl(); - setTicketState( ticket_data ); + // Create FIX handler + fixSessionHandler.current = new FIXMessageHandler({ + username: logonValue.username, + password: logonValue.password, + targetCompID: "FIX_GWY_1", + }); - if ( ticket_data.instrumentData != null ) - { - var price_levels = helpers.get_price_levels(ticket_data.instrumentData); - setPriceLevels(price_levels); - } - } + // Pass handler into client + fixClient.current = new FIXWebSocketClient(wsUrl, fixSessionHandler.current); + + dataMan.current = new DataMan(fixSessionHandler.current); + + fixClient.current.onopen = () => { + console.log("WS opened"); + }; + // ------------------------- + // WS MESSAGE + // ------------------------- + fixClient.current.onmessage = (msg) => { + try { + if (msg?.data_type !== "FIX") return; + + const msgType = msg?.Header?.["35"]; + + // ------------------------- + // FIX LOGON (35=A) + // ------------------------- + if (msgType === "A") { + console.log("🔐 FIX Logon received"); + setLoginState({ text: "Logon successful" }); + setSessionToken({ + token: msg.session_qualifier, + username: logonValue.username, + }); + return; + } else if ( msgType === "5") + { + console.log("🔐 FIX Logout received", msg); + setLoginState({ text: msg?.Body?.["58"] }); + return; + } - const Submit_cancel = (cancel_order) => - { - var cancel_out = {}; - - cancel_out["token"] = last_session_state.current.token; - cancel_out["username"] = last_session_state.current.username; - cancel_out["side"] = cancel_order.side; - cancel_out["orderKey"] = cancel_order.orderKey; - cancel_out["securityExchange"] = cancel_order.securityExchange; - cancel_out["symbol"] = cancel_order.symbol; - - const requestOptionsResults = { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(cancel_out) }; - fetch(url + '/CancelOrder', requestOptionsResults) .then(res => res.json()) - .then(result => setOrderCancelData(result)) - .catch(err => {console.log(err)}); - } + // ------------------------- + // ALL OTHER FIX MSGS + // ------------------------- + const result = dataMan.current.processFIXMessage( + fixClient.current, + msg + ); - useEffect(() => { - console.log("Hist Data : " + histData ); - },[histData]); + if (result !== undefined) { + setBlotterData(result); + } - useEffect(() => { - console.log("Order Cancel Data : " + orderCancelData );; - },[orderCancelData]); + const lastType = dataMan.current.get_last_msg_type(); - useEffect(() => { - - if ( sessionToken !== null ) - { - console.log("sessionToken : " + sessionToken.token); - - var session_state_request = {}; - session_state_request["token"] = sessionToken.Token; - session_state_request["stateMask"] = 0; - session_state_request["maxOrderSequenceNumber"] = 0; - session_state_request["marketDataSequenceNumber"] = 0; - - ticketState.username = sessionToken.username; - ticketState.token = sessionToken.Token;; - - var instrument = {}; - - var instrument_array = []; - session_state_request["activeSecurityList"] = instrument_array; - - const requestOptionsResults = { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(session_state_request) }; - fetch(url + '/SessionState', requestOptionsResults) .then(res => res.json()) - .then(result => setSessionState(result)) - .catch(err => { - var invalid_state = {}; - invalid_state["text"] = 'Disconnected from the server'; - setLoginState(invalid_state); - setSessionState(null); - }); + if (lastType === "y") { + fixSessionHandler.current.sendMassOrderStatusRequest(fixClient.current); + } else if (lastType === "8") { + const orderMan = dataMan.current.get_order_man(); + setHistData(orderMan.get_order_states()); + } + } catch (err) { + console.error("Failed to parse FIX WS message:", err); + } + }; + + // ------------------------- + // WS ERROR + // ------------------------- + fixClient.current.onerror = (err) => { + console.error("❌ FIX WS error:", err); + setLoginState({ text: "FIX WebSocket connection failed" }); + }; + + // ------------------------- + // WS CLOSE + // ------------------------- + fixClient.current.onclose = () => { + console.warn("⚠️ FIX WS closed"); + setLoginState({ text: "Disconnected from FIX WS" }); + setBlotterData(undefined); + }; } - }, [sessionToken]); + fixClient.current.connect(); + }, []); + // ===================================================================================== + // UPDATE UI ON BLOTTER CHANGES + // ===================================================================================== useEffect(() => { + if (!blotterData) return; - if ( sessionState !== null ) - { - last_session_state.current = sessionState; - - var session_state_request = {}; - session_state_request["token"] = sessionToken.Token; - - - const session_state_wrapper = new SessionStateWrapper(sessionState, session_state_request); - - setLoginState(session_state_wrapper.get_logon_state()); - - if ( sessionState.sessionState == 1 ) // This session is active - { - var blotter_data = session_state_wrapper.get_market_data_and_positions(); - - setBlotterData(blotter_data); - - marketDataAndPositionsRef.current.update_data(); - - setHistData(session_state_wrapper.get_hist_data(histData, last_sequence_number)); - - if ( current_ticket.current != null ) - { - - var instrument_data = blotter_data[current_ticket.current.instrumentName]; - if ( instrument_data != null ) - { - var price_levels = helpers.get_price_levels(instrument_data); - setPriceLevels(price_levels); - } - } - - } - - if ( sessionState.sessionState != 3 ) - { - const intervalId = setInterval(() => { - const requestOptionsResults = { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(session_state_request) }; - fetch(url + '/SessionState', requestOptionsResults) .then(res => res.json()) - .then(result => setSessionState(result)) - .catch(err => { - var invalid_state = {}; - invalid_state["text"] = 'Disconnected from the server'; - setLoginState(invalid_state); - setSessionState(null); - }); - }, 1000) - - return () => { - clearInterval(intervalId); - } - } else { + if (selectedInstrument) { + setSelectedInstrumentBlotterData({ + ...blotterData[selectedInstrument], + }); - } - } else { - console.log("Session State is Null"); + setLastExecReport({ + ...blotterData["last_exec_report"], + }); } - }, [sessionState]); + if (marketDataAndPositionsRef.current) { + marketDataAndPositionsRef.current.update_data(); + } + }, [blotterData, selectedInstrument]); + + // ===================================================================================== + // INSTRUMENT SELECTION CALLBACK + // ===================================================================================== + const SelectedInstrument = (instrument) => { + setSelectedInstrument(instrument); + }; + + // ===================================================================================== + // SESSION ESTABLISHED → REQUEST SECURITY LIST + // ===================================================================================== + useEffect(() => { + if (!sessionToken || !fixClient.current) return; - return ( ); + console.log("🔐 FIX session token:", sessionToken); + fixSessionHandler.current.sendSecurityListRequest(fixClient.current); + }, [sessionToken]); - /* + // ===================================================================================== + // CLEANUP + // ===================================================================================== + useEffect(() => { + return () => { + if (fixClient.current) fixClient.current.disconnect(); + }; + }, []); + + // ===================================================================================== + // RENDER UI + // ===================================================================================== + return (
-
- - -
- - -
- -
-
- Click on the instrument to trade. +
+ + + +
+ + + +
Click on an instrument to trade.
+ + + + { + return fixClient.current.sendJSON(msg); + }} + + sendCancelAll={(msg) => { + return fixClient.current.sendJSON(msg); + }} + + ref={ticketRef} + /> + +
+ +
+ { + return fixClient.current.sendJSON(msg); + }} + ref={histRef} + /> +
- - - - - -
- -
+
- -
-
*/ +
+ ); } export default App; diff --git a/MiscClients/cpp_ws_reactjs/webtrader_reactjs_ws/src/components/helpers.js b/MiscClients/cpp_ws_reactjs/webtrader_reactjs_ws/src/components/Helpers.js similarity index 53% rename from MiscClients/cpp_ws_reactjs/webtrader_reactjs_ws/src/components/helpers.js rename to MiscClients/cpp_ws_reactjs/webtrader_reactjs_ws/src/components/Helpers.js index 1c992c6..8976636 100644 --- a/MiscClients/cpp_ws_reactjs/webtrader_reactjs_ws/src/components/helpers.js +++ b/MiscClients/cpp_ws_reactjs/webtrader_reactjs_ws/src/components/Helpers.js @@ -25,7 +25,7 @@ const helpers = { } else { if ( price != null ) { - return price/tick_size; + return parseFloat(price/tick_size).toFixed(2); } } }, @@ -54,6 +54,36 @@ const helpers = { } return price_levels; + }, + + get_next_cl_order_id: function() + { + const currentTime = new Date(); + const secondsSinceMidnight = currentTime.getHours() * 3600 + currentTime.getMinutes() * 60 + currentTime.getSeconds(); + const year = currentTime.getFullYear(); + const month = String(currentTime.getMonth() + 1).padStart(2, '0'); // months are 0-based + const day = String(currentTime.getDate()).padStart(2, '0'); + const formattedDate = `${year}${month}${day}`; + + return formattedDate + "_" + secondsSinceMidnight; + }, + + + get_order_id: function(instrument_data) + { + return instrument_data['symbol'] + '_' + this.get_next_cl_order_id(); + }, + + get_fix_formatted_timestamp: function() + { + const now = new Date(); + return now.getUTCFullYear().toString().padStart(4, '0') + + String(now.getUTCMonth() + 1).padStart(2, '0') + + String(now.getUTCDate()).padStart(2, '0') + '-' + + String(now.getUTCHours()).padStart(2, '0') + ':' + + String(now.getUTCMinutes()).padStart(2, '0') + ':' + + String(now.getUTCSeconds()).padStart(2, '0') + '.' + + String(now.getUTCMilliseconds()).padStart(3, '0'); } } diff --git a/MiscClients/cpp_ws_reactjs/webtrader_reactjs_ws/src/components/History.js b/MiscClients/cpp_ws_reactjs/webtrader_reactjs_ws/src/components/History.js index e49dafe..d34325a 100644 --- a/MiscClients/cpp_ws_reactjs/webtrader_reactjs_ws/src/components/History.js +++ b/MiscClients/cpp_ws_reactjs/webtrader_reactjs_ws/src/components/History.js @@ -2,21 +2,41 @@ import { useCallback, useMemo, useEffect, useState } from 'react'; import React from 'react'; import {AgGridColumn, AgGridReact} from 'ag-grid-react'; -import helpers from './helpers'; +import Helpers from './Helpers'; import 'ag-grid-community/dist/styles/ag-grid.css'; import 'ag-grid-community/dist/styles/ag-theme-balham-dark.css'; +import { ORDER_STATUS_MAP, + ORDER_TYPE_FIX_MAP, + FIX_ORDER_CONDITION_MAP, + FIX_ORDER_TYPE_MAP, + FIX_ORDER_SIDE_MAP } from '../websocket_fix_utils/FIXConvertUtils'; + const { forwardRef, useRef, useImperativeHandle } = React; const History = React.forwardRef ((props, ref) => { - const HistMessageRenderer = (value) => { + const HistMessageRenderer = (order_to_cancel) => { - const invokeCancelMethod = () => { - props.orderCancelCallback(value); - }; + const invokeCancelMethod = () => { + + const instrument_data = order_to_cancel['instrument'] + const cancel_order_id = Helpers.get_order_id(instrument_data) + + const msg = props.fixSessionHandler.current.composeHeader("F") + msg.Body = { + '11': cancel_order_id, + '41': order_to_cancel['order_id'], + '54': order_to_cancel['side'], + '55': instrument_data['symbol'], + '207': instrument_data['securityExchange'], + '60': Helpers.get_fix_formatted_timestamp() + }; + + props.sendCancelOrder(msg) + }; return ( @@ -47,35 +67,56 @@ const History = React.forwardRef ((props, ref) => { { return ""; } else { - return helpers.get_display_price(params.value, params.data.instrument.tickSize); + return Helpers.get_display_price(params.value, params.data.instrument.tickSize); } + }; + function size_formatter(params) + { + if (params.value == null || isNaN(Number(params.value)) || Number(params.value) === 0) { + return ""; + } }; const [columnDefs, setColumnDefs] = useState([ - { headerName: 'Exec.Report Timestamp', field: 'lastUpdateTime', sortable: true,flex: 2, filter: 'agTextColumnFilter', }, - { headerName: 'Last.ExecReportID', field: 'lastExecutionReportId', sortable: true, flex: 2, filter: 'agTextColumnFilter', }, - { headerName: 'OrderStatus', field: 'status', sortable: true, flex: 2 }, + { headerName: 'Timestamp', field: 'lastUpdateTime', sortable: true,flex: 0, filter: 'agTextColumnFilter', }, + { headerName: 'Last.ExecReportID', field: 'lastExecutionReportId', sortable: true, flex: 2, filter: 'agTextColumnFilter', width:100}, + { headerName: 'OrderId', field: 'order_id', sortable: true, flex: 0, width:175 }, { headerName: 'SecurityExchange', field: 'securityExchange', sortable: true, flex: 2 }, { headerName: 'Symbol', field: 'symbol', sortable: true, flex: 2 }, - { headerName: 'OrderKey', field: 'orderKey', sortable: true, flex: 2 }, - { headerName: 'Price', field: 'price', sortable: true, flex: 2, valueFormatter:price_formatter }, - { headerName: 'StopPx', field: 'stop_price', sortable: true, flex: 2, valueFormatter:price_formatter }, - { headerName: 'Quantity', field: 'quantity', sortable: true, flex: 2 }, - { headerName: 'Side', field: 'side', sortable: true, flex: 2 }, - { headerName: 'FilledAvgPrice', field: 'filled_avg_price', sortable: true, flex: 2, valueFormatter:price_formatter }, - { headerName: 'FilledQty', field: 'filled_quantity', sortable: true, flex: 2 }, - { headerName: 'All or None', field: 'allOrNone', sortable: true, flex: 2 }, - { headerName: 'Condition', field: 'orderCondition', sortable: true, flex: 2 }, - { headerName: 'OrdType', field: 'orderType', sortable: true, flex: 2 }, - - + { headerName: 'OrderStatus', field: 'orderStatus', sortable: true, width:100, + valueFormatter: params => ORDER_STATUS_MAP[params.value] || params.value}, + { headerName: 'Side', field: 'side', sortable: true, flex: 2, valueFormatter: params => FIX_ORDER_SIDE_MAP[params.value] || params.value}, + { headerName: 'Price', field: 'price', sortable: true, flex: 2, valueFormatter:price_formatter, cellStyle: { textAlign: 'right' }}, + { headerName: 'StopPx', field: 'stop_price', sortable: true, flex: 2, valueFormatter:price_formatter, cellStyle: { textAlign: 'right' }}, + { headerName: 'Quantity', field: 'quantity', sortable: true, flex: 2, cellStyle: { textAlign: 'right' } }, + { headerName: 'FilledAvgPrice', field: 'filled_avg_price', sortable: true, flex: 2, valueFormatter:price_formatter, cellStyle: { textAlign: 'right' } }, + { headerName: 'FilledQty', field: 'filled_quantity', sortable: true, + flex: 2, valueFormatter:size_formatter, cellStyle: { textAlign: 'right' } }, + { + headerName: 'All or None', + flex: 2, + valueGetter: params => params.data.execInst?.includes('G') || false, + cellRenderer: 'agCheckboxCellRenderer', + sortable: true + }, + { + headerName: 'Condition', field: 'time_in_force', sortable: true, flex: 2, + valueFormatter: params => { + const value = params.value; + if (FIX_ORDER_CONDITION_MAP[value] === 'None') return ''; + return FIX_ORDER_CONDITION_MAP[value] || value; + } + }, + { headerName: 'OrdType', field: 'orderType', sortable: true, flex: 2, + valueFormatter: params => FIX_ORDER_TYPE_MAP[params.value] || params.value + }, { headerName: 'Actions', field: 'value', cellRenderer: params => { - if ( params.data.status == "New" || params.data.status == "Partially Filled") + if ( params.data.orderStatus == '0' || params.data.orderStatus == '1') return HistMessageRenderer(params.data); else return null; diff --git a/MiscClients/cpp_ws_reactjs/webtrader_reactjs_ws/src/components/Login.js b/MiscClients/cpp_ws_reactjs/webtrader_reactjs_ws/src/components/Login.js index 98fa1fc..447a92c 100644 --- a/MiscClients/cpp_ws_reactjs/webtrader_reactjs_ws/src/components/Login.js +++ b/MiscClients/cpp_ws_reactjs/webtrader_reactjs_ws/src/components/Login.js @@ -81,7 +81,7 @@ const Login = React.forwardRef ((props, ref) => {
-
+
diff --git a/MiscClients/cpp_ws_reactjs/webtrader_reactjs_ws/src/components/PositionsAndMarketData.js b/MiscClients/cpp_ws_reactjs/webtrader_reactjs_ws/src/components/PositionsAndMarketData.js index cc0d74d..58270e5 100644 --- a/MiscClients/cpp_ws_reactjs/webtrader_reactjs_ws/src/components/PositionsAndMarketData.js +++ b/MiscClients/cpp_ws_reactjs/webtrader_reactjs_ws/src/components/PositionsAndMarketData.js @@ -7,7 +7,7 @@ import {AgGridColumn, AgGridReact} from 'ag-grid-react'; import 'ag-grid-community/dist/styles/ag-grid.css'; import 'ag-grid-community/dist/styles/ag-theme-balham-dark.css'; -import helpers from './helpers'; +import Helpers from './Helpers'; const { forwardRef, useRef, useImperativeHandle } = React; @@ -20,25 +20,23 @@ const Blotter = React.forwardRef ((props, ref) => { function price_formatter(params) { - if (params.value == 0 ) + if (params.value == null || isNaN(Number(params.value)) || Number(params.value) === 0) { return ""; } else { - return helpers.get_display_price(params.value, params.data.tickSize); + return Helpers.get_display_price(params.value, params.data.tickSize); } }; function size_formatter(params) { - if (params.value == 0 ) - { + if (params.value == null || isNaN(Number(params.value)) || Number(params.value) === 0) { return ""; } }; - useImperativeHandle(ref, () => ({ update_data() { @@ -54,7 +52,7 @@ const Blotter = React.forwardRef ((props, ref) => { if ( instrument_update!== undefined && instrument_update!== null ) { - if ( instrument_update.marketDataSequenceNumber > last_market_data_sequence_number ) + //if ( instrument_update.marketDataSequenceNumber > last_market_data_sequence_number ) { node.setData(instrument_update); } @@ -72,15 +70,16 @@ const Blotter = React.forwardRef ((props, ref) => { } })); + const [columnDefs, setColumnDefs] = useState([ { headerName: 'symbol', field: 'symbol', filter: 'agTextColumnFilter', cellStyle: {'textAlign': 'left'}, sortable: true, }, - { headerName: 'Position', field: 'position', flex: 2, filter: 'agTextColumnFilter', }, + { headerName: 'Position', field: 'position', flex: 2, valueFormatter:size_formatter, filter: 'agTextColumnFilter', }, { headerName: 'VWAP', field: 'vwap', sortable: true, flex: 2,valueFormatter:price_formatter }, { headerName: 'PNL', field: 'pnl', sortable: true, flex: 2,valueFormatter:price_formatter }, - { headerName: 'BidPrice', field: 'bid_price', sortable: true, flex: 2, valueFormatter:price_formatter, cellStyle: {'textAlign': 'right'}}, - { headerName: 'AskPrice', field: 'ask_price', sortable: true, flex: 2, valueFormatter:price_formatter, cellStyle: {'textAlign': 'right'}}, - { headerName: 'BidSize', field: 'bid_size', sortable: true, flex: 2, valueFormatter:size_formatter, cellStyle: {'textAlign': 'right'}}, - { headerName: 'AskSize', field: 'ask_size', sortable: true, flex: 2, valueFormatter:size_formatter, cellStyle: {'textAlign': 'right'}}, + { headerName: 'BidPrice', field: 'top_level_bid_price', sortable: true, flex: 2, valueFormatter:price_formatter, cellStyle: {'textAlign': 'right'}}, + { headerName: 'AskPrice', field: 'top_level_ask_price', sortable: true, flex: 2, valueFormatter:price_formatter, cellStyle: {'textAlign': 'right'}}, + { headerName: 'BidSize', field: 'top_level_bid_size', sortable: true, flex: 2, valueFormatter:size_formatter, cellStyle: {'textAlign': 'right'}}, + { headerName: 'AskSize', field: 'top_level_ask_size', sortable: true, flex: 2, valueFormatter:size_formatter, cellStyle: {'textAlign': 'right'}}, { headerName: 'Volume', field: 'volume', sortable: true, flex: 2, valueFormatter:size_formatter, cellStyle: {'textAlign': 'right'}}, { headerName: 'OpenPrice', field: 'openPrice', sortable: true, flex: 2 , valueFormatter:price_formatter, cellStyle: {'textAlign': 'right'}}, { headerName: 'LastPrice', field: 'lastTradedPrice', sortable: true, flex: 2 , valueFormatter:price_formatter, cellStyle: {'textAlign': 'right'}}, @@ -91,54 +90,16 @@ const [columnDefs, setColumnDefs] = useState([ ]); - const gridOptions = { - rowSelection: 'single', - onRowClicked: event => { - - props.ticketState.instrumentName = event.data.instrumentName; - - if ( event.data.lastTradedPrice != 0 ) - props.ticketState.price = event.data.lastTradedPrice; - else - props.ticketState.price = event.data.openPrice; - - props.ticketState.price = props.ticketState.price / event.data.tickSize; - - props.ticketState.quantity = 100; - props.ticketState.securityExchange = event.data.securityExchange; - props.ticketState.symbol = event.data.symbol; - - /*let price_levels = []; - - for (let i = 0; i < 5; i++) - { - var level = {"bidPrice":0,"askPrice":0,"bidSize":0,"askSize":0}; - - if (event.data.bidSide[i]!=null) - { - level["bidPrice"] = event.data.bidSide[i].price; - level["bidSize"] = event.data.bidSide[i].size; - } - - if (event.data.askSide[i]!=null) - { - level["askPrice"] = event.data.askSide[i].price; - level["askSize"] = event.data.askSide[i].size; - } - - price_levels.push(level); - }*/ - - //props.ticketState.priceLevels = price_levels; - props.ticketState.instrumentData = event.data; - props.ticketState.tickSize = event.data.tickSize; - - props.marketDataCallback(props.ticketState); + const gridOptions = + { + rowSelection: 'single', + onRowClicked: event => { + props.selectedInstrument(event.data.instrumentName); }, onColumnResized: event => {}, - onRowDataChanged: event => { + onRowDataChanged: event => { var defaultSortModel = [ { colId: 'maturityDate', sort: 'asc', sortIndex: 0 } ]; @@ -150,9 +111,18 @@ const [columnDefs, setColumnDefs] = useState([ useEffect(() => { - props.marketDataCallback(ticketData); + console.log("Blotter Data : ", blotterData); + +}, [blotterData]); + + + /* +useEffect(() => { + + props.marketDataCallback(ticketData); -}, [ticketData]); + }, [ticketData]); +*/ const onBtnExport = useCallback(() => { diff --git a/MiscClients/cpp_ws_reactjs/webtrader_reactjs_ws/src/components/PriceLevels.js b/MiscClients/cpp_ws_reactjs/webtrader_reactjs_ws/src/components/PriceLevels.js index f7be31e..3618193 100644 --- a/MiscClients/cpp_ws_reactjs/webtrader_reactjs_ws/src/components/PriceLevels.js +++ b/MiscClients/cpp_ws_reactjs/webtrader_reactjs_ws/src/components/PriceLevels.js @@ -6,60 +6,51 @@ import {AgGridColumn, AgGridReact} from 'ag-grid-react'; import 'ag-grid-community/dist/styles/ag-grid.css'; import 'ag-grid-community/dist/styles/ag-theme-balham-dark.css'; -import helpers from './helpers'; +import Helpers from './Helpers'; const { forwardRef, useRef, useImperativeHandle } = React; const PriceLevels = React.forwardRef ((props, ref) => { + const [priceLevels, setPriceLevels] = useState(); - const [priceLevels, setPriceLevels] = useState({}); - const gridRef = useRef(); - - function price_formatter(params) + useEffect(() => { - if (params.value == 0 ) - { - return ""; - } else { + if ( props.selectedInstrumentBlotterData !== undefined ) + { + const bids = props.selectedInstrumentBlotterData['bidSide'] + const asks = props.selectedInstrumentBlotterData['askSide'] - return helpers.get_display_price(params.value, props.ticketState.tickSize); - } + const clean = val => (val === '0' || val === 0 || val === undefined ? ' ' : val); - }; - - function size_formatter(params) - { - if (params.value == 0 ) - { - return ""; - } - }; + const priceLevels = bids.map((bid, i) => ({ + bidPrice: clean(bid.price != 0 ? Helpers.get_display_price(bid.price, props.selectedInstrumentBlotterData['tickSize']): ' '), + bidSize: clean(bid.size), + askPrice: clean(asks[i]?.price != 0 ? Helpers.get_display_price(asks[i]?.price, props.selectedInstrumentBlotterData['tickSize']): ' '), + askSize: clean(asks[i]?.size) + })); - useImperativeHandle(ref, () => ({ + setPriceLevels(priceLevels); + } - update_history() - { - gridRef.current.api.refreshCells(); - } + }, [props.selectedInstrumentBlotterData ]); - })); -const [columnDefs, setColumnDefs] = useState([ - { headerName: 'BidPrice', field: 'bidPrice', sortable: false,flex: 2, valueFormatter:price_formatter }, - { headerName: 'AskPrice', field: 'askPrice', sortable: false, flex: 2, valueFormatter:price_formatter}, - { headerName: 'BidSize', field: 'bidSize', sortable: true, flex: 2, valueFormatter:size_formatter }, - { headerName: 'AskSize', field: 'askSize', sortable: true, flex: 2, valueFormatter:size_formatter }, - ]); + const [columnDefs, setColumnDefs] = useState([ + { headerName: 'BidPrice', field: 'bidPrice', sortable: false,flex: 2 }, + { headerName: 'AskPrice', field: 'askPrice', sortable: false, flex: 2 }, + { headerName: 'BidSize', field: 'bidSize', sortable: true, flex: 2 }, + { headerName: 'AskSize', field: 'askSize', sortable: true, flex: 2 }, + ]); return (
+ rowData={ priceLevels } columnDefs={columnDefs}>
); diff --git a/MiscClients/cpp_ws_reactjs/webtrader_reactjs_ws/src/components/SessionStateWrapper.js b/MiscClients/cpp_ws_reactjs/webtrader_reactjs_ws/src/components/SessionStateWrapper.js deleted file mode 100644 index f4b32d8..0000000 --- a/MiscClients/cpp_ws_reactjs/webtrader_reactjs_ws/src/components/SessionStateWrapper.js +++ /dev/null @@ -1,165 +0,0 @@ -// DistributedATS - Mike Kipnis (c) 2022 - -export const STATE_UNABLE_TO_CREATE_SESSION = -1; -export const STATE_PENDING_LOGON = 0; -export const STATE_SUCCESSFUL_LOGIN = 1; -export const STATE_LOGGED_OUT = 3; - - -export const LOGON_STATE_BIT = 0; -export const SECURITY_LIST_BIT = 1; -export const MARKET_DATA_BIT = 2; -export const ORDERS_DATA_BIT = 4; - -class SessionStateWrapper -{ - constructor(session_state, session_state_request) { - - console.log(session_state); - - this.session_state = session_state; - this.session_state_request = session_state_request; - this.login_state = {}; - - if ( this.session_state.sessionState == STATE_PENDING_LOGON ) - { - this.process_pending_login_state(); - } else if ( this.session_state.sessionState == STATE_LOGGED_OUT ) - { - this.process_logout_state(); - } else if ( this.session_state.sessionState == STATE_SUCCESSFUL_LOGIN ) - { - this.process_state_successful_logon(); - } - } - - process_pending_login_state() - { - this.stateMask = 0; - this.stateMask |= 1 << LOGON_STATE_BIT; - this.session_state_request["stateMask"] = this.stateMask; - var instrument_array = []; - this.session_state_request["activeSecurityList"] = instrument_array; - this.login_state = {text:"Pending Login ... "}; - } - - process_state_successful_logon() - { - this.login_state = {text:"Ready to trade"}; - - if ( this.session_state.activeSecurityList == null || this.session_state.activeSecurityList.length == 0 ) - { - this.stateMask = 0; - this.stateMask |= 1 << SECURITY_LIST_BIT; - this.session_state_request["stateMask"] = this.stateMask; - } else { - this.stateMask = 0; - this.stateMask |= 1 << MARKET_DATA_BIT; - this.stateMask |= 1 << ORDERS_DATA_BIT; - this.session_state_request["stateMask"] = this.stateMask; - this.session_state_request["activeSecurityList"] = this.session_state.activeSecurityList; - - - this.session_state_request.marketDataSequenceNumber = this.session_state.marketDataSequenceNumber; - } - } - - get_hist_data(histData, last_sequence_number) - { - for (const [index, order] of Object.entries(this.session_state.orders)) - { - last_sequence_number.current = order.sequenceNumber; - - order["orderKey"] = order.orderKey.orderKey; - order["securityExchange"] = order.instrument.securityExchange; - order["symbol"] = order.instrument.symbol; - - histData[order["orderKey"]] = order; - } - - this.session_state_request.maxOrderSequenceNumber = last_sequence_number.current; - - return histData; - } - - process_logout_state() - { - this.login_state = {text:"Disconnected : " + this.session_state.sessionStateText} - } - - get_logon_state() - { - return this.login_state; - } - - get_session_state_request() - { - return this.session_state_request; - } - - get_market_data_and_positions() - { - var active_instruments = {}; - - if ( this.session_state.activeSecurityList == null || this.session_state.activeSecurityList.length == 0 ) - return active_instruments; - - for (const [index, active_instrument] of Object.entries(this.session_state.activeSecurityList)) - { - var market_data_entry = this.session_state.instrumentMarketDataSnapshot[active_instrument.instrumentName]; - - if ( market_data_entry !== undefined ) - { - market_data_entry["instrumentName"] = active_instrument.instrumentName; - market_data_entry["securityExchange"] = active_instrument.securityExchange; - market_data_entry["symbol"] = active_instrument.symbol; - market_data_entry["maturityDate"] = active_instrument.maturityDate; - market_data_entry["tickSize"] = active_instrument.tickSize; - - if ( Object.values(market_data_entry.bidSide).length > 0 ) - { - market_data_entry["bid_price"] = market_data_entry.bidSide[0].price; - market_data_entry["bid_size"] = market_data_entry.bidSide[0].size; - } - - if ( Object.values(market_data_entry.askSide).length > 0 ) - { - market_data_entry["ask_price"] = market_data_entry.askSide[0].price; - market_data_entry["ask_size"] = market_data_entry.askSide[0].size; - } - - market_data_entry["volume"] = market_data_entry.volume; - market_data_entry["lastTradedPrice"] = market_data_entry.lastTradedPrice; - market_data_entry["openPrice"] = market_data_entry.openPrice; - if ( market_data_entry.lastTradedPrice != 0 && market_data_entry.openPrice!=0 ) - market_data_entry["priceChange"] = market_data_entry.lastTradedPrice-market_data_entry.openPrice; - else - market_data_entry["priceChange"] = 0; - - if ( this.session_state.positionsMap[active_instrument.instrumentName] !== undefined ) - { - var position_data = this.session_state.positionsMap[active_instrument.instrumentName]; - market_data_entry["position"] = position_data.position; - market_data_entry["vwap"] = position_data.vwap; - - if ( position_data.position != 0 ) - { - market_data_entry["pnl"] = ( market_data_entry["lastTradedPrice"] - market_data_entry["vwap"] ) * - market_data_entry["position"]; - } else { - market_data_entry["pnl"] = position_data.sell_amt * position_data.sell_avg_price - - position_data.buy_amt * position_data.buy_avg_price; - } - } - } - - active_instruments[active_instrument.instrumentName] = market_data_entry; - - } - - return active_instruments; - } - -} - -export default SessionStateWrapper; diff --git a/MiscClients/cpp_ws_reactjs/webtrader_reactjs_ws/src/components/Ticket.js b/MiscClients/cpp_ws_reactjs/webtrader_reactjs_ws/src/components/Ticket.js index 0915f81..c5e2b09 100644 --- a/MiscClients/cpp_ws_reactjs/webtrader_reactjs_ws/src/components/Ticket.js +++ b/MiscClients/cpp_ws_reactjs/webtrader_reactjs_ws/src/components/Ticket.js @@ -3,7 +3,8 @@ import React from 'react'; import { useEffect, useState } from 'react'; import { Container, Row, Col, Button, Form } from 'react-bootstrap/'; import PriceLevels from './PriceLevels'; - +import Helpers from './Helpers'; +import { ORDER_TYPE_FIX_MAP, ORDER_CONDITION_FIX_MAP } from '../websocket_fix_utils/FIXConvertUtils'; const { forwardRef, useRef, useImperativeHandle } = React; @@ -19,6 +20,8 @@ const Ticket = React.forwardRef ((props, ref) => { const [cancelAllResults, setCancelAllResults] = useState({}); const [isTicketing, setIsTicketing] = useState(false); + const [lastTicket, setLastTicket] = useState(); + //const [lastExecReport, setLastExecReport] = useState(); const [ticketPrice, setTicketPrice] = useState(0); const [ticketStopPrice, setTicketStopPrice] = useState(0); const [ticketSize, setTicketSize] = useState(0); @@ -28,66 +31,116 @@ const Ticket = React.forwardRef ((props, ref) => { function submit_buy( e ) { e.preventDefault(); - props.ticketState.side = "BUY"; - Submit_order(props.ticketState); + Submit_order(1); } function submit_sell( e ) { e.preventDefault(); - props.ticketState.side = "SELL"; - Submit_order(props.ticketState); + Submit_order(2); } - const Submit_order = ( trade ) => + const Submit_order = ( side ) => { var ticket = {}; setIsTicketing(true); - ticket["symbol"] = trade.symbol; - ticket["securityExchange"] = trade.securityExchange; - ticket["quantity"] = ticketSize; - ticket["price_in_ticks"] = Math.round(ticketPrice * props.ticketState.tickSize); - ticket["stop_price_in_ticks"] = Math.round(ticketStopPrice * props.ticketState.tickSize); - ticket["side"] = trade.side; - ticket["order_type"] = orderType; - ticket["order_condition"] = orderCondition; - ticket["all_or_none"] = allOrNone; - ticket["username"] = trade.username; - ticket["token"] = trade.token; - - const requestOptionsResults = { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(ticket) }; - fetch(trade.url + '/SubmitOrder', requestOptionsResults) - .then(res => res.json()) - .then(result => setOrderSubmitResults(result)); - } + const now = new Date(); + + const instrument_data = props.selectedInstrumentBlotterData; + const order_id = Helpers.get_order_id(instrument_data) + const orderTypeCode = ORDER_TYPE_FIX_MAP[orderType]; + const timeInforceCode = ORDER_CONDITION_FIX_MAP[orderCondition]; + + const msg = props.fixSessionHandler.current.composeHeader("D") + msg.Body = { + '11': order_id, + '38': ticketSize, + ...(orderTypeCode !== 1 && { '44': Math.round(ticketPrice * instrument_data['tickSize']) }), + '54': side, + '55': instrument_data['symbol'], + '207': instrument_data['securityExchange'], + '40': orderTypeCode, + '59': timeInforceCode, + '60': Helpers.get_fix_formatted_timestamp() + }; + + if (allOrNone == true ) + msg.Body['18'] = 'G' + if ( orderTypeCode === 3 ) + msg.Body['99'] = Math.round(ticketStopPrice * instrument_data['tickSize']) + + setLastTicket( + props.sendOrder(msg) + ) + } const handleCancellAll = (e) => { e.preventDefault(); - setIsTicketing(true); - - var cancel_all = {}; + const mass_cancel_order_id = Helpers.get_next_cl_order_id(); - cancel_all["username"] = props.ticketState.username; - cancel_all["token"] = props.ticketState.token; + const msg = props.fixSessionHandler.current.composeHeader("q") + msg.Body = { + '11': mass_cancel_order_id, + '530':'7', + '60': Helpers.get_fix_formatted_timestamp() + }; - const requestOptionsResults = { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(cancel_all) }; - fetch(props.ticketState.url + '/CancelAll', requestOptionsResults) - .then(res => res.json()) - .then(result => setCancelAllResults(result)); + props.sendCancelAll(msg) }; useEffect(() => { - setTicketPrice(props.ticketState.price); - setTicketStopPrice(props.ticketState.price); - setTicketSize(props.ticketState.quantity); - }, [props.ticketState.price]); + if ( props.selectedInstrumentBlotterData !== undefined ) + { + let price = 0; + let quantity = 0; + + let last_trade_price = props.selectedInstrumentBlotterData['lastTradedPrice'] + if ( last_trade_price == undefined ) + last_trade_price = props.selectedInstrumentBlotterData['openPrice'] + + const ticket_size = props.selectedInstrumentBlotterData['tickSize'] + + if ( last_trade_price !== undefined ) + { + price = last_trade_price/ticket_size; + quantity = 1; + } else if ( props.selectedInstrumentBlotterData['top_level_bid_price'] != 0 ) + { + price = props.selectedInstrumentBlotterData ['top_level_bid_price']/ticket_size; + quantity = props.selectedInstrumentBlotterData['top_level_bid_size']; + } else if ( props.selectedInstrumentBlotterData['top_level_ask_price'] != 0 ) + { + price = props.selectedInstrumentBlotterData['top_level_ask_price']/ticket_size; + quantity = props.selectedInstrumentBlotterData['top_level_ask_price']; + } else { + price = props.selectedInstrumentBlotterData['openPrice']/ticket_size; + quantity = 1; + } + + const lastExecReport = props.selectedInstrumentBlotterData['last_exec_report']; + + if ( lastExecReport !== undefined && lastTicket !== undefined ) + { + if ( lastTicket['Body']['11'] == lastExecReport['Body']['37'] ) + { + setIsTicketing(false); + } + } else { + setTicketPrice(price); + setTicketStopPrice(price); + setIsTicketing(false); + } + + setTicketSize(quantity); + + } + + }, [props.selectedInstrumentBlotterData, lastTicket]); useEffect(() => { setIsTicketing(false); @@ -99,15 +152,16 @@ const Ticket = React.forwardRef ((props, ref) => { console.log("Cancel All Results : " + cancelAllResults );; },[cancelAllResults]); + return ( - + -
Trade : {props.ticketState.instrumentName}
+
Trade : {props.instrumentName}
- +
@@ -120,12 +174,19 @@ const Ticket = React.forwardRef ((props, ref) => {
- { - setTicketPrice(value.target.value); - } }/> + { + const val = parseFloat(e.target.value); + setTicketPrice(isNaN(val) ? 0 : val); + }} + onBlur={(e) => { + // Format to 2 decimals on blur + setTicketPrice((prev) => Number(prev.toFixed(2))); + }} + />
@@ -155,9 +216,9 @@ const Ticket = React.forwardRef ((props, ref) => { console.log("e.target.value", e.target.value); setOrderCondition(e.target.value); }}> - - - + + +
@@ -168,10 +229,19 @@ const Ticket = React.forwardRef ((props, ref) => {
- { setTicketStopPrice(value.target.value); } }/> + { + const val = parseFloat(e.target.value); + setTicketStopPrice(isNaN(val) ? 0 : val); // keep numeric state + }} + onBlur={() => { + // Format to 2 decimals only when leaving the input + setTicketStopPrice(prev => Number(prev.toFixed(2))); + }} +/>
diff --git a/MiscClients/cpp_ws_reactjs/webtrader_reactjs_ws/src/data_man/DataMan.js b/MiscClients/cpp_ws_reactjs/webtrader_reactjs_ws/src/data_man/DataMan.js new file mode 100644 index 0000000..373f923 --- /dev/null +++ b/MiscClients/cpp_ws_reactjs/webtrader_reactjs_ws/src/data_man/DataMan.js @@ -0,0 +1,224 @@ +// © Mike Kipnis - DistributedATS +// FIX WebSocket Client — reconnecting FIX session handler + +import { OrderMan } from './OrderMan.js' + +export class DataMan { + + constructor(fix_session_handler) { + this.instruments = {}; + this.market_data = {}; + this.positions = {}; + this.last_exec_reports = {}; + this.last_message_type = ""; + this.order_man = new OrderMan(); + this.fix_session_handler = fix_session_handler; + } + + + processFIXMessage(fix_client, fix_message) + { + const message_type = fix_message?.Header?.['35'] + if (message_type === "y") { + const instruments = fix_message?.Body?.['146'] + for (const instrument of instruments) { + + const symbol = instrument['55'] + const securityExchange = instrument['207'] + const instrumentName = securityExchange + ':' + symbol + + + this.instruments[instrumentName] = + {'symbol':symbol, + 'securityExchange':securityExchange, + 'instrumentName': securityExchange + ':' + symbol, + 'tickSize':100} + } + + this.fix_session_handler.sendMarketDataRequest(fix_client, instruments); + } else if ( message_type === "W" ) + { + const market_data_snapshot = fix_message?.Body?.['268'] + + const symbol = fix_message['Body']['55'] + const securityExchange = fix_message['Body']['207'] + const instrumentName = securityExchange + ':' + symbol + + let instrument_market_data = {} + + for (const market_data_entry of market_data_snapshot) { + this.populate_market_data(market_data_entry, instrument_market_data); + } + + this.market_data[instrumentName] = instrument_market_data; + } else if ( message_type === "X" ) + { + + const market_data_refresh = fix_message?.Body?.['268'] + + let instrument_market_data = {} + + for (const market_data_entry of market_data_refresh) { + this.process_incremental_market_data_refresh(market_data_entry, instrument_market_data); + } + + for (const instrument_name of Object.keys(instrument_market_data)) { + let market_data = instrument_market_data[instrument_name]; + this.market_data[instrument_name] = market_data; + } + + } else if ( message_type === "8" ) + { + const symbol = fix_message['Body']['55'] + const securityExchange = fix_message['Body']['207'] + const instrumentName = securityExchange + ':' + symbol + + this.last_exec_reports[instrumentName] = fix_message; + + const instrument = this.instruments[instrumentName] + + this.order_man.process_exec_report(instrument, fix_message, this.positions); + } + + this.last_message_type = message_type; + + return this.getPositionsAndMarketData(); + } + + get_last_msg_type() + { + return this.last_message_type; + } + + get_order_man() + { + return this.order_man; + } + + + populate_market_data(market_data_entry, market_data_snapshot) + { + const entry_type = market_data_entry['269']; + const entry_px = market_data_entry['270']; + const entry_size = market_data_entry['271']; + if ( entry_type === '0' ) // bid + { + if (market_data_snapshot['bids'] == undefined) + market_data_snapshot['bids'] = [] + + market_data_snapshot['bids'].push({'price':entry_px, 'size':entry_size}); + } else if ( entry_type === '1' ) // ask + { + if (market_data_snapshot['asks'] == undefined) + market_data_snapshot['asks'] = [] + + market_data_snapshot['asks'].push({'price':entry_px, 'size':entry_size}); + } else if ( entry_type === '4' ) // open price + { + market_data_snapshot['openPrice'] = parseInt(entry_px); + } else if ( entry_type === 'B' ) // trade volume + { + market_data_snapshot['volume'] = parseInt(entry_size); + } else if ( entry_type === '2' ) // last trade price + { + market_data_snapshot['lastTradedPrice'] = parseInt(entry_px); + } + } + + process_incremental_market_data_refresh(market_data_entry, market_data_snapshot) + { + const symbol = market_data_entry['55'] + const securityExchange = market_data_entry['207'] + + const instrumentName = securityExchange + ':' + symbol + + const entry_type = market_data_entry['269']; + const entry_px = market_data_entry['270']; + const entry_size = market_data_entry['271']; + + if ( market_data_snapshot[instrumentName] === undefined ) + market_data_snapshot[instrumentName] = {} + + if ( entry_type === '0' ) // bid + { + if (market_data_snapshot[instrumentName]['bids'] == undefined) + market_data_snapshot[instrumentName]['bids'] = [] + + market_data_snapshot[instrumentName]['bids'].push({'price':entry_px, 'size':entry_size}); + } else if ( entry_type === '1' ) // ask + { + if (market_data_snapshot[instrumentName]['asks'] == undefined) + market_data_snapshot[instrumentName]['asks'] = [] + + market_data_snapshot[instrumentName]['asks'].push({'price':entry_px, 'size':entry_size}); + } else if ( entry_type === '4' ) // open price + { + market_data_snapshot[instrumentName]['openPrice'] = entry_px; + } else if ( entry_type === 'B' ) // trade volume + { + market_data_snapshot[instrumentName]['volume'] = parseInt(entry_size); + } else if ( entry_type === '2' ) // last trade price + { + market_data_snapshot[instrumentName]['lastTradedPrice'] = entry_px; + } + } + + getPositionsAndMarketData() + { + if ( Object.keys(this.instruments).length == 0 || Object.keys(this.market_data).length == 0 ) + return undefined; + + let position_and_market_data = {}; + + for (const instrument_name of Object.keys(this.instruments)) { + let instrument = this.instruments[instrument_name]; + + if ( this.market_data[instrument_name] !== undefined ) + { + const market_data = this.market_data[instrument_name] + instrument['openPrice'] = parseInt(market_data['openPrice']) + instrument['volume'] = parseInt(market_data['volume']) + instrument['lastTradedPrice'] = market_data['lastTradedPrice'] + instrument['priceChange'] = instrument['lastTradedPrice'] - instrument['openPrice'] + + // top level + if ( market_data['bids'] != undefined ) + { + instrument['bidSide'] = market_data['bids']; + instrument['top_level_bid_price'] = parseInt(instrument['bidSide'][0]['price']) + instrument['top_level_bid_size'] = parseInt(instrument['bidSide'][0]['size']) + } else { + instrument['bidSide'] = []; + } + + if ( market_data['asks'] != undefined ) + { + instrument['askSide'] = market_data['asks']; + instrument['top_level_ask_price'] = parseInt(instrument['askSide'][0]['price']) + instrument['top_level_ask_size'] = parseInt(instrument['askSide'][0]['size']) + } else { + instrument['askSide'] = []; + } + + if ( this.positions[instrument_name] !== undefined ) + { + instrument['position'] = this.positions[instrument_name]['position'] + instrument['vwap'] = this.positions[instrument_name]['vwap'] + + instrument['pnl'] = ( instrument['lastTradedPrice'] - instrument["vwap"] ) * + instrument["position"]; + } + } + + if ( this.last_exec_reports[instrument_name] !== undefined ) + { + instrument['last_exec_report'] = this.last_exec_reports[instrument_name]; + } + + position_and_market_data[instrument_name] = instrument; + } + + return position_and_market_data; + } + +} diff --git a/MiscClients/cpp_ws_reactjs/webtrader_reactjs_ws/src/data_man/OrderMan.js b/MiscClients/cpp_ws_reactjs/webtrader_reactjs_ws/src/data_man/OrderMan.js new file mode 100644 index 0000000..9c40930 --- /dev/null +++ b/MiscClients/cpp_ws_reactjs/webtrader_reactjs_ws/src/data_man/OrderMan.js @@ -0,0 +1,64 @@ + +import { PositionMan } from './PositionMan.js' + + +export class OrderMan { + + constructor() { + this.order_trail = {}; + this.order_state = {}; + this.position_mananger = new PositionMan(); + } + + get_order_states() + { + return this.order_state; + } + + process_exec_report(instrument, execution_report, positions) { + + var execution_report_body = execution_report['Body'] + const order_id = execution_report_body['37'] + if ( this.order_trail[order_id] === undefined ) + this.order_trail[order_id] = [] + + this.order_trail[order_id].push(execution_report_body) + + var order_state = {} + order_state['filled_avg_price'] = parseInt(execution_report_body['6']) + order_state['filled_quantity'] = parseInt(execution_report_body['14']) + order_state['lastExecutionReportId'] = execution_report_body['17'] + order_state['execInst'] = execution_report_body['18'] + order_state['last_px'] = parseInt(execution_report_body['31']) + order_state['last_qty'] = parseInt(execution_report_body['32']) + order_state['order_id'] = execution_report_body['37'] + order_state['quantity'] = parseInt(execution_report_body['38']) + order_state['orderStatus'] = execution_report_body['39'] + order_state['orderType'] = execution_report_body['40'] + order_state['price'] = parseInt(execution_report_body['44']) + order_state['side'] = execution_report_body['54'] + order_state['symbol'] = execution_report_body['55'] + order_state['text'] = execution_report_body['58'] + order_state['time_in_force'] = execution_report_body['59'] + order_state['lastUpdateTime'] = execution_report_body['60'] + order_state['stop_price'] = execution_report_body['99'] + order_state['reject_reaseon'] = execution_report_body['103'] + order_state['exec_type'] = execution_report_body['150'] + order_state['leaves_qty'] = parseInt(execution_report_body['151']) + order_state['securityExchange'] = execution_report_body['207'] + order_state['instrument'] = instrument + + this.order_state[order_id] = order_state; + + if ( order_state['last_qty'] != 0) + { + this.position_mananger.insert_trade(instrument['instrumentName'], order_state['side'], parseInt(order_state['last_px']), parseInt(order_state['last_qty'])) + positions[instrument['instrumentName']] = { + 'position': this.position_mananger.get_position(instrument['instrumentName']), + 'vwap': this.position_mananger.get_vwap(instrument['instrumentName']) + } + + } + } + +}; diff --git a/MiscClients/cpp_ws_reactjs/webtrader_reactjs_ws/src/data_man/PositionMan.js b/MiscClients/cpp_ws_reactjs/webtrader_reactjs_ws/src/data_man/PositionMan.js new file mode 100644 index 0000000..f10a546 --- /dev/null +++ b/MiscClients/cpp_ws_reactjs/webtrader_reactjs_ws/src/data_man/PositionMan.js @@ -0,0 +1,55 @@ + + +export class PositionMan { + + constructor() { + this.positions = {}; + } + + insert_trade(instrumentName, side, price, quantity ) + { + let position = this.positions[instrumentName] + + if ( position == undefined ) + { + position = {'buy_amt':0, 'buy_avg_price':0, 'sell_amt':0, 'sell_avg_price':0}; + this.positions[instrumentName] = position; + } + + if ( side == 1 ) + { + position['buy_avg_price'] = ( position['buy_avg_price'] * position['buy_amt'] ) + + ( price * quantity ) / (position['buy_amt'] + quantity ) + + position['buy_amt'] = position['buy_amt'] + quantity; + } else if (side == 2 ) { + position['sell_avg_price'] = ( position['sell_avg_price'] * position['sell_amt'] ) + + ( price * quantity ) / (position['sell_amt'] + quantity ) + position['sell_amt'] = position['sell_amt'] + quantity; + } + + return this.positions; + } + + get_position(instrumentName) + { + let position = this.positions[instrumentName] + + if ( position != undefined ) + return position['buy_amt'] - position['sell_amt']; + + return 0; + } + + get_vwap(instrumentName) + { + let position = this.positions[instrumentName] + + if ( position != undefined && position['buy_amt']-position['sell_amt']!=0) + return (position['buy_amt']*position['buy_avg_price'] - position['sell_amt']*this['sell_avg_price'])/(position['buy_amt']-position['sell_amt']); + + return 0; + } + + +} diff --git a/MiscClients/cpp_ws_reactjs/webtrader_reactjs_ws/src/websocket_fix_utils/FIXConvertUtils.js b/MiscClients/cpp_ws_reactjs/webtrader_reactjs_ws/src/websocket_fix_utils/FIXConvertUtils.js new file mode 100644 index 0000000..560368d --- /dev/null +++ b/MiscClients/cpp_ws_reactjs/webtrader_reactjs_ws/src/websocket_fix_utils/FIXConvertUtils.js @@ -0,0 +1,114 @@ +export const ORDER_TYPE_FIX_MAP = { + 'Market': 1, + 'Limit': 2, + 'Stop': 3 +}; + +export const FIX_ORDER_TYPE_MAP = Object.fromEntries( + Object.entries(ORDER_TYPE_FIX_MAP).map(([k, v]) => [v, k]) +); + + +export const ORDER_SIDE_FIX_MAP = { + 'Buy': 1, + 'Sell': 2 +}; + +export const FIX_ORDER_SIDE_MAP = Object.fromEntries( + Object.entries(ORDER_SIDE_FIX_MAP).map(([k, v]) => [v, k]) +); + + +export const ORDER_CONDITION_FIX_MAP = { + 'None': 0, + 'IOC': 3, + 'FoK': 4 +}; + +export const FIX_ORDER_CONDITION_MAP = Object.fromEntries( + Object.entries(ORDER_CONDITION_FIX_MAP).map(([k, v]) => [v, k]) +); + +export const ORDER_STATUS_MAP = { + 0: "New", + 1: "Partially filled", + 2: "Filled", + 3: "Done for day", + 4: "Canceled", + 5: "Replaced (No longer used)", + 6: "Pending Cancel", + 7: "Stopped", + 8: "Rejected", + 9: "Suspended", + A: "Pending New", + B: "Calculated", + C: "Expired", + D: "Accepted for Bidding", + E: "Pending Replace" +}; + + +export const EXEC_INST_FIX_MAP = { + '0': 'Stay on offer side', + '1': 'Not held', + '2': 'Work', + '3': 'Go along', + '4': 'Over the day', + '5': 'Held', + '6': "Participate don't initiate", + '7': 'Strict scale', + '8': 'Try to scale', + '9': 'Stay on bid side', + 'A': 'No cross', + 'B': 'OK to cross', + 'C': 'Call first', + 'D': 'Percent of volume', + 'E': 'Do not increase - DNI', + 'F': 'Do not reduce - DNR', + 'G': 'All or none - AON', + 'H': 'Reinstate on system failure', + 'I': 'Institutions only', + 'J': 'Reinstate on trading halt', + 'K': 'Cancel on trading halt', + 'L': 'Last peg (last sale)', + 'M': 'Mid-price peg (midprice of inside quote)', + 'N': 'Non-negotiable', + 'O': 'Opening peg', + 'P': 'Market peg', + 'Q': 'Cancel on system failure', + 'R': 'Primary peg', + 'S': 'Suspend', + 'T': 'Fixed peg to local best bid or offer at time of order', + 'U': 'Customer display instruction', + 'V': 'Netting (for Forex)', + 'W': 'Peg to VWAP', + 'X': 'Trade along', + 'Y': 'Try to stop', + 'Z': 'Cancel if not best', + 'a': 'Trailing stop peg', + 'b': 'Strict limit', + 'c': 'Ignore price validity checks', + 'd': 'Peg to limit price', + 'e': 'Work to target strategy', + 'f': 'Intermarket sweep', + 'g': 'External routing allowed', + 'h': 'External routing not allowed', + 'i': 'Imbalance only', + 'j': 'Single execution requested for block trade', + 'k': 'Best execution', + 'l': 'Suspend on system failure', + 'm': 'Suspend on trading halt', + 'n': 'Reinstate on connection loss', + 'o': 'Cancel on connection loss', + 'p': 'Suspend on connection loss', + 'q': 'Release', + 'r': 'Execute as delta neutral using volatility provided', + 's': 'Execute as duration neutral', + 't': 'Execute as FX neutral', + 'u': 'Minimum guaranteed fill eligible', + 'v': 'Bypass non-displayed liquidity', + 'w': 'Lock', + 'x': 'Ignore notional value checks', + 'y': 'Trade at reference price', + 'z': 'Allow facilitation' +}; diff --git a/MiscClients/cpp_ws_reactjs/webtrader_reactjs_ws/src/websocket_fix_utils/FIXMessageHandler.js b/MiscClients/cpp_ws_reactjs/webtrader_reactjs_ws/src/websocket_fix_utils/FIXMessageHandler.js new file mode 100644 index 0000000..8d67625 --- /dev/null +++ b/MiscClients/cpp_ws_reactjs/webtrader_reactjs_ws/src/websocket_fix_utils/FIXMessageHandler.js @@ -0,0 +1,81 @@ +// © Mike Kipnis - DistributedATS +// Handles FIX-specific messages + +export class FIXMessageHandler { + constructor({ username, password, targetCompID }) { + this.username = username; + this.password = password; + this.targetCompID = targetCompID; + } + + composeHeader(msgType) { + return { + Header: { + "8": "FIX.4.4", + "35": msgType, + "49": this.username, + "56": this.targetCompID, + }, + }; + } + + sendLogon(client) { + const msg = this.composeHeader("A"); + msg.Body = { "553": this.username, "554": this.password }; + client.sendJSON(msg); + console.log("➡️ Sent FIX Logon"); + } + + sendHeartbeat(client) { + const msg = this.composeHeader("0"); + msg.Body = {}; + client.sendJSON(msg); + console.log("➡️ Sent FIX heartbeat"); + } + + sendSecurityListRequest(client) { + const msg = this.composeHeader("x"); + msg.Body = { "320": "RequestInstrList1", "559": 4 }; + client.sendJSON(msg); + console.log("➡️ Sent Security List Request"); + } + + sendMassOrderStatusRequest(client) { + const msg = this.composeHeader("AF"); + msg.Body = { "584": "RequestMassOrdStatus1", "585": 7 }; + client.sendJSON(msg); + console.log("➡️ Sent Mass Order Status Request"); + } + + sendMarketDataRequest(client, instruments = []) { + const msg = this.composeHeader("V"); + msg.Body = { + "262": "MarketDataSnapshot_1", + "263": 1, + "264": 0, + "146": [], + "267": [{ "269": 0 }, { "269": 1 }], + }; + + for (const instr of instruments) { + const filtered = {}; + if ("55" in instr) filtered["55"] = instr["55"]; + if ("207" in instr) filtered["207"] = instr["207"]; + msg.Body["146"].push(filtered); + } + + client.sendJSON(msg); + console.log("➡️ Sent Market Data Request"); + } + + sendHeartbeat(client) { + const msg = this.composeHeader("0"); + client.sendJSON(msg); + } + + sendLogout(client) { + const msg = this.composeHeader("5"); + client.sendJSON(msg); + console.log("➡️ Sent Logout"); + } +} diff --git a/MiscClients/cpp_ws_reactjs/webtrader_reactjs_ws/src/websocket_fix_utils/FIXWebSocketClient.js b/MiscClients/cpp_ws_reactjs/webtrader_reactjs_ws/src/websocket_fix_utils/FIXWebSocketClient.js new file mode 100644 index 0000000..4f16e2f --- /dev/null +++ b/MiscClients/cpp_ws_reactjs/webtrader_reactjs_ws/src/websocket_fix_utils/FIXWebSocketClient.js @@ -0,0 +1,104 @@ +// © Mike Kipnis - DistributedATS +// WebSocket FIX Client — heartbeat only, no auto-reconnect + +export class FIXWebSocketClient { + constructor(url, handler = null) { + this.url = url; + this.ws = null; + + this.handler = handler; // handler will send FIX messages + + this.heartbeatInterval = 30000; + this.heartbeatTimer = null; + + // Callbacks + this.onopen = null; + this.onmessage = null; + this.onerror = null; + this.onclose = null; + } + + get state() { + if (!this.ws) return "CLOSED"; + const rs = this.ws.readyState; + return rs === WebSocket.CONNECTING + ? "CONNECTING" + : rs === WebSocket.OPEN + ? "OPEN" + : rs === WebSocket.CLOSING + ? "CLOSING" + : "CLOSED"; + } + + connect() { + if (this.ws && (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING)) return; + + try { + this.ws = new WebSocket(this.url); + } catch (err) { + console.error("WS init failed:", err); + return; + } + + this.ws.onopen = evt => { + this.startHeartbeat(); + + // Send FIX Logon automatically + if (this.handler && typeof this.handler.sendLogon === "function") { + this.handler.sendLogon(this); + } + + if (this.onopen) this.onopen(evt); + }; + + this.ws.onmessage = evt => { + try { + const obj = JSON.parse(evt.data); + if (this.onmessage) this.onmessage(obj); + } catch (e) { + console.error("Invalid WS message:", evt.data); + } + }; + + this.ws.onerror = evt => { + console.error("FIX WS error", evt); + if (this.onerror) this.onerror(evt); + }; + + this.ws.onclose = evt => { + this.stopHeartbeat(); + if (this.onclose) this.onclose(evt); + // No auto-reconnect + }; + } + + disconnect() { + this.stopHeartbeat(); + if (this.ws) this.ws.close(); + } + + startHeartbeat() { + this.stopHeartbeat(); + this.heartbeatTimer = setInterval(() => { + if (this.ws && this.ws.readyState === WebSocket.OPEN) { + if (this.handler && typeof this.handler.sendHeartbeat === "function") { + this.handler.sendHeartbeat(this); + } + } + }, this.heartbeatInterval); + } + + stopHeartbeat() { + if (this.heartbeatTimer) clearInterval(this.heartbeatTimer); + this.heartbeatTimer = null; + } + + sendJSON(obj) { + if (this.ws && this.ws.readyState === WebSocket.OPEN) { + this.ws.send(JSON.stringify(obj)); + } else { + console.warn("WS send skipped — socket not OPEN"); + } + return obj; + } +} diff --git a/docker/Docker.MultiMatchingEngineATS b/docker/Docker.MultiMatchingEngineATS new file mode 100644 index 0000000..8fc667d --- /dev/null +++ b/docker/Docker.MultiMatchingEngineATS @@ -0,0 +1,18 @@ +FROM ghcr.io/mkipnis/distributed_ats:latest + +EXPOSE 15001 +EXPOSE 16001 +EXPOSE 17001 + +COPY FastDDS.xml /usr/local/ + +ENV DATS_HOME=/usr/local +ENV DEPS_HOME=/usr/local +ENV DATS_LOG_HOME=/var/log +ENV LOG_FILE_NAME=default_ats.log + +ENV LD_LIBRARY_PATH=/usr/local/lib:/lib +ENV LOG4CXX_CONFIGURATION=/usr/local/config/log4cxx.xml +ENV FASTDDS_DEFAULT_PROFILES_FILE=/usr/local/FastDDS.xml + +ENV BASEDIR_ATS=/usr/local/MiscATS/MultiMatchingEngineATS diff --git a/docker/Docker.WebTrader b/docker/Docker.WebTrader index d108ede..82b3555 100644 --- a/docker/Docker.WebTrader +++ b/docker/Docker.WebTrader @@ -1,7 +1,26 @@ -FROM tomcat:9.0 +# ---- Build Stage ---- +FROM node:18 AS build -ADD WebTraderRest-0.0.1-SNAPSHOT.war /usr/local/tomcat/webapps/ROOT.war +WORKDIR /app -COPY logging.properties /usr/local/tomcat/conf/logging.properties +# Copy only package files first for caching +COPY WebTrader/package*.json ./WebTrader/ -EXPOSE 8080 +RUN cd WebTrader && npm install + +# Copy the rest of the project +COPY WebTrader ./WebTrader + +# Build React app +RUN cd WebTrader && npm run build + + +# ---- Production Stage ---- +FROM nginx:alpine + +# Copy build output from build stage +COPY --from=build /app/WebTrader/build /usr/share/nginx/html + +EXPOSE 80 + +CMD ["nginx", "-g", "daemon off;"] diff --git a/docker/Docker.fix_ws_proxy b/docker/Docker.fix_ws_proxy new file mode 100644 index 0000000..fe0d36c --- /dev/null +++ b/docker/Docker.fix_ws_proxy @@ -0,0 +1,16 @@ +FROM ghcr.io/mkipnis/distributed_ats:latest + +# Add /usr/local/lib to library path +ENV LD_LIBRARY_PATH="/usr/local/lib:${LD_LIBRARY_PATH}" + +# Create config directory +RUN mkdir -p /usr/local/config + +# Copy fixproxy.ini into container config directory +COPY fixproxy.ini /usr/local/config/ + +# Expose the port +EXPOSE 9002 + +# Set entrypoint +ENTRYPOINT ["/usr/local/bin/fix_ws_proxy", "--fix_client_config", "/usr/local/config/fixproxy.ini"] diff --git a/docker/build_distributed_ats.sh b/docker/build_distributed_ats.sh index 7c87b35..bf454bc 100755 --- a/docker/build_distributed_ats.sh +++ b/docker/build_distributed_ats.sh @@ -1,6 +1,6 @@ #!/bin/bash -git clone -b enhancements_0424 https://github.com/mkipnis/DistributedATS /opt/distributed_ats_src +git clone -b misc_1025 https://github.com/mkipnis/DistributedATS /opt/distributed_ats_src cd /opt/distributed_ats_src cmake -S . -B build -DCMAKE_INSTALL_PREFIX=/usr/local -DDDS_ROOT_DIR=/usr/local -DLOG4CXX_ROOT_DIR=/usr/local -DQUICKFIX_ROOT_DIR=/usr/local -DLIQUIBOOK_ROOT_DIR=/usr/local diff --git a/docker/docker-compose-crypto.yml b/docker/docker-compose-crypto.yml index 8f58a97..90b5e2d 100644 --- a/docker/docker-compose-crypto.yml +++ b/docker/docker-compose-crypto.yml @@ -25,13 +25,27 @@ services: - "17001:17001" restart: unless-stopped + fix-ws-proxy: + build: + context: . + dockerfile: Dockerfile + container_name: fix-ws-proxy + image: ghcr.io/mkipnis/fix_ws_proxy:latest + ports: + - "9002:9002" + environment: + LD_LIBRARY_PATH: "/usr/local/lib" + volumes: + # Optional: override config without rebuilding + - ./fixproxy.ini:/usr/local/config/fixproxy.ini:ro + restart: unless-stopped + + # WebTrader Front-End distributed_ats_webtrader: container_name: distributed_ats_webtrader image: ghcr.io/mkipnis/distributed_ats_webtrader:latest - depends_on: - - distributed_ats volumes: - - ./webtrader_logs:/usr/local/tomcat/logs + - ./webtrader_logs:/var/log/nginx ports: - - "8080:8080" - restart: unless-stopped + - "8080:80" + restart: "no" diff --git a/docker/docker-compose-ust.yml b/docker/docker-compose-ust.yml index 3e3f49b..192e90f 100644 --- a/docker/docker-compose-ust.yml +++ b/docker/docker-compose-ust.yml @@ -24,14 +24,28 @@ services: - "16001:16001" restart: unless-stopped + fix-ws-proxy: + build: + context: . + dockerfile: Dockerfile + container_name: fix-ws-proxy + image: ghcr.io/mkipnis/fix_ws_proxy:latest + ports: + - "9002:9002" + environment: + LD_LIBRARY_PATH: "/usr/local/lib" + volumes: + # Optional: override config without rebuilding + - ./fixproxy.ini:/usr/local/config/fixproxy.ini:ro + restart: unless-stopped + # WebTrader Front-End distributed_ats_webtrader: container_name: distributed_ats_webtrader image: ghcr.io/mkipnis/distributed_ats_webtrader:latest - depends_on: - - distributed_ats volumes: - - ./webtrader_logs:/usr/local/tomcat/logs + - ./webtrader_logs:/var/log/nginx ports: - - "8080:8080" - restart: unless-stopped + - "8080:80" + restart: "no" + diff --git a/docker/docker-compose-webtrader.yml b/docker/docker-compose-webtrader.yml index 3b74624..9e0ad0b 100644 --- a/docker/docker-compose-webtrader.yml +++ b/docker/docker-compose-webtrader.yml @@ -1,12 +1,28 @@ version: '2' services: + + fix-ws-proxy: + build: + context: . + dockerfile: Dockerfile + container_name: fix-ws-proxy + image: ghcr.io/mkipnis/fix_ws_proxy:latest + ports: + - "9002:9002" + environment: + LD_LIBRARY_PATH: "/usr/local/lib" + volumes: + # Optional: override config without rebuilding + - ./fixproxy.ini:/usr/local/config/fixproxy.ini:ro + restart: unless-stopped + # WebTrader Front-End distributed_ats_webtrader: container_name: distributed_ats_webtrader image: ghcr.io/mkipnis/distributed_ats_webtrader:latest volumes: - - ./webtrader_logs:/usr/local/tomcat/logs + - ./webtrader_logs:/var/log/nginx ports: - - "8080:8080" - restart: no + - "8080:80" + restart: "no" diff --git a/docker/dockerize_dats.sh b/docker/dockerize_dats.sh index 539bba4..a8b6846 100755 --- a/docker/dockerize_dats.sh +++ b/docker/dockerize_dats.sh @@ -11,7 +11,21 @@ docker build --no-cache -t ghcr.io/mkipnis/dats_crypto_clob:latest -f Docker.Cry docker build --no-cache -t ghcr.io/mkipnis/dats_ust_clob:latest -f Docker.UST_CLOB . docker build --no-cache -t ghcr.io/mkipnis/multi_matching_engine_ats:latest -f Docker.MultiMatchingEngineATS . +# FIX WS Proxy +docker build --no-cache -t ghcr.io/mkipnis/fix_ws_proxy:latest -f Docker.fix_ws_proxy . + +# Webtrader Front-end +rm -rf WebTrader +mkdir WebTrader +cp -r ../MiscClients/cpp_ws_reactjs/webtrader_reactjs_ws/package.json WebTrader +cp -r ../MiscClients/cpp_ws_reactjs/webtrader_reactjs_ws/src WebTrader +cp -r ../MiscClients/cpp_ws_reactjs/webtrader_reactjs_ws/public WebTrader +docker build -t ghcr.io/mkipnis/distributed_ats_webtrader:latest -f Docker.WebTrader . + +# Push images to the github docker push ghcr.io/mkipnis/distributed_ats_deps:latest docker push ghcr.io/mkipnis/distributed_ats:latest docker push ghcr.io/mkipnis/dats_crypto_clob:latest docker push ghcr.io/mkipnis/dats_ust_clob:latest +docker push ghcr.io/mkipnis/fix_ws_proxy:latest +docker push ghcr.io/mkipnis/distributed_ats_webtrader:latest