diff --git a/CMakeLists.txt b/CMakeLists.txt index 4ad633d..501bad7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -31,7 +31,7 @@ add_subdirectory( FIXGateway/src ) add_subdirectory( DataService/src ) add_subdirectory( MatchingEngine/src ) add_subdirectory( LatencyTest ) -add_subdirectory( MiscClients/cpp_ws_reactjs/oms ) +add_subdirectory( MiscClients/cpp_ws_reactjs/fix_ws_proxy ) install(DIRECTORY ${CMAKE_SOURCE_DIR}/MiscATS/ DESTINATION MiscATS diff --git a/Common/fix_json_converter.h b/Common/fix_json_converter.h new file mode 100644 index 0000000..77b5455 --- /dev/null +++ b/Common/fix_json_converter.h @@ -0,0 +1,127 @@ +// Copyright (C) Mike Kipnis - DistributedATS +#pragma once + +#include +#include +#include +#include +#include + +namespace distributed_ats { +namespace fix_json { + +// Convert a FieldMap (Header, Body, or Trailer) to JSON object (recursive) +inline boost::json::object field_map_to_json(const FIX::FieldMap& map) +{ + boost::json::object json_obj; + + // --- Fields --- + for (FIX::FieldMap::iterator it = const_cast(map).begin(); + it != const_cast(map).end(); ++it) + { + const FIX::FieldBase& field = *it; + int tag = field.getTag(); + json_obj[std::to_string(tag)] = field.getString(); + } + + // --- Groups (repeating groups) --- + const FIX::FieldMap::Groups& group_maps = map.groups(); + for (auto group_it = group_maps.begin(); group_it != group_maps.end(); ++group_it) + { + int group_tag = group_it->first; + const std::vector& group_vector = group_it->second; + + boost::json::array json_group_array; + for (auto* group_map : group_vector) + { + json_group_array.push_back(field_map_to_json(*group_map)); + } + + json_obj[std::to_string(group_tag)] = std::move(json_group_array); + } + + return json_obj; +} + +// Convert entire FIX message to JSON +inline boost::json::object fix_to_json(const FIX::Message& message) +{ + boost::json::object json_root; + + json_root["Header"] = field_map_to_json(message.getHeader()); + json_root["Body"] = field_map_to_json(message); + json_root["Trailer"] = field_map_to_json(message.getTrailer()); + + return json_root; +} + +// Helper: convert JSON object to FieldMap (recursive) +inline void json_to_field_map(const boost::json::object& json_obj, FIX::FieldMap& map) +{ + for (auto& kv : json_obj) + { + const std::string& key = kv.key(); + const boost::json::value& value = kv.value(); + int tag = std::stoi(key); + + if (value.is_string()) + { + map.setField(FIX::StringField(tag, value.as_string().c_str())); + } + else if (value.is_array()) + { + const boost::json::array& arr = value.as_array(); + for (auto& item : arr) + { + if (!item.is_object()) continue; + FIX::Group group(tag, 0); // 0 used as placeholder delimiter + json_to_field_map(item.as_object(), group); + map.addGroup(tag, group); // FIX: addGroup(tag, group) + } + } + else + { + // Convert other JSON types to string + std::ostringstream oss; + oss << value; + map.setField(FIX::StringField(tag, oss.str())); + } + } +} + +// Convert JSON to FIX::Message +inline FIX::Message json_to_fix(const boost::json::object& json_msg) +{ + FIX::Message message; + + if (json_msg.if_contains("Header")) + json_to_field_map(json_msg.at("Header").as_object(), message.getHeader()); + + if (json_msg.if_contains("Body")) + json_to_field_map(json_msg.at("Body").as_object(), message); + + if (json_msg.if_contains("Trailer")) + json_to_field_map(json_msg.at("Trailer").as_object(), message.getTrailer()); + + return message; +} + +// Helper: JSON string → FIX::Message +inline FIX::Message json_to_fix(const std::string& json_str) +{ + boost::json::value parsed = boost::json::parse(json_str); + if (!parsed.is_object()) + throw std::runtime_error("Invalid JSON format for FIX message"); + + return json_to_fix(parsed.as_object()); +} + +// Helper: FIX::Message → JSON string +inline std::string fix_to_json_string(const FIX::Message& message) +{ + boost::json::object json_obj = fix_to_json(message); + return boost::json::serialize(json_obj); +} + +} // namespace fix_json +} // namespace distributed_ats diff --git a/MiscClients/cpp_ws_reactjs/oms/CMakeLists.txt b/MiscClients/cpp_ws_reactjs/fix_ws_proxy/CMakeLists.txt similarity index 69% rename from MiscClients/cpp_ws_reactjs/oms/CMakeLists.txt rename to MiscClients/cpp_ws_reactjs/fix_ws_proxy/CMakeLists.txt index 35c4124..9337830 100644 --- a/MiscClients/cpp_ws_reactjs/oms/CMakeLists.txt +++ b/MiscClients/cpp_ws_reactjs/fix_ws_proxy/CMakeLists.txt @@ -1,6 +1,6 @@ cmake_minimum_required(VERSION 3.12.4) -project(oms) +project(fix_ws_proxy) message(STATUS "Current source dir: ${CMAKE_CURRENT_SOURCE_DIR}") @@ -16,15 +16,18 @@ link_directories(${LOG4CXX_LIBRARY_DIR}) include_directories(${Boost_INCLUDE_DIR}) link_directories(${Boost_LIBRARY_DIRS}) -file(GLOB OMS_SRC *.cpp) -add_executable(oms ${OMS_SRC}) +file(GLOB FIX_WS_SRC *.cpp) +add_executable(fix_ws_proxy ${FIX_WS_SRC}) -find_package(Boost REQUIRED COMPONENTS program_options) +find_package(Boost REQUIRED COMPONENTS program_options thread chrono json) include_directories(${Boost_INCLUDE_DIRS}) -target_link_libraries(oms +target_link_libraries(fix_ws_proxy PRIVATE quickfix log4cxx Boost::program_options + Boost::thread + Boost::json + Boost::chrono ) diff --git a/MiscClients/cpp_ws_reactjs/fix_ws_proxy/FIX44.xml b/MiscClients/cpp_ws_reactjs/fix_ws_proxy/FIX44.xml new file mode 100755 index 0000000..030c920 --- /dev/null +++ b/MiscClients/cpp_ws_reactjs/fix_ws_proxy/FIX44.xml @@ -0,0 +1,6594 @@ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/MiscClients/cpp_ws_reactjs/fix_ws_proxy/fix_application.cpp b/MiscClients/cpp_ws_reactjs/fix_ws_proxy/fix_application.cpp new file mode 100644 index 0000000..092e688 --- /dev/null +++ b/MiscClients/cpp_ws_reactjs/fix_ws_proxy/fix_application.cpp @@ -0,0 +1,66 @@ +// Copyright (C) Mike Kipnis - DistributedATS +#include "fix_application.h" +#include "ws_server.h" + + +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 ); + + 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; + + FIX::Username userName(sessionID.getSenderCompID()); + message.setField(userName); + + FIX::Password password; + ws_logon.getField(password); + + message.setField(password); + + std::cout << "Sending Logon : " << message.toString() << std::endl; + } + } else if ( msgType == FIX::MsgType_Logout ) + { + std::cout << "Logout : " << message << std::endl; + } +} + +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); + } +} + + +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); + } +} diff --git a/MiscClients/cpp_ws_reactjs/fix_ws_proxy/fix_application.h b/MiscClients/cpp_ws_reactjs/fix_ws_proxy/fix_application.h new file mode 100644 index 0000000..56dcf9b --- /dev/null +++ b/MiscClients/cpp_ws_reactjs/fix_ws_proxy/fix_application.h @@ -0,0 +1,48 @@ +// Copyright (C) Mike Kipnis - DistributedATS +#pragma once + +#include +#include +#include +#include + +class Session; + +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; + } + + void onCreate(const FIX::SessionID& id) override { + std::cout << "Created: " << id << std::endl; + } + void onLogon(const FIX::SessionID& id) override { + std::cout << "Logon: " << id << std::endl; + } + void onLogout(const FIX::SessionID& id) override { + std::cout << "Logout: " << id << std::endl; + } + void toAdmin(FIX::Message& message, const FIX::SessionID& sessionID) override; + + void fromAdmin(const FIX::Message &, + const FIX::SessionID &) throw(FIX::FieldNotFound, + FIX::IncorrectDataFormat, + FIX::IncorrectTagValue, + FIX::RejectLogon) override; + void fromApp(const FIX::Message &message, + const FIX::SessionID &sessionID) throw(FIX::FieldNotFound, + FIX::IncorrectDataFormat, + FIX::IncorrectTagValue, + FIX::UnsupportedMessageType) override; + + + void toApp(FIX::Message&, const FIX::SessionID&) throw(FIX::DoNotSend) override {}; + +private: + std::weak_ptr _ws_session; +}; + 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 new file mode 100644 index 0000000..28f96f5 --- /dev/null +++ b/MiscClients/cpp_ws_reactjs/fix_ws_proxy/fix_ws_proxy.cpp @@ -0,0 +1,70 @@ +// Copyright (C) Mike Kipnis - DistributedATS + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "fix_application.h" +#include "ws_server.h" + +using boost::asio::ip::tcp; +namespace asio = boost::asio; +namespace po = boost::program_options; + + +namespace asio = boost::asio; +namespace beast = boost::beast; +namespace http = beast::http; +namespace ws = beast::websocket; +using tcp = asio::ip::tcp; + + +int main(int argc, char** argv) { + try { + // Default values + auto const address = asio::ip::make_address("0.0.0.0"); + unsigned short port = 9002; + std::string fix_client_config; + + po::options_description desc("Allowed options"); + desc.add_options() + ("help,h", "produce help message") + ("port,p", po::value(), "set WebSocket listening port") + ("fix_client_config,f", po::value(), "set FIX client configuration file"); + + po::variables_map vm; + po::store(po::parse_command_line(argc, argv, desc), vm); + po::notify(vm); + + if (vm.count("help")) { + std::cout << desc << "\n"; + return 0; + } + + if (vm.count("port")) + port = vm["port"].as(); + + if (vm.count("fix_client_config")) + fix_client_config = vm["fix_client_config"].as(); + + std::cout << "Using port: " << port << "\n"; + if (!fix_client_config.empty()) + std::cout << "Using FIX client config file: " << fix_client_config << "\n"; + + + asio::io_context ioc{1}; // number of ws threads + std::make_shared(ioc, tcp::endpoint{address, port})->run(); + + std::cout << "WebSocket echo server listening on port " << port << "\n"; + ioc.run(); + } catch (const std::exception& ex) { + std::cerr << "Fatal: " << ex.what() << "\n"; + return EXIT_FAILURE; + } + return EXIT_SUCCESS; +} diff --git a/MiscClients/cpp_ws_reactjs/fix_ws_proxy/fixproxy.ini b/MiscClients/cpp_ws_reactjs/fix_ws_proxy/fixproxy.ini new file mode 100644 index 0000000..79605fe --- /dev/null +++ b/MiscClients/cpp_ws_reactjs/fix_ws_proxy/fixproxy.ini @@ -0,0 +1,17 @@ +[DEFAULT] +BeginString=FIX.4.4 +ConnectionType=initiator +ReconnectInterval=30 +FileStorePath=store +FileLogPath=log +StartTime=00:00:00 +EndTime=00:00:00 +ResetOnLogon=Y +ResetSeqNumFlag=Y +ResetOnDisconnect=Y + +TargetCompID=FIX_GWY_1 +SocketConnectHost=127.0.0.1 +SocketConnectPort=15001 +HeartBtInt=30 +DataDictionary=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 new file mode 100644 index 0000000..3e9d37f --- /dev/null +++ b/MiscClients/cpp_ws_reactjs/fix_ws_proxy/ws_server.cpp @@ -0,0 +1,78 @@ +// Copyright (C) Mike Kipnis - DistributedATS + +#include "ws_server.h" +#include "fix_application.h" + +#include +#include +#include + +#include +#include +#include + + +void Session::on_read(beast::error_code ec, std::size_t bytes_transferred) { + + boost::shared_lock lock(rw_mutex_); + + boost::ignore_unused(bytes_transferred); + if (ec == ws::error::closed) return; + if (ec) { + std::cerr << "read error: " << ec.message() << "\n"; + return; + } + + auto message = beast::buffers_to_string(buffer_.data()); + auto fix_message = distributed_ats::fix_json::json_to_fix(message); + auto ws_session_id = fix_message.getSessionID(); + + FIX::SessionID qualified_id( + ws_session_id.getBeginString(), + ws_session_id.getSenderCompID(), + ws_session_id.getTargetCompID(), + fix_session_qualifier_ + ); + + 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; + + storeFactory_ = std::make_shared(); + logFactory_ = std::make_shared( *settings_ ); + + std::cout << "FIX Message : " << fix_message << std::endl; + + application_ = std::make_shared(shared_from_this()); + socket_initator_ = std::make_shared( *application_, *storeFactory_, *settings_, *logFactory_ ); + + socket_initator_->start(); + replay["token"] = fix_session_qualifier_; + replay["success"] = true; + } else { + + std::cout << "About to send: " << fix_message << std::endl; + replay["token"] = fix_session_qualifier_; + + bool success = FIX::Session::sendToTarget(fix_message, qualified_id); + if (!success) + std::cerr << "Failed to send FIX message to " << qualified_id << std::endl; + + replay["success"] = success; + + } + + replay["data_type"] = "WS"; + std::string replay_str = boost::json::serialize(replay); + + // Send over WebSocket + ws_.text(true); // ensure it's sent as text + ws_.async_write( + boost::asio::buffer(replay_str), + beast::bind_front_handler(&Session::on_write, shared_from_this()) + ); +} diff --git a/MiscClients/cpp_ws_reactjs/fix_ws_proxy/ws_server.h b/MiscClients/cpp_ws_reactjs/fix_ws_proxy/ws_server.h new file mode 100644 index 0000000..37e5244 --- /dev/null +++ b/MiscClients/cpp_ws_reactjs/fix_ws_proxy/ws_server.h @@ -0,0 +1,216 @@ +// Copyright (C) Mike Kipnis - DistributedATS + +#pragma once + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include + +namespace asio = boost::asio; +namespace beast = boost::beast; +namespace http = beast::http; +namespace ws = beast::websocket; +using tcp = asio::ip::tcp; + + +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)) {} + + void start() { + // Accept the websocket handshake + ws_.async_accept( + beast::bind_front_handler(&Session::on_accept, shared_from_this())); + } + + ~Session() { + + boost::shared_lock lock(rw_mutex_); + try { + if (socket_initator_) { + socket_initator_->stop(); + socket_initator_.reset(); + } + } catch (std::exception const& e) { + std::cerr << "Exception in Session destructor: " << e.what() << std::endl; + } + } + +private: + ws::stream ws_; + beast::flat_buffer buffer_; + + std::string fix_session_qualifier_; + std::string ws_session_qualifier_; + + + std::shared_ptr settings_; + std::shared_ptr storeFactory_; + std::shared_ptr logFactory_; + + + std::shared_ptr application_; + std::shared_ptr socket_initator_; + + FIX::Message pending_logon_; + + boost::shared_mutex rw_mutex_; + + void on_accept(beast::error_code ec) { + if (ec) { + std::cerr << "accept error: " << ec.message() << "\n"; + return; + } + + auto& tcp_stream = ws_.next_layer(); + auto remote_endpoint = tcp_stream.remote_endpoint(); + auto local_endpoint = tcp_stream.local_endpoint(); + + ws_session_qualifier_ = + local_endpoint.address().to_string() + ":" + std::to_string(local_endpoint.port()) + + " <-> " + + remote_endpoint.address().to_string() + ":" + std::to_string(remote_endpoint.port()); + + boost::uuids::uuid token = boost::uuids::random_generator()(); + fix_session_qualifier_ = boost::uuids::to_string(token); + + do_read(); + } + + void do_read() { + buffer_.consume(buffer_.size()); // ensure buffer empty + ws_.async_read( + buffer_, + beast::bind_front_handler(&Session::on_read, shared_from_this())); + } + + void on_read(beast::error_code ec, std::size_t bytes_transferred); + + void on_write(beast::error_code ec, std::size_t bytes_transferred) { + boost::ignore_unused(bytes_transferred); + if (ec) { + std::cerr << "write error: " << ec.message() << "\n"; + return; + } + // Clear the buffer and read another message + buffer_.consume(buffer_.size()); + do_read(); + } + + void on_close() { + + boost::shared_lock lock(rw_mutex_); + + std::cout << "[WS] Connection closed: " << ws_session_qualifier_ << std::endl; + + // Gracefully stop the FIX initiator if running + if (socket_initator_) { + try { + socket_initator_->stop(); + socket_initator_.reset(); + } catch (std::exception const& e) { + std::cerr << "Error stopping FIX initiator: " << e.what() << std::endl; + } + } + } + +public: + + const FIX::Message& get_pending_logon() + { + return pending_logon_; + } + + void send_string(const std::string& message) { + + boost::unique_lock lock(rw_mutex_); + + asio::dispatch(ws_.get_executor(), [self = shared_from_this(), message]() { + try { + self->ws_.text(true); + self->ws_.write(boost::asio::buffer(message)); // synchronous write + } catch (const std::exception& e) { + std::cerr << "sync write error: " << e.what() << std::endl; + } + }); + + /* + auto self = shared_from_this(); // keep session alive during async write + ws_.text(true); // send as text + ws_.async_write( + boost::asio::buffer(message), + [self](beast::error_code ec, std::size_t bytes_transferred){ + boost::ignore_unused(bytes_transferred); + if (ec) { + std::cerr << "write error: " << ec.message() << "\n"; + } + } + );*/ + } +}; + + +// 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) { + beast::error_code ec; + + acceptor_.open(endpoint.protocol(), ec); + if (ec) throw beast::system_error(ec); + + acceptor_.set_option(asio::socket_base::reuse_address(true), ec); + if (ec) throw beast::system_error(ec); + + acceptor_.bind(endpoint, ec); + if (ec) throw beast::system_error(ec); + + acceptor_.listen(asio::socket_base::max_listen_connections, ec); + if (ec) throw beast::system_error(ec); + } + + void run() { + do_accept(); + } + +private: + asio::io_context& ioc_; + tcp::acceptor acceptor_; + + void do_accept() { + acceptor_.async_accept( + asio::make_strand(ioc_), + beast::bind_front_handler(&Listener::on_accept, shared_from_this())); + } + + void on_accept(beast::error_code ec, tcp::socket socket) { + if (ec) { + std::cerr << "accept failed: " << ec.message() << "\n"; + } else { + // Create and run session + std::make_shared(std::move(socket))->start(); + } + do_accept(); + } +}; + diff --git a/MiscClients/cpp_ws_reactjs/oms/oms.cpp b/MiscClients/cpp_ws_reactjs/oms/oms.cpp deleted file mode 100644 index 3dc2192..0000000 --- a/MiscClients/cpp_ws_reactjs/oms/oms.cpp +++ /dev/null @@ -1,6 +0,0 @@ -#include - -int main( int argc, char* argv[] ) -{ - return 0; -}; diff --git a/MiscClients/cpp_ws_reactjs/py_client/py_client.py b/MiscClients/cpp_ws_reactjs/py_client/py_client.py new file mode 100644 index 0000000..34376b3 --- /dev/null +++ b/MiscClients/cpp_ws_reactjs/py_client/py_client.py @@ -0,0 +1,145 @@ +# Copyright (C) Mike Kipnis - DistributedATS +import asyncio +import json +import argparse +from datetime import datetime, timezone + +import websockets + + +def compose_header(sender_comp_id: str, target_comp_id: str, msg_type: str): + return { + 'Header': { + '8': "FIX.4.4", + '35': msg_type, + '49': sender_comp_id, + '56': target_comp_id, + } + } + + +def compose_logon(sender_comp_id: str, target_comp_id: str): + logon = compose_header(sender_comp_id, target_comp_id, 'A') + logon['Body'] = {'553': sender_comp_id, '554': "TEST"} + return logon + + +def compose_security_list_request(sender_comp_id: str, target_comp_id: str): + req = compose_header(sender_comp_id, target_comp_id, 'x') + req['Body'] = {'320': 'RequestInstrList1', '559': 4} + return req + + +def compose_mass_order_status_request(sender_comp_id: str, target_comp_id: str): + req = compose_header(sender_comp_id, target_comp_id, 'AF') + req['Body'] = {'584': 'RequestMassOrdStatus1', '559': 7} + return req + + +def compose_market_data_request(sender_comp_id: str, target_comp_id: str, instruments: list): + req = compose_header(sender_comp_id, target_comp_id, 'V') + req['Body'] = { + '262': 'MarketDataSnapshot_1', + '263': 1, + '264': 0, + '146': [], + '267': [{"269": 0}, {"269": 1}], + } + + for instr in instruments: + req['Body']['146'].append({k: instr[k] for k in ["55", "207"] if k in instr}) + + return req + + +def compose_order_from_open_price(sender_comp_id: str, target_comp_id: str, snapshot: dict): + order = compose_header(sender_comp_id, target_comp_id, 'D') + order['Body'] = { + '11': f"{snapshot['Body']['55']}_1", + '55': snapshot['Body']['55'], + '207': snapshot['Body']['207'], + '40': 2, # Limit + '59': 0, # Time in force + '60': datetime.now(timezone.utc).strftime("%Y%m%d-%H:%M:%S.%f")[:-3], + } + + for entry in snapshot['Body'].get('268', []): + if entry.get('269') == '4': # Open price + order['Body'].update({'54': 1, '44': entry['270'], '38': 1000}) + break + + return order + + +def pretty_print(description: str, message: dict): + print(f"\n{description}:\n{json.dumps(message, indent=4)}") + + +async def run_fix_session(sender_comp_id: str, target_comp_id: str, ws_url: str): + async with websockets.connect(ws_url) as ws: + print(f"✅ Connected to {ws_url}") + + # Send logon + logon = compose_logon(sender_comp_id, target_comp_id) + await ws.send(json.dumps(logon)) + pretty_print("➡️ Sent Logon", logon) + + session_msg = await ws.recv() + pretty_print("⬅️ Session Qualifier", json.loads(session_msg)) + + try: + async for raw_msg in ws: + msg = json.loads(raw_msg) + + if msg.get("data_type") == "WS": + continue + + msg_type = msg.get('Header', {}).get('35') + + pretty_print("⬅️ Incoming Message", msg) + + if msg_type == 'A': # Logon Ack + req1 = compose_security_list_request(sender_comp_id, target_comp_id) + req2 = compose_mass_order_status_request(sender_comp_id, target_comp_id) + await ws.send(json.dumps(req1)) + await ws.send(json.dumps(req2)) + pretty_print("➡️ Sent Security List Request", req1) + pretty_print("➡️ Sent Mass Order Request", req2) + + elif msg_type == 'y': # Security List + instruments = msg['Body']['146'] + md_req = compose_market_data_request(sender_comp_id, target_comp_id, instruments) + await ws.send(json.dumps(md_req)) + pretty_print("➡️ Sent Market Data Request", md_req) + + elif msg_type == 'W': # Market Data Snapshot + pretty_print("📈 Market Data Snapshot", msg) + order = compose_order_from_open_price(sender_comp_id, target_comp_id, msg) + await ws.send(json.dumps(order)) + pretty_print("➡️ Sent Order", order) + + elif msg_type == 'X': + pretty_print("📊 Market Data Update", msg) + + elif msg_type == '8': + pretty_print("🧾 Execution Report", msg) + + except websockets.ConnectionClosed: + print("❌ Connection closed.") + except Exception as e: + print(f"⚠️ Error: {e}") + + +def main(): + parser = argparse.ArgumentParser(description="FIX over WebSocket client") + parser.add_argument("--sender", required=True, help="SenderCompID (e.g., TRADER_1)") + parser.add_argument("--target", required=True, help="TargetCompID (e.g., FIX_GWY_1)") + parser.add_argument("--url", required=True, help="WebSocket URL (e.g., ws://localhost:9002)") + + args = parser.parse_args() + + asyncio.run(run_fix_session(args.sender, args.target, args.url)) + + +if __name__ == "__main__": + main() diff --git a/MiscClients/cpp_ws_reactjs/webtrader_reactjs_ws/package.json b/MiscClients/cpp_ws_reactjs/webtrader_reactjs_ws/package.json new file mode 100644 index 0000000..c05b8ad --- /dev/null +++ b/MiscClients/cpp_ws_reactjs/webtrader_reactjs_ws/package.json @@ -0,0 +1,43 @@ +{ + "name": "web-trader", + "version": "0.1.0", + "private": true, + "dependencies": { + "@testing-library/jest-dom": "^5.16.4", + "@testing-library/react": "^13.2.0", + "@testing-library/user-event": "^13.5.0", + "ag-grid-community": "^27.3.0", + "ag-grid-react": "^27.3.0", + "bootstrap": "^5.2.2", + "rc-input-number": "^7.3.9", + "react": "^18.1.0", + "react-bootstrap": "^2.6.0", + "react-dom": "^18.1.0", + "react-scripts": "5.0.1", + "web-vitals": "^2.1.4" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test", + "eject": "react-scripts eject" + }, + "eslintConfig": { + "extends": [ + "react-app", + "react-app/jest" + ] + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + } +} diff --git a/MiscClients/cpp_ws_reactjs/webtrader_reactjs_ws/public/favicon.ico b/MiscClients/cpp_ws_reactjs/webtrader_reactjs_ws/public/favicon.ico new file mode 100644 index 0000000..a11777c Binary files /dev/null and b/MiscClients/cpp_ws_reactjs/webtrader_reactjs_ws/public/favicon.ico differ diff --git a/MiscClients/cpp_ws_reactjs/webtrader_reactjs_ws/public/index.html b/MiscClients/cpp_ws_reactjs/webtrader_reactjs_ws/public/index.html new file mode 100644 index 0000000..7478acb --- /dev/null +++ b/MiscClients/cpp_ws_reactjs/webtrader_reactjs_ws/public/index.html @@ -0,0 +1,43 @@ + + + + + + + + + + + + + DistributedATS - WebTrader + + + +
+ + + diff --git a/MiscClients/cpp_ws_reactjs/webtrader_reactjs_ws/public/logo192.png b/MiscClients/cpp_ws_reactjs/webtrader_reactjs_ws/public/logo192.png new file mode 100644 index 0000000..fc44b0a Binary files /dev/null and b/MiscClients/cpp_ws_reactjs/webtrader_reactjs_ws/public/logo192.png differ diff --git a/MiscClients/cpp_ws_reactjs/webtrader_reactjs_ws/public/logo512.png b/MiscClients/cpp_ws_reactjs/webtrader_reactjs_ws/public/logo512.png new file mode 100644 index 0000000..a4e47a6 Binary files /dev/null and b/MiscClients/cpp_ws_reactjs/webtrader_reactjs_ws/public/logo512.png differ diff --git a/MiscClients/cpp_ws_reactjs/webtrader_reactjs_ws/public/manifest.json b/MiscClients/cpp_ws_reactjs/webtrader_reactjs_ws/public/manifest.json new file mode 100644 index 0000000..080d6c7 --- /dev/null +++ b/MiscClients/cpp_ws_reactjs/webtrader_reactjs_ws/public/manifest.json @@ -0,0 +1,25 @@ +{ + "short_name": "React App", + "name": "Create React App Sample", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + }, + { + "src": "logo192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "logo512.png", + "type": "image/png", + "sizes": "512x512" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/MiscClients/cpp_ws_reactjs/webtrader_reactjs_ws/public/robots.txt b/MiscClients/cpp_ws_reactjs/webtrader_reactjs_ws/public/robots.txt new file mode 100644 index 0000000..e9e57dc --- /dev/null +++ b/MiscClients/cpp_ws_reactjs/webtrader_reactjs_ws/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/MiscClients/cpp_ws_reactjs/webtrader_reactjs_ws/src/App.css b/MiscClients/cpp_ws_reactjs/webtrader_reactjs_ws/src/App.css new file mode 100644 index 0000000..6441f6f --- /dev/null +++ b/MiscClients/cpp_ws_reactjs/webtrader_reactjs_ws/src/App.css @@ -0,0 +1,102 @@ +.App { + text-align: center; +} + +.App-logo { + height: 40vmin; + pointer-events: none; +} + +@media (prefers-reduced-motion: no-preference) { + .App-logo { + animation: App-logo-spin infinite 20s linear; + } +} + +.App-header { + background-color: #282c34; + min-height: 100vh; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + font-size: calc(10px + 2vmin); + color: white; +} + +.App-link { + color: #61dafb; +} + +@keyframes App-logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +.contaner +{ + margin: 0; + font-family: Inter, sans-serif; + height: 100vh; + background-color: #282D35; + color: white; + max-width: 100%; + width: 80%; +} + +.App-Row { + margin: auto; + width: 100%; + height: 30%; + display: inline-block; + vertical-align: top; +} + +.App-Column +{ + vertical-align: top; + width: 50%; + display: inline-block; + margin: auto; +} + +* { + box-sizing: border-box; +} + +.body { + margin: 10; + font-family: Inter, sans-serif; + height: 100vh; + background-color: #282D35; +} + +nav { + display: flex; + align-items: center; + background-color: #21222A; + height: 70px; + padding: 10px 10px; +} + + +.nav--logo_text, .nav--title { + margin: 20; +} + +.nav--logo_text { + margin-right: auto; + /* color: #61DAFB;*/ + color : white; + font-weight: 700; + font-size: 22px; +} + +.nav--title { + color: #DEEBF8; + font-weight: 600; +} diff --git a/MiscClients/cpp_ws_reactjs/webtrader_reactjs_ws/src/App.js b/MiscClients/cpp_ws_reactjs/webtrader_reactjs_ws/src/App.js new file mode 100644 index 0000000..e27334b --- /dev/null +++ b/MiscClients/cpp_ws_reactjs/webtrader_reactjs_ws/src/App.js @@ -0,0 +1,236 @@ + +// 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 WebSocketDemo from './components/WebSocketDemo'; + + +const { forwardRef, useRef, useImperativeHandle } = React; + +function App() +{ + const ticketRef = React.useRef(); + const histRef = React.useRef(); + const marketDataAndPositionsRef = React.useRef(); + + const url = window.location.href; + //const url = 'http://localhost:8080'; + + 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); + + + 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 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 => { + + var invalid_state = {}; + invalid_state["text"] = 'Unable to connect to the server'; + setLoginState(invalid_state); + + }); + } + + 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; + + current_ticket.current = ticket_data; + } + + setTicketState( ticket_data ); + + if ( ticket_data.instrumentData != null ) + { + var price_levels = helpers.get_price_levels(ticket_data.instrumentData); + setPriceLevels(price_levels); + } + } + + 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)}); + } + + useEffect(() => { + console.log("Hist Data : " + histData ); + },[histData]); + + useEffect(() => { + console.log("Order Cancel Data : " + orderCancelData );; + },[orderCancelData]); + + 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); + }); + } + + }, [sessionToken]); + + useEffect(() => { + + 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 { + + } + } else { + console.log("Session State is Null"); + } + + }, [sessionState]); + + return ( ); + + /* +
+
+ + +
+ + +
+ +
+
+ Click on the instrument to trade. +
+ + + + +
+
+ +
+
+
+
+
*/ +} + +export default App; diff --git a/MiscClients/cpp_ws_reactjs/webtrader_reactjs_ws/src/App.test.js b/MiscClients/cpp_ws_reactjs/webtrader_reactjs_ws/src/App.test.js new file mode 100644 index 0000000..1f03afe --- /dev/null +++ b/MiscClients/cpp_ws_reactjs/webtrader_reactjs_ws/src/App.test.js @@ -0,0 +1,8 @@ +import { render, screen } from '@testing-library/react'; +import App from './App'; + +test('renders learn react link', () => { + render(); + const linkElement = screen.getByText(/learn react/i); + expect(linkElement).toBeInTheDocument(); +}); 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 new file mode 100644 index 0000000..e49dafe --- /dev/null +++ b/MiscClients/cpp_ws_reactjs/webtrader_reactjs_ws/src/components/History.js @@ -0,0 +1,119 @@ +// DistributedATS - Mike Kipnis (c) 2022 +import { useCallback, useMemo, useEffect, useState } from 'react'; +import React from 'react'; +import {AgGridColumn, AgGridReact} from 'ag-grid-react'; +import helpers from './helpers'; + +import 'ag-grid-community/dist/styles/ag-grid.css'; +import 'ag-grid-community/dist/styles/ag-theme-balham-dark.css'; + +const { forwardRef, useRef, useImperativeHandle } = React; + +const History = React.forwardRef ((props, ref) => { + + + const HistMessageRenderer = (value) => { + + const invokeCancelMethod = () => { + props.orderCancelCallback(value); + }; + + return ( + + + + ); +}; + + const [histData, setHistData] = useState({}); + + const gridRef = useRef(); + + useImperativeHandle(ref, () => ({ + + update_history() + { + gridRef.current.api.refreshCells(); + } + + })); + + function price_formatter(params) + { + if (params.value == 0 ) + { + return ""; + } else { + return helpers.get_display_price(params.value, params.data.instrument.tickSize); + } + + }; + +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: '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: 'Actions', + field: 'value', + cellRenderer: params => { + if ( params.data.status == "New" || params.data.status == "Partially Filled") + return HistMessageRenderer(params.data); + else + return null; + }, + colId: 'params', + editable: false, + minWidth: 150, + }, + ]); + + const gridOptions = { + + rowSelection: 'single', + + onRowDataChanged: event => { + var defaultSortModel = [ + { colId: 'lastUpdateTime', sort: 'desc', sortIndex: 0 } + ]; + gridRef.current.columnApi.applyColumnState({ state: defaultSortModel }); + }, + + } + + + const onBtnExport = useCallback(() => { + + props.dataExportCallback(gridRef); + }, []); + + return ( +
+
+ + +
+
+ ); +}); + +export default History; 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 new file mode 100644 index 0000000..98fa1fc --- /dev/null +++ b/MiscClients/cpp_ws_reactjs/webtrader_reactjs_ws/src/components/Login.js @@ -0,0 +1,101 @@ +// DistributedATS - Mike Kipnis (c) 2022 + +import React from 'react'; +import { useEffect, useState } from 'react'; + +import { Container, Row, Col, Button, Form } from 'react-bootstrap/'; + + +const { forwardRef, useRef, useImperativeHandle } = React; + +const Login = React.forwardRef ((props, ref) => { + + + const [name, setName] = useState(''); + const [password, setPassword] = useState(''); + + const [submitted, setSubmitted] = useState(false); + const [error, setError] = useState(null); + const [sessionToken, setSessionToken] = useState(null); + const [sessionState, setSessionState] = useState(null); + + + const handleName = (e) => { + setName(e.target.value); + setSubmitted(false); + }; + + const handlePassword = (e) => { + setPassword(e.target.value); + setSubmitted(false); + }; + + const handleSubmit = (e) => { + e.preventDefault(); + if (name === '' || password === '') { + setError(true); + } else { + + var logon_value = {}; + logon_value["username"] = name; + logon_value["password"] = password; + + props.logonCallback(logon_value); + + setSubmitted(true); + setError(false); + } + }; + const successMessage = () => { + + return ( +
+ User {name} successfully registered!! + +
+ ); + + }; + + // Showing error message if error is true + const infoMessage = () => { + return ( +
+ {props.loginState.text} +
+ ); + }; + + return ( + + +

DistributedATS - WebTrader

+
{infoMessage()}
+ + +
+
+ + + + + + + +
+
+ +
+ ); +}); + +export default Login; 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 new file mode 100644 index 0000000..cc0d74d --- /dev/null +++ b/MiscClients/cpp_ws_reactjs/webtrader_reactjs_ws/src/components/PositionsAndMarketData.js @@ -0,0 +1,171 @@ +// DistributedATS - Mike Kipnis (c) 2022 + +import { useCallback, useMemo, useEffect, useState } from 'react'; +import React from 'react'; +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'; + +const { forwardRef, useRef, useImperativeHandle } = React; + +const Blotter = React.forwardRef ((props, ref) => { + + const [ticketData, setTicketData] = useState({}); + const [blotterData, setBlotterData] = useState([]); + + const gridRef = useRef(); + + function price_formatter(params) + { + if (params.value == 0 ) + { + return ""; + } else { + + return helpers.get_display_price(params.value, params.data.tickSize); + } + + }; + + function size_formatter(params) + { + if (params.value == 0 ) + { + return ""; + } + }; + + + useImperativeHandle(ref, () => ({ + update_data() { + + gridRef.current.api.forEachNode(node => + { + if (node.data == null ) + return; + + var instrument_name = node.data.instrumentName; + var last_market_data_sequence_number = node.data.marketDataSequenceNumber; + + var instrument_update = props.blotterData[instrument_name]; + + if ( instrument_update!== undefined && instrument_update!== null ) + { + if ( instrument_update.marketDataSequenceNumber > last_market_data_sequence_number ) + { + node.setData(instrument_update); + } + } + + + } + ); + + if (gridRef.current.api.getDisplayedRowCount() === 0 ) + { + setBlotterData(Object.values(props.blotterData)); + } + //gridRef.current.api.refreshCells(); + } + })); + +const [columnDefs, setColumnDefs] = useState([ + { headerName: 'symbol', field: 'symbol', filter: 'agTextColumnFilter', cellStyle: {'textAlign': 'left'}, sortable: true, }, + { headerName: 'Position', field: 'position', flex: 2, 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: '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'}}, + { headerName: 'PriceChange', field: 'priceChange', sortable: true, flex: 2 , valueFormatter:price_formatter, cellStyle: {'textAlign': 'right'}}, + { headerName: 'MaturityDate', field: 'maturityDate', sortable: true, flex: 2 , hide:true }, + { headerName: 'TickSize', field: 'tickSize', sortable: true, flex: 2, hide:true }, + + + ]); + + 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); + }, + + onColumnResized: event => {}, + + onRowDataChanged: event => { + var defaultSortModel = [ + { colId: 'maturityDate', sort: 'asc', sortIndex: 0 } + ]; + gridRef.current.columnApi.applyColumnState({ state: defaultSortModel }); + }, + + getRowHeight: (params) => 25 + } + + useEffect(() => { + + props.marketDataCallback(ticketData); + +}, [ticketData]); + + const onBtnExport = useCallback(() => { + + props.dataExportCallback(gridRef); + }, []); + + return ( +
+ + +
+ ); +}); + +export default Blotter; 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 new file mode 100644 index 0000000..f7be31e --- /dev/null +++ b/MiscClients/cpp_ws_reactjs/webtrader_reactjs_ws/src/components/PriceLevels.js @@ -0,0 +1,68 @@ +// DistributedATS - Mike Kipnis (c) 2022 +import { useCallback, useMemo, useEffect, useState } from 'react'; +import React from 'react'; +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'; + +const { forwardRef, useRef, useImperativeHandle } = React; + +const PriceLevels = React.forwardRef ((props, ref) => { + + + const [priceLevels, setPriceLevels] = useState({}); + + const gridRef = useRef(); + + function price_formatter(params) + { + if (params.value == 0 ) + { + return ""; + } else { + + return helpers.get_display_price(params.value, props.ticketState.tickSize); + } + + }; + + function size_formatter(params) + { + if (params.value == 0 ) + { + return ""; + } + }; + + + + useImperativeHandle(ref, () => ({ + + update_history() + { + gridRef.current.api.refreshCells(); + } + + })); + +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 }, + ]); + + + return ( +
+ + +
+ ); +}); + +export default PriceLevels; 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 new file mode 100644 index 0000000..f4b32d8 --- /dev/null +++ b/MiscClients/cpp_ws_reactjs/webtrader_reactjs_ws/src/components/SessionStateWrapper.js @@ -0,0 +1,165 @@ +// 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 new file mode 100644 index 0000000..0915f81 --- /dev/null +++ b/MiscClients/cpp_ws_reactjs/webtrader_reactjs_ws/src/components/Ticket.js @@ -0,0 +1,200 @@ +// DistributedATS - Mike Kipnis (c) 2022 +import React from 'react'; +import { useEffect, useState } from 'react'; +import { Container, Row, Col, Button, Form } from 'react-bootstrap/'; +import PriceLevels from './PriceLevels'; + + +const { forwardRef, useRef, useImperativeHandle } = React; + + +const Ticket = React.forwardRef ((props, ref) => { + + const [qty, setQty] = useState(1); + const [orderType, setOrderType] = useState('Limit'); + const [allOrNone, setAllOrNone] = useState(false); + const [orderCondition, setOrderCondition] = useState('Day'); + const [size, setSize] = useState(null); + const [orderSubmitResults, setOrderSubmitResults] = useState({}); + const [cancelAllResults, setCancelAllResults] = useState({}); + const [isTicketing, setIsTicketing] = useState(false); + + const [ticketPrice, setTicketPrice] = useState(0); + const [ticketStopPrice, setTicketStopPrice] = useState(0); + const [ticketSize, setTicketSize] = useState(0); + + const priceLevelsRef = React.useRef(); + + function submit_buy( e ) + { + e.preventDefault(); + props.ticketState.side = "BUY"; + Submit_order(props.ticketState); + } + + function submit_sell( e ) + { + e.preventDefault(); + props.ticketState.side = "SELL"; + Submit_order(props.ticketState); + } + + const Submit_order = ( trade ) => + { + 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 handleCancellAll = (e) => { + e.preventDefault(); + + setIsTicketing(true); + + var cancel_all = {}; + + cancel_all["username"] = props.ticketState.username; + cancel_all["token"] = props.ticketState.token; + + 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)); + }; + + useEffect(() => + { + setTicketPrice(props.ticketState.price); + setTicketStopPrice(props.ticketState.price); + setTicketSize(props.ticketState.quantity); + }, [props.ticketState.price]); + + useEffect(() => { + setIsTicketing(false); + console.log("Order Submit Results : " + orderSubmitResults ); + },[orderSubmitResults]); + + useEffect(() => { + setIsTicketing(false); + console.log("Cancel All Results : " + cancelAllResults );; + },[cancelAllResults]); + + return ( + + + +
Trade : {props.ticketState.instrumentName}
+ +
+ + + + +
+
+
+ + + + + + +
+ { + setTicketPrice(value.target.value); + } }/> +
+ + + { setTicketSize(value.target.value); } }/> + +
+ + + + + + + + + + + + + + + + + + +
+ { setTicketStopPrice(value.target.value); } }/> +
+ + + setAllOrNone(event.currentTarget.checked)}/> + +
+ + + + + + + + + + + +
+
+
+
+
+ ); +}); + +export default Ticket; 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 new file mode 100644 index 0000000..1c992c6 --- /dev/null +++ b/MiscClients/cpp_ws_reactjs/webtrader_reactjs_ws/src/components/helpers.js @@ -0,0 +1,60 @@ +const helpers = { + + dec_to_tic: function(dec_price) + { + var is_negative = ( dec_price < 0 ? true : false); + + dec_price = Math.abs(dec_price); + + var d_handle = dec_price|0; + var fraction_1 = dec_price - d_handle; + var f_handle = fraction_1*32|0; + var fraction_2 = (fraction_1-f_handle/32)*256|0; + + var price_str = d_handle.toString() + "-" + f_handle.toString().padStart(2,'0') + fraction_2.toString(); + + + return (is_negative == true ? "(" + price_str + ")" : price_str ); + }, + + get_display_price: function(price, tick_size) + { + if (tick_size == 256 ) + { + return helpers.dec_to_tic(price/tick_size); + } else { + if ( price != null ) + { + return price/tick_size; + } + } + }, + + get_price_levels: function(instrument_data) + { + let price_levels = []; + + for (let i = 0; i < 5; i++) + { + var level = {"bidPrice":0,"askPrice":0,"bidSize":0,"askSize":0}; + + if (instrument_data.bidSide[i]!=null) + { + level["bidPrice"] = instrument_data.bidSide[i].price; + level["bidSize"] = instrument_data.bidSide[i].size; + } + + if (instrument_data.askSide[i]!=null) + { + level["askPrice"] = instrument_data.askSide[i].price; + level["askSize"] = instrument_data.askSide[i].size; + } + + price_levels.push(level); + } + + return price_levels; + } +} + +export default helpers; diff --git a/MiscClients/cpp_ws_reactjs/webtrader_reactjs_ws/src/index.css b/MiscClients/cpp_ws_reactjs/webtrader_reactjs_ws/src/index.css new file mode 100644 index 0000000..ec2585e --- /dev/null +++ b/MiscClients/cpp_ws_reactjs/webtrader_reactjs_ws/src/index.css @@ -0,0 +1,13 @@ +body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', + 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', + sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +code { + font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', + monospace; +} diff --git a/MiscClients/cpp_ws_reactjs/webtrader_reactjs_ws/src/index.js b/MiscClients/cpp_ws_reactjs/webtrader_reactjs_ws/src/index.js new file mode 100644 index 0000000..d563c0f --- /dev/null +++ b/MiscClients/cpp_ws_reactjs/webtrader_reactjs_ws/src/index.js @@ -0,0 +1,17 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import './index.css'; +import App from './App'; +import reportWebVitals from './reportWebVitals'; + +const root = ReactDOM.createRoot(document.getElementById('root')); +root.render( + + + +); + +// If you want to start measuring performance in your app, pass a function +// to log results (for example: reportWebVitals(console.log)) +// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals +reportWebVitals(); diff --git a/MiscClients/cpp_ws_reactjs/webtrader_reactjs_ws/src/logo.svg b/MiscClients/cpp_ws_reactjs/webtrader_reactjs_ws/src/logo.svg new file mode 100644 index 0000000..9dfc1c0 --- /dev/null +++ b/MiscClients/cpp_ws_reactjs/webtrader_reactjs_ws/src/logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/MiscClients/cpp_ws_reactjs/webtrader_reactjs_ws/src/reportWebVitals.js b/MiscClients/cpp_ws_reactjs/webtrader_reactjs_ws/src/reportWebVitals.js new file mode 100644 index 0000000..5253d3a --- /dev/null +++ b/MiscClients/cpp_ws_reactjs/webtrader_reactjs_ws/src/reportWebVitals.js @@ -0,0 +1,13 @@ +const reportWebVitals = onPerfEntry => { + if (onPerfEntry && onPerfEntry instanceof Function) { + import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { + getCLS(onPerfEntry); + getFID(onPerfEntry); + getFCP(onPerfEntry); + getLCP(onPerfEntry); + getTTFB(onPerfEntry); + }); + } +}; + +export default reportWebVitals; diff --git a/MiscClients/cpp_ws_reactjs/webtrader_reactjs_ws/src/setupTests.js b/MiscClients/cpp_ws_reactjs/webtrader_reactjs_ws/src/setupTests.js new file mode 100644 index 0000000..8f2609b --- /dev/null +++ b/MiscClients/cpp_ws_reactjs/webtrader_reactjs_ws/src/setupTests.js @@ -0,0 +1,5 @@ +// jest-dom adds custom jest matchers for asserting on DOM nodes. +// allows you to do things like: +// expect(element).toHaveTextContent(/react/i) +// learn more: https://github.com/testing-library/jest-dom +import '@testing-library/jest-dom'; diff --git a/build_with_cmake.sh b/build_with_cmake.sh index 724ff5f..5ee015d 100755 --- a/build_with_cmake.sh +++ b/build_with_cmake.sh @@ -6,7 +6,7 @@ set -ex # Print each command and exit on error OS="$(uname)" if [[ "$OS" == "Darwin" ]]; then LIB_PATH_VAR="DYLD_LIBRARY_PATH" -# CMAKE_FLAGS="-G Xcode" + #CMAKE_FLAGS="-G Xcode" else LIB_PATH_VAR="LD_LIBRARY_PATH" fi