diff --git a/.github/workflows/ci-python.yml b/.github/workflows/ci-python.yml index 9c2cf277..fd56a448 100644 --- a/.github/workflows/ci-python.yml +++ b/.github/workflows/ci-python.yml @@ -25,7 +25,7 @@ jobs: fetch-depth: 0 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} @@ -36,7 +36,7 @@ jobs: cache: "gradle" - name: Install poetry - uses: abatilo/actions-poetry@v2 + uses: abatilo/actions-poetry@v3 # Cache node_modules - uses: actions/setup-node@v4 diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index bef6c698..48cfe811 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -56,7 +56,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -69,7 +69,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v2 + uses: github/codeql-action/autobuild@v3 # ℹī¸ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun @@ -82,6 +82,6 @@ jobs: # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/release-on-push.yml b/.github/workflows/release-on-push.yml index 46f80c95..71a28c8b 100644 --- a/.github/workflows/release-on-push.yml +++ b/.github/workflows/release-on-push.yml @@ -35,7 +35,7 @@ jobs: run: | mkdir -p ./version echo "${{ needs.release-on-push.outputs.version }}" > ./version/version_number - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 with: name: version_number path: ./version diff --git a/README.md b/README.md index 6f0a92c4..f20b4959 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,8 @@ +![GitHub Release](https://img.shields.io/github/v/release/SamuelGuillemet/pfe-asr-broker?label=Version) [![CodeQL](https://github.com/SamuelGuillemet/pfe-asr-broker/actions/workflows/codeql.yml/badge.svg)](https://github.com/SamuelGuillemet/pfe-asr-broker/actions/workflows/codeql.yml) -[![CI](https://github.com/SamuelGuillemet/pfe-asr-broker/actions/workflows/ci.yml/badge.svg)](https://github.com/SamuelGuillemet/pfe-asr-broker/actions/workflows/ci.yml) +[![CI - Java](https://github.com/SamuelGuillemet/pfe-asr-broker/actions/workflows/ci-java.yml/badge.svg)](https://github.com/SamuelGuillemet/pfe-asr-broker/actions/workflows/ci-java.yml) +[![CI - Python](https://github.com/SamuelGuillemet/pfe-asr-broker/actions/workflows/ci-python.yml/badge.svg)](https://github.com/SamuelGuillemet/pfe-asr-broker/actions/workflows/ci-python.yml) + # PFE Asr Broker diff --git a/clients/quickfix-client/.flake8 b/clients/quickfix-client/.flake8 new file mode 100644 index 00000000..fe92a0ea --- /dev/null +++ b/clients/quickfix-client/.flake8 @@ -0,0 +1,7 @@ +[flake8] +max-line-length = 120 + +extend-exclude = + .git + .venv + data \ No newline at end of file diff --git a/clients/quickfix-client/.gitignore b/clients/quickfix-client/.gitignore new file mode 100644 index 00000000..fa8d6c71 --- /dev/null +++ b/clients/quickfix-client/.gitignore @@ -0,0 +1,7 @@ +.venv/ +# .vscode/ +data/* +!data/.gitkeep +**/__pycache__/ +storage/** +latency*.txt diff --git a/clients/quickfix-client/.pylintrc b/clients/quickfix-client/.pylintrc new file mode 100644 index 00000000..39ec902f --- /dev/null +++ b/clients/quickfix-client/.pylintrc @@ -0,0 +1,9 @@ +[MASTER] +init-hook='import sys; sys.path.append(".")' + +[MESSAGES CONTROL] +disable=missing-module-docstring, missing-function-docstring, missing-class-docstring, too-few-public-methods, too-many-arguments, too-many-instance-attributes, too-many-locals, logging-fstring-interpolation, redefined-builtin, arguments-renamed + +[FORMAT] +good-names=i,j,k,ex,Run,_,pk,x,y,e,f +max-line-length = 120 diff --git a/clients/quickfix-client/.vscode/launch.json b/clients/quickfix-client/.vscode/launch.json new file mode 100644 index 00000000..d4daf9d3 --- /dev/null +++ b/clients/quickfix-client/.vscode/launch.json @@ -0,0 +1,19 @@ +{ + "configurations": [ + { + "name": "Python: File", + "type": "python", + "request": "launch", + "program": "${file}", + "justMyCode": true + }, + { + "name": "Main", + "type": "python", + "request": "launch", + "program": "${workspaceFolder}/broker_quickfix_client/main.py", + "console": "integratedTerminal", + "justMyCode": true + } + ] +} diff --git a/clients/quickfix-client/.vscode/settings.json b/clients/quickfix-client/.vscode/settings.json new file mode 100644 index 00000000..8ebb7329 --- /dev/null +++ b/clients/quickfix-client/.vscode/settings.json @@ -0,0 +1,36 @@ +{ + "files.exclude": { + "**/.git": true, + "**/.svn": true, + "**/.hg": true, + "**/CVS": true, + "**/.DS_Store": true, + "**/Thumbs.db": true, + "**/__pycache__": true, + "**/.pytest_cache": true, + "**/.mypy_cache": true, + "**/.mypy*": true, + "**/.venv": true, + "**/storage/": true, + }, + "python.analysis.autoImportCompletions": true, + "python.analysis.typeCheckingMode": "basic", + "python.analysis.inlayHints.callArgumentNames": "partial", + "python.analysis.inlayHints.variableTypes": true, + "python.analysis.inlayHints.functionReturnTypes": true, + "editor.formatOnSave": true, + "isort.check": true, + "isort.args": ["--profile", "black"], + "[python]": { + "editor.codeActionsOnSave": { + "source.organizeImports": "explicit", + "source.fixAll": "explicit" + }, + "editor.defaultFormatter": "ms-python.black-formatter", + "editor.codeLens": true + }, + "search.exclude": { + "**/dist": true, + "**/.venv": true + } +} diff --git a/clients/quickfix-client/README.md b/clients/quickfix-client/README.md new file mode 100644 index 00000000..236b2930 --- /dev/null +++ b/clients/quickfix-client/README.md @@ -0,0 +1,116 @@ +# Quickfix client + +This repository comprises a Python-based client application designed to interact with a broker's system using the QuickFIX library. The architecture is structured as follows: + +## Global Architecture Overview + +### Entry Point +- **main.py**: This file serves as the entry point of the application. It initializes and starts the main functionality. + +### Functionality Modules +- **application.py**: Contains the core logic of the quickfix application. +- **constant.py**: Stores constants used throughout the application. +- **decorators.py**: Defines decorators used within the application. + +### Handlers +- **handlers/**: This directory holds modules responsible for handling specific message types. `execution_report.py` focuses on handling execution reports from the broker's system. + +### Utility Modules +- **utils/**: Houses utility modules utilized across the application: + - `loader.py`: Handles loading operations. + - `logger.py`: Provides logging functionality. + - `quickfix.py`: Offers utility functions for interfacing with the QuickFIX library. + +### Wrappers +- **wrappers/**: Encapsulates and extends functionality from the QuickFIX library: + - `enums.py`: Houses enumerations and constants related to QuickFIX. + - `execution_report.py`: Wrapper dedicated to store execution reports as python objects. + - `new_order_single.py`: Wrapper facilitating the handling of new single orders, for example its creation and storage as a python object. + +The architecture follows a modular approach, segregating functionalities into discrete modules and directories. It aims to streamline interactions with the QuickFIX library and broker system by providing wrappers and utilities while maintaining clear separation of concerns. + + +## Specifications + +In order to interact flawlessly with the quickfix application, you can define callback functions when instantiating the application. These callbacks are defined as follows: + +```python +execution_handler = ExecutionReportHandler( + on_filled_report=lambda report: logger.info(f"Filled: {report}"), + on_rejected_report=lambda report: logger.info(f"Rejected: {report}"), +) +application, initiator = setup() +application.set_execution_report_handler(execution_handler) +``` + +Callbacks function should take as input a python object representing the execution report. For example we have: + +```python +@dataclass +class FilledExecutionReport: + order_id: int + client_order_id: int + symbol: str + side: SideEnum + type: OrderTypeEnum + leaves_quantity: int + price: float + cum_quantity: int + + +@dataclass +class RejectedExecutionReport: + order_id: int + client_order_id: int + symbol: str + side: SideEnum + type: OrderTypeEnum + leaves_quantity: int + reject_reason: OrderRejectReasonEnum +``` + +In this example, we define two callbacks, one for filled reports and one for rejected reports. These callbacks are called whenever the application receives a filled or rejected report from the broker's system. + +To send new orders, you can use the following function: + +```python +order = NewOrderSingle.new_market_order(0, SideEnum.SELL, 1, "ACGL") +application.send(order) +``` + +If you want to store the order as a python object, you can use the following function: + +```python +python_order = order.get_order() +``` + +This function returns a `Order` dataclass object, which contains the order as a python object. This object is defined as follows: + +```python +@dataclass +class Order: + order_id: int | None + client_order_id: int + symbol: str + side: SideEnum + price: float | None + quantity: int +``` + +--- + +## Installation + +To install the client, you need to run the following commands: + +```bash +$ poetry install +``` + +## Running the client + +To run the client, you need to run the following command: + +```bash +(.venv) $ python broker_quickfix_client/main.py +``` diff --git a/clients/quickfix-client/broker_quickfix_client/__init__.py b/clients/quickfix-client/broker_quickfix_client/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/clients/quickfix-client/broker_quickfix_client/application.py b/clients/quickfix-client/broker_quickfix_client/application.py new file mode 100644 index 00000000..f6b8886c --- /dev/null +++ b/clients/quickfix-client/broker_quickfix_client/application.py @@ -0,0 +1,130 @@ +# pylint: disable=unused-argument,invalid-name,super-init-not-called + +import logging +from time import sleep + +from quickfix import ( + Application, + FileStoreFactory, + Message, + MsgType, + MsgType_ExecutionReport, + MsgType_Logon, + MsgType_MarketDataSnapshotFullRefresh, + MsgType_OrderCancelReject, + Password, + Session, + SessionID, + SocketInitiator, + Username, +) + +from broker_quickfix_client.handlers.execution_report import ExecutionReportHandler +from broker_quickfix_client.handlers.order_cancel_reject import OrderCancelRejectHandler +from broker_quickfix_client.utils.logger import setup_logs +from broker_quickfix_client.utils.quickfix import log_quick_fix_message, set_settings + +logger = logging.getLogger("client.application") + + +class ClientApplication(Application): + session_id: SessionID | None = None + + execution_report_handler = ExecutionReportHandler() + order_cancel_reject_handler = OrderCancelRejectHandler() + + username: str | None = None + password: str | None = None + + def set_execution_report_handler( + self, execution_report_handler: ExecutionReportHandler + ): + self.execution_report_handler = execution_report_handler + + def set_order_cancel_reject_handler( + self, order_cancel_reject_handler: OrderCancelRejectHandler + ): + self.order_cancel_reject_handler = order_cancel_reject_handler + + def onCreate(self, sessionId: SessionID): + pass + + def onLogon(self, sessionId: SessionID): + self.session_id = sessionId + + def onLogout(self, sessionId: SessionID): + pass + + def toAdmin(self, message: Message, sessionId: SessionID): + log_quick_fix_message(message, "Sending") + if message.getHeader().getField(MsgType()).getString() == MsgType_Logon: + message.setField(Username(self.username)) + message.setField(Password(self.password)) + + def toApp(self, message: Message, sessionId: SessionID): + log_quick_fix_message(message, "Sending", logging.INFO) + + def fromAdmin(self, message: Message, sessionId: SessionID): + log_quick_fix_message(message, "Received") + + def fromApp(self, message: Message, sessionId: SessionID): + log_quick_fix_message(message, "Received", logging.INFO) + + msg_type = message.getHeader().getField(MsgType()).getString() + + if msg_type == MsgType_ExecutionReport: + self.execution_report_handler.handle_execution_report(message) + elif msg_type == MsgType_OrderCancelReject: + self.order_cancel_reject_handler.handle_order_cancel_reject(message) + elif msg_type == MsgType_MarketDataSnapshotFullRefresh: + logger.info("Market data snapshot full refresh received") + else: + logger.warning(f"Unknown message type: {msg_type}") + + def send(self, message: Message): + return Session.sendToTarget(message, self.session_id) + + def get_session_id(self): + return self.session_id + + def set_credentials(self, username, password): + self.username = username + self.password = password + + +def build_initiator(username: str, application: ClientApplication) -> SocketInitiator: + settings = set_settings(username) + store_factory = FileStoreFactory(settings) + initiator = SocketInitiator(application, store_factory, settings) + return initiator + + +def start_initiator(initiator: SocketInitiator, application: ClientApplication): + if not application.username or not application.password: + raise ValueError("Username and password must be set before starting initiator") + + initiator.start() + # Wait for the session to logon before sending messages. + while not application.get_session_id(): + sleep(0.1) + + +def setup( + username: str, + password: str, + execution_report_handler: ExecutionReportHandler | None = None, + order_cancel_reject_handler: OrderCancelRejectHandler | None = None, +): + setup_logs("client") + setup_logs("quickfix") + application = ClientApplication() + + application.set_credentials(username, password) + if execution_report_handler: + application.set_execution_report_handler(execution_report_handler) + if order_cancel_reject_handler: + application.set_order_cancel_reject_handler(order_cancel_reject_handler) + + initiator = build_initiator(username, application) + start_initiator(initiator, application) + return application, initiator diff --git a/clients/quickfix-client/broker_quickfix_client/constant.py b/clients/quickfix-client/broker_quickfix_client/constant.py new file mode 100644 index 00000000..516df6c4 --- /dev/null +++ b/clients/quickfix-client/broker_quickfix_client/constant.py @@ -0,0 +1,11 @@ +from quickfix import DataDictionary + +from broker_quickfix_client.utils.loader import get_config_dir + +DATA_DICTIONNARY: DataDictionary = DataDictionary(str(get_config_dir() / "FIX44.xml")) + +SERVER_IP = "127.0.0.1" + +SERVER_PORT = 5001 + +SERVER_NAME = "SERVER" diff --git a/clients/quickfix-client/broker_quickfix_client/decorators.py b/clients/quickfix-client/broker_quickfix_client/decorators.py new file mode 100644 index 00000000..23508deb --- /dev/null +++ b/clients/quickfix-client/broker_quickfix_client/decorators.py @@ -0,0 +1,60 @@ +import logging +import time +from typing import List + +logger = logging.getLogger("broker_quickfix_client.decorators") + + +def performance_timer_decorator(context_args: List[str] | None = None, disable=False): + """ + Decorator to time the execution of a function + + Args: + context_args (List[str], optional): A list of arguments to print in the context. Defaults to None. + disable (bool, optional): Disable the decorator. Defaults to False. + + Returns: + function: The decorated function + """ + + def decorator(func): + def wrapper(*args, **kwargs): + start_time = time.perf_counter() + result = func(*args, **kwargs) + end_time = time.perf_counter() + execution_time = end_time - start_time + + if disable: + return result + + context = "" + for arg in context_args or []: + context += str(kwargs[arg]) + " " + + if context is not None: + logger.debug( + f"{context} - {func.__name__} took {execution_time:.6f} seconds to execute" + ) + else: + logger.debug( + f"{func.__name__} took {execution_time:.6f} seconds to execute" + ) + return result + + return wrapper + + return decorator + + +# Add a decorator which return a default value specified as a parameter if the function throws an exception +def default_return_value_decorator(default_return_value: str | None): + def decorator(func): + def wrapper(*args, **kwargs): + try: + return func(*args, **kwargs) + except Exception: # pylint: disable=broad-exception-caught + return default_return_value + + return wrapper + + return decorator diff --git a/clients/quickfix-client/broker_quickfix_client/handlers/__init__.py b/clients/quickfix-client/broker_quickfix_client/handlers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/clients/quickfix-client/broker_quickfix_client/handlers/execution_report.py b/clients/quickfix-client/broker_quickfix_client/handlers/execution_report.py new file mode 100644 index 00000000..28a1ed5d --- /dev/null +++ b/clients/quickfix-client/broker_quickfix_client/handlers/execution_report.py @@ -0,0 +1,151 @@ +import logging +from typing import Callable, TypeVar + +import quickfix as fix + +from broker_quickfix_client.utils.quickfix import get_message_field +from broker_quickfix_client.wrappers.enums import ( + ExecTypeEnum, + OrderRejectReasonEnum, + OrderStatusEnum, + OrderTypeEnum, + SideEnum, +) +from broker_quickfix_client.wrappers.execution_report import ( + AcceptedOrderExecutionReport, + BaseExecutionReport, + CanceledOrderExecutionReport, + FilledExecutionReport, + RejectedExecutionReport, + ReplacedOrderExecutionReport, +) + +logger = logging.getLogger("client.execution_report_handler") + +T = TypeVar("T", bound=BaseExecutionReport) +CallbackType = Callable[[T], None] | None + + +class ExecutionReportHandler: + def __init__( + self, + order_filled_callback: CallbackType[FilledExecutionReport] = None, + order_rejected_callback: CallbackType[RejectedExecutionReport] = None, + order_accepted_callback: CallbackType[AcceptedOrderExecutionReport] = None, + order_replaced_callback: CallbackType[ReplacedOrderExecutionReport] = None, + order_canceled_callback: CallbackType[CanceledOrderExecutionReport] = None, + ): + self.order_filled_callback = order_filled_callback + self.order_rejected_callback = order_rejected_callback + self.order_accepted_callback = order_accepted_callback + self.order_replaced_callback = order_replaced_callback + self.order_canceled_callback = order_canceled_callback + + def handle_execution_report(self, execution_report: fix.Message): + ord_status = OrderStatusEnum(get_message_field(execution_report, fix.OrdStatus)) + exec_type = ExecTypeEnum(get_message_field(execution_report, fix.ExecType)) + + if ( + ord_status == OrderStatusEnum.REJECTED + and exec_type == ExecTypeEnum.REJECTED + ): + self._handle_order_rejected(execution_report) + elif ord_status == OrderStatusEnum.FILLED and exec_type == ExecTypeEnum.TRADE: + self._handle_order_filled(execution_report) + elif ord_status == OrderStatusEnum.NEW and exec_type == ExecTypeEnum.NEW: + self._handle_order_accepted(execution_report) + elif ord_status == OrderStatusEnum.NEW and exec_type == ExecTypeEnum.REPLACED: + self._handle_order_replaced(execution_report) + elif ( + ord_status == OrderStatusEnum.CANCELED + and exec_type == ExecTypeEnum.CANCELED + ): + self._handle_order_canceled(execution_report) + else: + logger.warning(f"Unsupported execution report for {ord_status} {exec_type}") + + def _extract_common_fields(self, execution_report): + return { + "order_id": int(get_message_field(execution_report, fix.OrderID)), + "client_order_id": int(get_message_field(execution_report, fix.ClOrdID)), + "symbol": get_message_field(execution_report, fix.Symbol), + "side": SideEnum(get_message_field(execution_report, fix.Side)), + "type": OrderTypeEnum(get_message_field(execution_report, fix.OrdType)), + "leaves_quantity": int(get_message_field(execution_report, fix.LeavesQty)), + } + + def _handle_order_rejected(self, execution_report: fix.Message): + common_fields = self._extract_common_fields(execution_report) + reject_reason = OrderRejectReasonEnum( + get_message_field(execution_report, fix.OrdRejReason) + ) + + rejected = RejectedExecutionReport( + **common_fields, + reject_reason=reject_reason, + ) + + if self.order_rejected_callback: + self.order_rejected_callback(rejected) + else: + logger.debug(f"Order rejected: {rejected}") + + def _handle_order_filled(self, execution_report: fix.Message): + common_fields = self._extract_common_fields(execution_report) + price = get_message_field(execution_report, fix.AvgPx) + cum_quantity = get_message_field(execution_report, fix.CumQty) + + filled = FilledExecutionReport( + **common_fields, + price=float(price), + cum_quantity=int(cum_quantity), + ) + if self.order_filled_callback: + self.order_filled_callback(filled) + else: + logger.debug(f"Order filled: {filled}") + + def _handle_order_accepted(self, execution_report: fix.Message): + common_fields = self._extract_common_fields(execution_report) + price = get_message_field(execution_report, fix.AvgPx) + + accepted = AcceptedOrderExecutionReport( + **common_fields, + price=float(price), + ) + + if self.order_accepted_callback: + self.order_accepted_callback(accepted) + else: + logger.debug(f"Order accepted: {accepted}") + + def _handle_order_replaced(self, execution_report: fix.Message): + common_fields = self._extract_common_fields(execution_report) + price = get_message_field(execution_report, fix.AvgPx) + original_client_order_id = get_message_field(execution_report, fix.OrigClOrdID) + + replaced = ReplacedOrderExecutionReport( + **common_fields, + price=float(price), + original_client_order_id=int(original_client_order_id), + ) + + if self.order_replaced_callback: + self.order_replaced_callback(replaced) + else: + logger.debug(f"Order replaced: {replaced}") + + def _handle_order_canceled(self, execution_report: fix.Message): + common_fields = self._extract_common_fields(execution_report) + original_client_order_id = get_message_field(execution_report, fix.OrigClOrdID) + + canceled = CanceledOrderExecutionReport( + **common_fields, + original_client_order_id=int(original_client_order_id), + price=0, + ) + + if self.order_canceled_callback: + self.order_canceled_callback(canceled) + else: + logger.debug(f"Order canceled: {canceled}") diff --git a/clients/quickfix-client/broker_quickfix_client/handlers/order_cancel_reject.py b/clients/quickfix-client/broker_quickfix_client/handlers/order_cancel_reject.py new file mode 100644 index 00000000..6eb6fa97 --- /dev/null +++ b/clients/quickfix-client/broker_quickfix_client/handlers/order_cancel_reject.py @@ -0,0 +1,71 @@ +import logging +from typing import Callable, TypeVar + +import quickfix as fix + +from broker_quickfix_client.utils.quickfix import get_message_field +from broker_quickfix_client.wrappers.enums import CxlRejResponseToEnum +from broker_quickfix_client.wrappers.order_cancel_reject import OrderCancelReject + +logger = logging.getLogger("client.order_cancel_reject_handler") + +T = TypeVar("T", bound=OrderCancelReject) +CallbackType = Callable[[T], None] | None + + +class OrderCancelRejectHandler: + def __init__( + self, + order_cancel_rejected_callback: CallbackType[OrderCancelReject] = None, + order_cancel_replace_rejected_callback: CallbackType[OrderCancelReject] = None, + ): + self.order_cancel_rejected_callback = order_cancel_rejected_callback + self.order_cancel_replace_rejected_callback = ( + order_cancel_replace_rejected_callback + ) + + def handle_order_cancel_reject(self, order_cancel_reject: fix.Message): + cancel_reject_response_to = CxlRejResponseToEnum( + get_message_field(order_cancel_reject, fix.CxlRejResponseTo) + ) + + if cancel_reject_response_to == CxlRejResponseToEnum.ORDER_CANCEL_REQUEST: + self._handle_order_cancel_rejected(order_cancel_reject) + elif ( + cancel_reject_response_to + == CxlRejResponseToEnum.ORDER_CANCEL_REPLACE_REQUEST + ): + self._handle_order_cancel_replace_rejected(order_cancel_reject) + else: + raise ValueError( + f"Unknown cancel reject response to: {cancel_reject_response_to}" + ) + + def _extract_common_fields(self, order_cancel_reject: fix.Message) -> dict: + return { + "order_id": int(get_message_field(order_cancel_reject, fix.OrderID)), + "client_order_id": int(get_message_field(order_cancel_reject, fix.ClOrdID)), + "original_client_order_id": int( + get_message_field(order_cancel_reject, fix.OrigClOrdID) + ), + } + + def _handle_order_cancel_rejected(self, order_cancel_reject: fix.Message): + common_fields = self._extract_common_fields(order_cancel_reject) + + rejected = OrderCancelReject(**common_fields) + + if self.order_cancel_rejected_callback: + self.order_cancel_rejected_callback(rejected) + else: + logger.warning(f"Order book modification rejected: {rejected}") + + def _handle_order_cancel_replace_rejected(self, order_cancel_reject: fix.Message): + common_fields = self._extract_common_fields(order_cancel_reject) + + rejected = OrderCancelReject(**common_fields) + + if self.order_cancel_replace_rejected_callback: + self.order_cancel_replace_rejected_callback(rejected) + else: + logger.warning(f"Order book modification rejected: {rejected}") diff --git a/clients/quickfix-client/broker_quickfix_client/main.py b/clients/quickfix-client/broker_quickfix_client/main.py new file mode 100644 index 00000000..c82f1042 --- /dev/null +++ b/clients/quickfix-client/broker_quickfix_client/main.py @@ -0,0 +1,39 @@ +import argparse +import faulthandler +import logging + +from broker_quickfix_client.application import setup +from broker_quickfix_client.runners.latency import latency_test +from broker_quickfix_client.runners.order_book import order_book_test + +logger = logging.getLogger("client") + +faulthandler.enable() + + +def main(username: str, password: str): + application, initiator = setup(username, password) + latency_test(username, application) + order_book_test(application) + initiator.stop() + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Customize parameters") + + parser.add_argument( + "--username", + type=str, + default="user1", + help="The username to use to connect to the server", + ) + parser.add_argument( + "--password", + type=str, + default="password", + help="The password to use to connect to the server", + ) + + args = parser.parse_args() + + main(args.username, args.password) diff --git a/clients/quickfix-client/broker_quickfix_client/runners/__init__.py b/clients/quickfix-client/broker_quickfix_client/runners/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/clients/quickfix-client/broker_quickfix_client/runners/latency.py b/clients/quickfix-client/broker_quickfix_client/runners/latency.py new file mode 100644 index 00000000..34a89c84 --- /dev/null +++ b/clients/quickfix-client/broker_quickfix_client/runners/latency.py @@ -0,0 +1,68 @@ +import logging +import time +from time import sleep + +from broker_quickfix_client.application import ClientApplication +from broker_quickfix_client.handlers.execution_report import ExecutionReportHandler +from broker_quickfix_client.wrappers.enums import SideEnum +from broker_quickfix_client.wrappers.new_order_single import NewOrderSingle + +logger = logging.getLogger("client") + + +def latency_test(username: str, application: ClientApplication): + order_time_map: dict[int, list[float]] = {} + cl_ord_id = 0 + nb_rejected = 0 + + def on_filled_report(report): + order_time_map[report.client_order_id].append(time.time()) + logger.info(f"Filled: {report}") + + def on_rejected_report(report): + logger.warning(f"Rejected: {report}") + nonlocal nb_rejected + nb_rejected += 1 + + execution_handler = ExecutionReportHandler( + order_filled_callback=on_filled_report, + order_rejected_callback=on_rejected_report, + ) + application.set_execution_report_handler(execution_handler) + + nb_orders = 20 + + for _ in range(nb_orders): + order = NewOrderSingle.new_market_order(cl_ord_id, SideEnum.BUY, 1, "ACGL") + application.send(order) + order_time_map[cl_ord_id] = [time.time()] + cl_ord_id += 1 + sleep(0.1) + for _ in range(nb_orders): + order = NewOrderSingle.new_market_order(cl_ord_id, SideEnum.SELL, 1, "ACGL") + application.send(order) + order_time_map[cl_ord_id] = [time.time()] + cl_ord_id += 1 + sleep(0.1) + + sleep(5) + + # Print avg latency for each order + sum_latency = 0.0 + latency_list: list[float] = [] + try: + for _, order_time_array in order_time_map.items(): + if len(order_time_array) != 2: + continue + latency = order_time_array[1] - order_time_array[0] + sum_latency += latency + latency_list.append(latency) + except Exception as e: + print(e) + print(order_time_map) + + # Save latency list to file + with open(f"latency-{username}.txt", "w", encoding="utf-8") as f: + for item in latency_list: + f.write(f"{item}\n") + print(f"Average latency: {sum_latency/len(latency_list)} seconds") diff --git a/clients/quickfix-client/broker_quickfix_client/runners/order_book.py b/clients/quickfix-client/broker_quickfix_client/runners/order_book.py new file mode 100644 index 00000000..fa825bf4 --- /dev/null +++ b/clients/quickfix-client/broker_quickfix_client/runners/order_book.py @@ -0,0 +1,76 @@ +import logging +from copy import deepcopy +from time import sleep + +from broker_quickfix_client.application import ClientApplication +from broker_quickfix_client.handlers.execution_report import ExecutionReportHandler +from broker_quickfix_client.wrappers.enums import SideEnum +from broker_quickfix_client.wrappers.execution_report import ( + AcceptedOrderExecutionReport, + CanceledOrderExecutionReport, + ReplacedOrderExecutionReport, +) +from broker_quickfix_client.wrappers.new_order_single import NewOrderSingle +from broker_quickfix_client.wrappers.order import Order +from broker_quickfix_client.wrappers.order_cancel_replace_request import ( + OrderCancelReplaceRequest, +) +from broker_quickfix_client.wrappers.order_cancel_request import OrderCancelRequest + +logger = logging.getLogger("client") + + +def order_book_test(application: ClientApplication): + order_map: dict[int, Order] = {} + + def order_filled_callback(report): + logger.info(f"Filled: {report}") + + def order_rejected_callback(report): + logger.warning(f"Rejected: {report}") + del order_map[report.client_order_id] + + def order_accepted_callback(report: AcceptedOrderExecutionReport): + logger.info(f"Accepted: {report}") + order_map[report.client_order_id].order_id = report.order_id + + def order_canceled_callback(report: CanceledOrderExecutionReport): + logger.info(f"Canceled: {report}") + del order_map[report.original_client_order_id] + + def order_replaced_callback(report: ReplacedOrderExecutionReport): + logger.info(f"Replaced: {report}") + order_map[report.client_order_id] = deepcopy( + order_map[report.original_client_order_id] + ) + order_map[report.client_order_id].price = report.price + order_map[report.client_order_id].quantity = report.leaves_quantity + order_map[report.client_order_id].client_order_id = report.client_order_id + + execution_handler = ExecutionReportHandler( + order_filled_callback=order_filled_callback, + order_rejected_callback=order_rejected_callback, + order_accepted_callback=order_accepted_callback, + order_canceled_callback=order_canceled_callback, + order_replaced_callback=order_replaced_callback, + ) + application.set_execution_report_handler(execution_handler) + + order = NewOrderSingle.new_limit_order(1, SideEnum.BUY, 1, "ACGL", 40.4) + application.send(order) + order_map[1] = order.get_order() + while order_map[1].order_id is None: + sleep(0.1) + replaced_order = OrderCancelReplaceRequest.new_replace_order( + 2, order_map[1], 40.5, 2 + ) + application.send(replaced_order) + # Wait for the order to be replaced at index 2 + while order_map.get(2) is None: + sleep(0.1) + canceled_order = OrderCancelRequest.new_cancel_order(3, order_map[2]) + application.send(canceled_order) + while order_map.get(2) is not None: + sleep(0.1) + + logger.info("Done") diff --git a/clients/quickfix-client/broker_quickfix_client/utils/__init__.py b/clients/quickfix-client/broker_quickfix_client/utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/clients/quickfix-client/broker_quickfix_client/utils/loader.py b/clients/quickfix-client/broker_quickfix_client/utils/loader.py new file mode 100644 index 00000000..c1e62db9 --- /dev/null +++ b/clients/quickfix-client/broker_quickfix_client/utils/loader.py @@ -0,0 +1,23 @@ +from configparser import ConfigParser +from io import TextIOWrapper +from pathlib import Path + + +def get_project_dir() -> Path: + return Path(__file__).resolve().parents[2] + + +def get_config_dir() -> Path: + return get_project_dir() / "config" + + +def get_file_as_stream(file_path: Path) -> TextIOWrapper: + """Get the file as a stream""" + return open(file_path, "r", encoding="utf-8") + + +def load_cfg(file_path: Path): + """Load a config file in the form of a ini file""" + config = ConfigParser() + config.read_file(get_file_as_stream(file_path)) + return config diff --git a/clients/quickfix-client/broker_quickfix_client/utils/logger.py b/clients/quickfix-client/broker_quickfix_client/utils/logger.py new file mode 100644 index 00000000..d0ffcbb5 --- /dev/null +++ b/clients/quickfix-client/broker_quickfix_client/utils/logger.py @@ -0,0 +1,87 @@ +""" Logging configuration for the application. """ + +import logging +import sys +from typing import Optional + +BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = range(8) + +# The background is set with 40 plus the number of the color, and the foreground with 30 + +# These are the sequences need to get colored ouput +RESET_SEQ = "\033[0m" +COLOR_SEQ = "\033[1;%dm" +BOLD_SEQ = "\033[1m" + +COLORS = { + "WARNING": YELLOW, + "INFO": GREEN, + "DEBUG": BLUE, + "CRITICAL": MAGENTA, + "ERROR": RED, +} + + +class ColoredFormatter(logging.Formatter): + def __init__(self, msg): + msg = msg.replace("$RESET", RESET_SEQ).replace("$BOLD", BOLD_SEQ) + logging.Formatter.__init__(self, msg) + + def format(self, record): # pragma: no cover + levelname = record.levelname + if levelname in COLORS: + levelname_color = ( + COLOR_SEQ % (30 + COLORS[levelname]) + levelname + RESET_SEQ + ) + record.levelname = levelname_color + return logging.Formatter.format(self, record) + + +def setup_logs( + logger_name: str, + level: int = logging.DEBUG, +): + """ + Sets up the logger with the specified logger name and configures it to log to stdout if `to_stdout` is True. + The logger's log level is set based on the `LOG_LEVEL` value in the application's settings. + + Args: + logger_name (str): The name of the logger. + to_stdout (bool, optional): Whether to log to stdout. Defaults to True. + """ + logger = logging.getLogger(logger_name) + + logger.setLevel(level) + + format_string = ( + "%(levelname)-18s | %(asctime)s | $BOLD%(name)-25s$RESET | %(message)s" + ) + color_formatter = ColoredFormatter(format_string) + + configure_stdout_logging( + logger=logger, + formatter=color_formatter, + log_level=level, + ) + + +def configure_stdout_logging( + logger: logging.Logger, + formatter: Optional[logging.Formatter] = None, + log_level: int = logging.DEBUG, +): + """ + Configures the logger to log to stdout with the specified logger, formatter and log level. + + Args: + logger (Optional[logging.Logger], optional): The logger to configure. Defaults to None. + formatter (Optional[logging.Formatter], optional): The formatter to use. Defaults to None. + log_level (str, optional): The log level to use. Defaults to "DEV". + """ + stream_handler = logging.StreamHandler(stream=sys.stdout) + + stream_handler.setFormatter(formatter) + stream_handler.setLevel(log_level) + + logger.handlers = [] + logger.addHandler(stream_handler) diff --git a/clients/quickfix-client/broker_quickfix_client/utils/quickfix.py b/clients/quickfix-client/broker_quickfix_client/utils/quickfix.py new file mode 100644 index 00000000..6ca75668 --- /dev/null +++ b/clients/quickfix-client/broker_quickfix_client/utils/quickfix.py @@ -0,0 +1,97 @@ +import logging + +from quickfix import ( + DataDictionary, + Dictionary, + FieldBase, + Message, + MsgType, + MsgType_Heartbeat, + SessionID, + SessionSettings, +) + +from broker_quickfix_client.constant import ( + DATA_DICTIONNARY, + SERVER_IP, + SERVER_NAME, + SERVER_PORT, +) +from broker_quickfix_client.decorators import default_return_value_decorator + +logger = logging.getLogger("quickfix.event") + + +def log_quick_fix_message( + message: Message, + prefix: str | None, + level: int = logging.DEBUG, + data_dictionary: DataDictionary = DATA_DICTIONNARY, +): + def get_field_name(key): + field_name = key + ret = data_dictionary.getFieldName(field=int(key), name=field_name) + return ret[0] + + def get_field_value(key, value): + field_value = value + ret = data_dictionary.getValueName( + field=int(key), value=value, name=field_value + ) + return ret[0] + + if is_heartbeat(message): + return + + message_parts = [ + "=".join([get_field_name(split[0]), get_field_value(split[0], split[1])]) + if len(split := s.split("=")) == 2 + else s + for s in str(message).split("\x01") + ] + + message_string = "|".join(message_parts) + logger.log(level, f"{prefix}: {message_string}") + + +def is_heartbeat(message: Message) -> bool: + return message.getHeader().getField(MsgType()).getString() == MsgType_Heartbeat + + +@default_return_value_decorator(None) +def get_message_field(message: Message, field_type: type[FieldBase]) -> str: + field = field_type() + message.getField(field) + return field.getString() + + +def set_settings(username: str): + settings = SessionSettings() + + # Default settings + default_dict = Dictionary() + + default_dict.setString("FileStorePath", "./storage/") + default_dict.setString("FileLogPath", "./logs/client") + default_dict.setBool("ResetOnLogon", True) + default_dict.setBool("ResetOnLogout", True) + default_dict.setBool("ResetOnDisconnect", True) + default_dict.setBool("UseDataDictionary", True) + + settings.set(default_dict) + + # User settings + session_id = SessionID("FIX.4.4", username, SERVER_NAME) + session_dict = Dictionary() + + session_dict.setString("ConnectionType", "initiator") + session_dict.setString("SocketConnectHost", SERVER_IP) + session_dict.setInt("SocketConnectPort", SERVER_PORT) + session_dict.setString("DataDictionary", "./config/FIX44.xml") + session_dict.setString("StartTime", "00:00:00") + session_dict.setString("EndTime", "00:00:00") + session_dict.setInt("HeartBtInt", 30) + + settings.set(session_id, session_dict) + + return settings diff --git a/clients/quickfix-client/broker_quickfix_client/wrappers/__init__.py b/clients/quickfix-client/broker_quickfix_client/wrappers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/clients/quickfix-client/broker_quickfix_client/wrappers/enums.py b/clients/quickfix-client/broker_quickfix_client/wrappers/enums.py new file mode 100644 index 00000000..ec95cfc5 --- /dev/null +++ b/clients/quickfix-client/broker_quickfix_client/wrappers/enums.py @@ -0,0 +1,75 @@ +from enum import Enum + + +class OrderTypeEnum(Enum): + MARKET = "1" + LIMIT = "2" + STOP = "3" + STOP_LIMIT = "4" + + +class SideEnum(Enum): + BUY = "1" + SELL = "2" + + +class OrderStatusEnum(Enum): + NEW = "0" + PARTIALLY_FILLED = "1" + FILLED = "2" + DONE_FOR_DAY = "3" + CANCELED = "4" + REPLACED = "5" + PENDING_CANCEL = "6" + STOPPED = "7" + REJECTED = "8" + SUSPENDED = "9" + PENDING_NEW = "A" + CALCULATED = "B" + EXPIRED = "C" + ACCEPTED_FOR_BIDDING = "D" + PENDING_REPLACE = "E" + + +class ExecTypeEnum(Enum): + NEW = "0" + DONE_FOR_DAY = "3" + CANCELED = "4" + REPLACED = "5" + PENDING_CANCEL = "6" + STOPPED = "7" + REJECTED = "8" + SUSPENDED = "9" + PENDING_NEW = "A" + CALCULATED = "B" + EXPIRED = "C" + RESTATED = "D" + PENDING_REPLACE = "E" + TRADE = "F" + TRADE_CORRECT = "G" + TRADE_CANCEL = "H" + ORDER_STATUS = "I" + + +class OrderRejectReasonEnum(Enum): + BROKER_CREDIT = "0" + UNKNOWN_SYMBOL = "1" + EXCHANGE_CLOSED = "2" + ORDER_EXCEEDS_LIMIT = "3" + TOO_LATE_TO_ENTER = "4" + UNKNOWN_ORDER = "5" + DUPLICATE_ORDER = "6" + DUPLICATE_OF_A_VERBALLY_COMMUNICATED_ORDER = "7" + STALE_ORDER = "8" + TRADE_ALONG_REQUIRED = "9" + INVALID_INVESTOR_ID = "10" + UNSUPPORTED_ORDER_CHARACTERISTIC = "11" + INCORRECT_QUANTITY = "13" + INCORRECT_ALLOCATED_QUANTITY = "14" + UNKNOWN_ACCOUNT = "15" + OTHER = "99" + + +class CxlRejResponseToEnum(Enum): + ORDER_CANCEL_REQUEST = "1" + ORDER_CANCEL_REPLACE_REQUEST = "2" diff --git a/clients/quickfix-client/broker_quickfix_client/wrappers/execution_report.py b/clients/quickfix-client/broker_quickfix_client/wrappers/execution_report.py new file mode 100644 index 00000000..3f869f77 --- /dev/null +++ b/clients/quickfix-client/broker_quickfix_client/wrappers/execution_report.py @@ -0,0 +1,45 @@ +from dataclasses import dataclass + +from broker_quickfix_client.wrappers.enums import ( + OrderRejectReasonEnum, + OrderTypeEnum, + SideEnum, +) + + +@dataclass +class BaseExecutionReport: + order_id: int + client_order_id: int + symbol: str + side: SideEnum + leaves_quantity: int + type: OrderTypeEnum + + +@dataclass +class FilledExecutionReport(BaseExecutionReport): + cum_quantity: int + price: float + + +@dataclass +class RejectedExecutionReport(BaseExecutionReport): + reject_reason: OrderRejectReasonEnum + + +@dataclass +class AcceptedOrderExecutionReport(BaseExecutionReport): + price: float + + +@dataclass +class ReplacedOrderExecutionReport(BaseExecutionReport): + original_client_order_id: int + price: float + + +@dataclass +class CanceledOrderExecutionReport(BaseExecutionReport): + original_client_order_id: int + price: float diff --git a/clients/quickfix-client/broker_quickfix_client/wrappers/new_order_single.py b/clients/quickfix-client/broker_quickfix_client/wrappers/new_order_single.py new file mode 100644 index 00000000..3a792638 --- /dev/null +++ b/clients/quickfix-client/broker_quickfix_client/wrappers/new_order_single.py @@ -0,0 +1,108 @@ +import quickfix44 +from quickfix import ClOrdID, OrderQty, OrdType, Price, Side, Symbol, TransactTime + +from broker_quickfix_client.utils.quickfix import get_message_field +from broker_quickfix_client.wrappers.enums import OrderTypeEnum, SideEnum +from broker_quickfix_client.wrappers.order import Order + + +class NewOrderSingle(quickfix44.NewOrderSingle): + def __init__( + self, + clOrdID: ClOrdID, + side: Side, + orderQty: OrderQty, + ordType: OrdType, + symbol: Symbol, + price: Price | None = None, + ): + super().__init__() + transact_time = TransactTime() + + self.setField(clOrdID) + self.setField(side) + self.setField(orderQty) + self.setField(ordType) + self.setField(symbol) + self.setField(transact_time) + + if price: + self.setField(price) + + def get_order(self) -> Order: + price = get_message_field(self, Price) + return Order( + order_id=None, + client_order_id=int(get_message_field(self, ClOrdID)), + symbol=get_message_field(self, Symbol), + side=SideEnum(get_message_field(self, Side)), + type=OrderTypeEnum(get_message_field(self, OrdType)), + price=float(price) if price else None, + quantity=int(get_message_field(self, OrderQty)), + ) + + @staticmethod + def new_market_order( + cl_ord_id: int, + side: SideEnum, + order_qty: int, + symbol: str, + ) -> "NewOrderSingle": + return NewOrderSingle( + ClOrdID(str(cl_ord_id)), + Side(side.value), + OrderQty(order_qty), + OrdType(OrderTypeEnum.MARKET.value), + Symbol(symbol), + ) + + @staticmethod + def new_limit_order( + cl_ord_id: int, + side: SideEnum, + order_qty: int, + symbol: str, + price: float, + ) -> "NewOrderSingle": + return NewOrderSingle( + ClOrdID(str(cl_ord_id)), + Side(side.value), + OrderQty(order_qty), + OrdType(OrderTypeEnum.LIMIT.value), + Symbol(symbol), + Price(price), + ) + + @staticmethod + def new_stop_order( + cl_ord_id: int, + side: SideEnum, + order_qty: int, + symbol: str, + price: str, + ) -> "NewOrderSingle": + return NewOrderSingle( + ClOrdID(str(cl_ord_id)), + Side(side.value), + OrderQty(order_qty), + OrdType(OrderTypeEnum.STOP.value), + Symbol(symbol), + Price(price), + ) + + @staticmethod + def new_stop_limit_order( + cl_ord_id: int, + side: SideEnum, + order_qty: int, + symbol: str, + price: float, + ) -> "NewOrderSingle": + return NewOrderSingle( + ClOrdID(str(cl_ord_id)), + Side(side.value), + OrderQty(order_qty), + OrdType(OrderTypeEnum.STOP_LIMIT.value), + Symbol(symbol), + Price(price), + ) diff --git a/clients/quickfix-client/broker_quickfix_client/wrappers/order.py b/clients/quickfix-client/broker_quickfix_client/wrappers/order.py new file mode 100644 index 00000000..e8f17153 --- /dev/null +++ b/clients/quickfix-client/broker_quickfix_client/wrappers/order.py @@ -0,0 +1,14 @@ +from dataclasses import dataclass + +from broker_quickfix_client.wrappers.enums import OrderTypeEnum, SideEnum + + +@dataclass +class Order: + order_id: int | None + client_order_id: int + symbol: str + side: SideEnum + type: OrderTypeEnum + price: float | None + quantity: int diff --git a/clients/quickfix-client/broker_quickfix_client/wrappers/order_cancel_reject.py b/clients/quickfix-client/broker_quickfix_client/wrappers/order_cancel_reject.py new file mode 100644 index 00000000..f1c01780 --- /dev/null +++ b/clients/quickfix-client/broker_quickfix_client/wrappers/order_cancel_reject.py @@ -0,0 +1,8 @@ +from dataclasses import dataclass + + +@dataclass +class OrderCancelReject: + order_id: int + client_order_id: int + original_client_order_id: int diff --git a/clients/quickfix-client/broker_quickfix_client/wrappers/order_cancel_replace_request.py b/clients/quickfix-client/broker_quickfix_client/wrappers/order_cancel_replace_request.py new file mode 100644 index 00000000..483dbb62 --- /dev/null +++ b/clients/quickfix-client/broker_quickfix_client/wrappers/order_cancel_replace_request.py @@ -0,0 +1,74 @@ +import quickfix +import quickfix44 + +from broker_quickfix_client.utils.quickfix import get_message_field +from broker_quickfix_client.wrappers.enums import OrderTypeEnum, SideEnum +from broker_quickfix_client.wrappers.order import Order + + +class OrderCancelReplaceRequest(quickfix44.OrderCancelReplaceRequest): + def __init__( + self, + orderID: quickfix.OrderID, + clOrdID: quickfix.ClOrdID, + origClOrdID: quickfix.OrigClOrdID, + side: quickfix.Side, + orderQty: quickfix.OrderQty, + ordType: quickfix.OrdType, + symbol: quickfix.Symbol, + price: quickfix.Price, + ): + super().__init__() + transact_time = quickfix.TransactTime() + + self.setField(orderID) + self.setField(clOrdID) + self.setField(origClOrdID) + self.setField(side) + self.setField(orderQty) + self.setField(ordType) + self.setField(symbol) + self.setField(price) + self.setField(transact_time) + + def get_order(self) -> Order: + return Order( + order_id=int(get_message_field(self, quickfix.OrderID)), + client_order_id=int(get_message_field(self, quickfix.ClOrdID)), + symbol=get_message_field(self, quickfix.Symbol), + side=SideEnum(get_message_field(self, quickfix.Side)), + type=OrderTypeEnum(get_message_field(self, quickfix.OrdType)), + price=float(get_message_field(self, quickfix.Price)), + quantity=int(get_message_field(self, quickfix.OrderQty)), + ) + + @staticmethod + def new_replace_order( + cl_ord_id: int, + original_order: Order, + modified_price: float | None = None, + modified_quantity: int | None = None, + ) -> "OrderCancelReplaceRequest": + if modified_price is None and modified_quantity is None: + raise ValueError("Either price and/or quantity must be modified") + if original_order.order_id is None: + raise ValueError("Order id must be set") + if original_order.type != OrderTypeEnum.LIMIT: + raise ValueError("Only limit orders can be replaced") + + return OrderCancelReplaceRequest( + quickfix.OrderID(str(original_order.order_id)), + quickfix.ClOrdID(str(cl_ord_id)), + quickfix.OrigClOrdID(str(original_order.client_order_id)), + quickfix.Side(original_order.side.value), + quickfix.OrderQty( + original_order.quantity + if modified_quantity is None + else modified_quantity + ), + quickfix.OrdType(OrderTypeEnum.LIMIT.value), + quickfix.Symbol(original_order.symbol), + quickfix.Price( + original_order.price if modified_price is None else modified_price + ), + ) diff --git a/clients/quickfix-client/broker_quickfix_client/wrappers/order_cancel_request.py b/clients/quickfix-client/broker_quickfix_client/wrappers/order_cancel_request.py new file mode 100644 index 00000000..55b4c3a0 --- /dev/null +++ b/clients/quickfix-client/broker_quickfix_client/wrappers/order_cancel_request.py @@ -0,0 +1,55 @@ +import quickfix +import quickfix44 + +from broker_quickfix_client.utils.quickfix import get_message_field +from broker_quickfix_client.wrappers.enums import OrderTypeEnum, SideEnum +from broker_quickfix_client.wrappers.order import Order + + +class OrderCancelRequest(quickfix44.OrderCancelRequest): + def __init__( + self, + orderID: quickfix.OrderID, + clOrdID: quickfix.ClOrdID, + origClOrdID: quickfix.OrigClOrdID, + side: quickfix.Side, + symbol: quickfix.Symbol, + ): + super().__init__() + transact_time = quickfix.TransactTime() + + self.setField(orderID) + self.setField(clOrdID) + self.setField(origClOrdID) + self.setField(side) + self.setField(symbol) + self.setField(transact_time) + + def get_order(self) -> Order: + return Order( + order_id=int(get_message_field(self, quickfix.OrderID)), + client_order_id=int(get_message_field(self, quickfix.ClOrdID)), + symbol=get_message_field(self, quickfix.Symbol), + side=SideEnum(get_message_field(self, quickfix.Side)), + type=OrderTypeEnum.LIMIT, + price=0.0, + quantity=0, + ) + + @staticmethod + def new_cancel_order( + cl_ord_id: int, + original_order: Order, + ) -> "OrderCancelRequest": + if original_order.order_id is None: + raise ValueError("Order id must be set") + if original_order.type != OrderTypeEnum.LIMIT: + raise ValueError("Only limit orders can be canceled") + + return OrderCancelRequest( + quickfix.OrderID(str(original_order.order_id)), + quickfix.ClOrdID(str(cl_ord_id)), + quickfix.OrigClOrdID(str(original_order.client_order_id)), + quickfix.Side(original_order.side.value), + quickfix.Symbol(original_order.symbol), + ) diff --git a/clients/quickfix-client/config/FIX44.xml b/clients/quickfix-client/config/FIX44.xml new file mode 100644 index 00000000..3de90f99 --- /dev/null +++ b/clients/quickfix-client/config/FIX44.xml @@ -0,0 +1,6599 @@ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/clients/quickfix-client/latency.sh b/clients/quickfix-client/latency.sh new file mode 100755 index 00000000..72e03d51 --- /dev/null +++ b/clients/quickfix-client/latency.sh @@ -0,0 +1,20 @@ +#! /bin/bash + +# This script is used to stress test the engine by starting n clients + +# Get the number of clients to start +if [ $# -eq 0 ]; then + echo "No arguments supplied" + echo "Usage: ./latency.sh " + exit 1 +fi + +num_clients=$1 + +for ((i = 0; i < num_clients; i++)); do + # Start the client + poetry run python broker_quickfix_client/main.py --username user$i & +done + +# Wait for the clients to finish +wait diff --git a/clients/quickfix-client/poetry.lock b/clients/quickfix-client/poetry.lock new file mode 100644 index 00000000..f9a25e8c --- /dev/null +++ b/clients/quickfix-client/poetry.lock @@ -0,0 +1,749 @@ +# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand. + +[[package]] +name = "astroid" +version = "2.15.8" +description = "An abstract syntax tree for Python with inference support." +optional = false +python-versions = ">=3.7.2" +files = [ + {file = "astroid-2.15.8-py3-none-any.whl", hash = "sha256:1aa149fc5c6589e3d0ece885b4491acd80af4f087baafa3fb5203b113e68cd3c"}, + {file = "astroid-2.15.8.tar.gz", hash = "sha256:6c107453dffee9055899705de3c9ead36e74119cee151e5a9aaf7f0b0e020a6a"}, +] + +[package.dependencies] +lazy-object-proxy = ">=1.4.0" +wrapt = {version = ">=1.14,<2", markers = "python_version >= \"3.11\""} + +[[package]] +name = "black" +version = "23.12.0" +description = "The uncompromising code formatter." +optional = false +python-versions = ">=3.8" +files = [ + {file = "black-23.12.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:67f19562d367468ab59bd6c36a72b2c84bc2f16b59788690e02bbcb140a77175"}, + {file = "black-23.12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:bbd75d9f28a7283b7426160ca21c5bd640ca7cd8ef6630b4754b6df9e2da8462"}, + {file = "black-23.12.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:593596f699ca2dcbbbdfa59fcda7d8ad6604370c10228223cd6cf6ce1ce7ed7e"}, + {file = "black-23.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:12d5f10cce8dc27202e9a252acd1c9a426c83f95496c959406c96b785a92bb7d"}, + {file = "black-23.12.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e73c5e3d37e5a3513d16b33305713237a234396ae56769b839d7c40759b8a41c"}, + {file = "black-23.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ba09cae1657c4f8a8c9ff6cfd4a6baaf915bb4ef7d03acffe6a2f6585fa1bd01"}, + {file = "black-23.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ace64c1a349c162d6da3cef91e3b0e78c4fc596ffde9413efa0525456148873d"}, + {file = "black-23.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:72db37a2266b16d256b3ea88b9affcdd5c41a74db551ec3dd4609a59c17d25bf"}, + {file = "black-23.12.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fdf6f23c83078a6c8da2442f4d4eeb19c28ac2a6416da7671b72f0295c4a697b"}, + {file = "black-23.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:39dda060b9b395a6b7bf9c5db28ac87b3c3f48d4fdff470fa8a94ab8271da47e"}, + {file = "black-23.12.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7231670266ca5191a76cb838185d9be59cfa4f5dd401b7c1c70b993c58f6b1b5"}, + {file = "black-23.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:193946e634e80bfb3aec41830f5d7431f8dd5b20d11d89be14b84a97c6b8bc75"}, + {file = "black-23.12.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bcf91b01ddd91a2fed9a8006d7baa94ccefe7e518556470cf40213bd3d44bbbc"}, + {file = "black-23.12.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:996650a89fe5892714ea4ea87bc45e41a59a1e01675c42c433a35b490e5aa3f0"}, + {file = "black-23.12.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bdbff34c487239a63d86db0c9385b27cdd68b1bfa4e706aa74bb94a435403672"}, + {file = "black-23.12.0-cp38-cp38-win_amd64.whl", hash = "sha256:97af22278043a6a1272daca10a6f4d36c04dfa77e61cbaaf4482e08f3640e9f0"}, + {file = "black-23.12.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ead25c273adfad1095a8ad32afdb8304933efba56e3c1d31b0fee4143a1e424a"}, + {file = "black-23.12.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c71048345bdbced456cddf1622832276d98a710196b842407840ae8055ade6ee"}, + {file = "black-23.12.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81a832b6e00eef2c13b3239d514ea3b7d5cc3eaa03d0474eedcbbda59441ba5d"}, + {file = "black-23.12.0-cp39-cp39-win_amd64.whl", hash = "sha256:6a82a711d13e61840fb11a6dfecc7287f2424f1ca34765e70c909a35ffa7fb95"}, + {file = "black-23.12.0-py3-none-any.whl", hash = "sha256:a7c07db8200b5315dc07e331dda4d889a56f6bf4db6a9c2a526fa3166a81614f"}, + {file = "black-23.12.0.tar.gz", hash = "sha256:330a327b422aca0634ecd115985c1c7fd7bdb5b5a2ef8aa9888a82e2ebe9437a"}, +] + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +packaging = ">=22.0" +pathspec = ">=0.9.0" +platformdirs = ">=2" + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + +[[package]] +name = "cfgv" +version = "3.4.0" +description = "Validate configuration and produce human readable error messages." +optional = false +python-versions = ">=3.8" +files = [ + {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, + {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, +] + +[[package]] +name = "click" +version = "8.1.7" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.7" +files = [ + {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, + {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "coverage" +version = "7.3.3" +description = "Code coverage measurement for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "coverage-7.3.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d874434e0cb7b90f7af2b6e3309b0733cde8ec1476eb47db148ed7deeb2a9494"}, + {file = "coverage-7.3.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ee6621dccce8af666b8c4651f9f43467bfbf409607c604b840b78f4ff3619aeb"}, + {file = "coverage-7.3.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1367aa411afb4431ab58fd7ee102adb2665894d047c490649e86219327183134"}, + {file = "coverage-7.3.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1f0f8f0c497eb9c9f18f21de0750c8d8b4b9c7000b43996a094290b59d0e7523"}, + {file = "coverage-7.3.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db0338c4b0951d93d547e0ff8d8ea340fecf5885f5b00b23be5aa99549e14cfd"}, + {file = "coverage-7.3.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d31650d313bd90d027f4be7663dfa2241079edd780b56ac416b56eebe0a21aab"}, + {file = "coverage-7.3.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:9437a4074b43c177c92c96d051957592afd85ba00d3e92002c8ef45ee75df438"}, + {file = "coverage-7.3.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:9e17d9cb06c13b4f2ef570355fa45797d10f19ca71395910b249e3f77942a837"}, + {file = "coverage-7.3.3-cp310-cp310-win32.whl", hash = "sha256:eee5e741b43ea1b49d98ab6e40f7e299e97715af2488d1c77a90de4a663a86e2"}, + {file = "coverage-7.3.3-cp310-cp310-win_amd64.whl", hash = "sha256:593efa42160c15c59ee9b66c5f27a453ed3968718e6e58431cdfb2d50d5ad284"}, + {file = "coverage-7.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8c944cf1775235c0857829c275c777a2c3e33032e544bcef614036f337ac37bb"}, + {file = "coverage-7.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:eda7f6e92358ac9e1717ce1f0377ed2b9320cea070906ece4e5c11d172a45a39"}, + {file = "coverage-7.3.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c854c1d2c7d3e47f7120b560d1a30c1ca221e207439608d27bc4d08fd4aeae8"}, + {file = "coverage-7.3.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:222b038f08a7ebed1e4e78ccf3c09a1ca4ac3da16de983e66520973443b546bc"}, + {file = "coverage-7.3.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff4800783d85bff132f2cc7d007426ec698cdce08c3062c8d501ad3f4ea3d16c"}, + {file = "coverage-7.3.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fc200cec654311ca2c3f5ab3ce2220521b3d4732f68e1b1e79bef8fcfc1f2b97"}, + {file = "coverage-7.3.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:307aecb65bb77cbfebf2eb6e12009e9034d050c6c69d8a5f3f737b329f4f15fb"}, + {file = "coverage-7.3.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ffb0eacbadb705c0a6969b0adf468f126b064f3362411df95f6d4f31c40d31c1"}, + {file = "coverage-7.3.3-cp311-cp311-win32.whl", hash = "sha256:79c32f875fd7c0ed8d642b221cf81feba98183d2ff14d1f37a1bbce6b0347d9f"}, + {file = "coverage-7.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:243576944f7c1a1205e5cd658533a50eba662c74f9be4c050d51c69bd4532936"}, + {file = "coverage-7.3.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:a2ac4245f18057dfec3b0074c4eb366953bca6787f1ec397c004c78176a23d56"}, + {file = "coverage-7.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f9191be7af41f0b54324ded600e8ddbcabea23e1e8ba419d9a53b241dece821d"}, + {file = "coverage-7.3.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:31c0b1b8b5a4aebf8fcd227237fc4263aa7fa0ddcd4d288d42f50eff18b0bac4"}, + {file = "coverage-7.3.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee453085279df1bac0996bc97004771a4a052b1f1e23f6101213e3796ff3cb85"}, + {file = "coverage-7.3.3-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1191270b06ecd68b1d00897b2daddb98e1719f63750969614ceb3438228c088e"}, + {file = "coverage-7.3.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:007a7e49831cfe387473e92e9ff07377f6121120669ddc39674e7244350a6a29"}, + {file = "coverage-7.3.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:af75cf83c2d57717a8493ed2246d34b1f3398cb8a92b10fd7a1858cad8e78f59"}, + {file = "coverage-7.3.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:811ca7373da32f1ccee2927dc27dc523462fd30674a80102f86c6753d6681bc6"}, + {file = "coverage-7.3.3-cp312-cp312-win32.whl", hash = "sha256:733537a182b5d62184f2a72796eb6901299898231a8e4f84c858c68684b25a70"}, + {file = "coverage-7.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:e995efb191f04b01ced307dbd7407ebf6e6dc209b528d75583277b10fd1800ee"}, + {file = "coverage-7.3.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:fbd8a5fe6c893de21a3c6835071ec116d79334fbdf641743332e442a3466f7ea"}, + {file = "coverage-7.3.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:50c472c1916540f8b2deef10cdc736cd2b3d1464d3945e4da0333862270dcb15"}, + {file = "coverage-7.3.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e9223a18f51d00d3ce239c39fc41410489ec7a248a84fab443fbb39c943616c"}, + {file = "coverage-7.3.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f501e36ac428c1b334c41e196ff6bd550c0353c7314716e80055b1f0a32ba394"}, + {file = "coverage-7.3.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:475de8213ed95a6b6283056d180b2442eee38d5948d735cd3d3b52b86dd65b92"}, + {file = "coverage-7.3.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:afdcc10c01d0db217fc0a64f58c7edd635b8f27787fea0a3054b856a6dff8717"}, + {file = "coverage-7.3.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:fff0b2f249ac642fd735f009b8363c2b46cf406d3caec00e4deeb79b5ff39b40"}, + {file = "coverage-7.3.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:a1f76cfc122c9e0f62dbe0460ec9cc7696fc9a0293931a33b8870f78cf83a327"}, + {file = "coverage-7.3.3-cp38-cp38-win32.whl", hash = "sha256:757453848c18d7ab5d5b5f1827293d580f156f1c2c8cef45bfc21f37d8681069"}, + {file = "coverage-7.3.3-cp38-cp38-win_amd64.whl", hash = "sha256:ad2453b852a1316c8a103c9c970db8fbc262f4f6b930aa6c606df9b2766eee06"}, + {file = "coverage-7.3.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3b15e03b8ee6a908db48eccf4e4e42397f146ab1e91c6324da44197a45cb9132"}, + {file = "coverage-7.3.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:89400aa1752e09f666cc48708eaa171eef0ebe3d5f74044b614729231763ae69"}, + {file = "coverage-7.3.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c59a3e59fb95e6d72e71dc915e6d7fa568863fad0a80b33bc7b82d6e9f844973"}, + {file = "coverage-7.3.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9ede881c7618f9cf93e2df0421ee127afdfd267d1b5d0c59bcea771cf160ea4a"}, + {file = "coverage-7.3.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3bfd2c2f0e5384276e12b14882bf2c7621f97c35320c3e7132c156ce18436a1"}, + {file = "coverage-7.3.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7f3bad1a9313401ff2964e411ab7d57fb700a2d5478b727e13f156c8f89774a0"}, + {file = "coverage-7.3.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:65d716b736f16e250435473c5ca01285d73c29f20097decdbb12571d5dfb2c94"}, + {file = "coverage-7.3.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a702e66483b1fe602717020a0e90506e759c84a71dbc1616dd55d29d86a9b91f"}, + {file = "coverage-7.3.3-cp39-cp39-win32.whl", hash = "sha256:7fbf3f5756e7955174a31fb579307d69ffca91ad163467ed123858ce0f3fd4aa"}, + {file = "coverage-7.3.3-cp39-cp39-win_amd64.whl", hash = "sha256:cad9afc1644b979211989ec3ff7d82110b2ed52995c2f7263e7841c846a75348"}, + {file = "coverage-7.3.3-pp38.pp39.pp310-none-any.whl", hash = "sha256:d299d379b676812e142fb57662a8d0d810b859421412b4d7af996154c00c31bb"}, + {file = "coverage-7.3.3.tar.gz", hash = "sha256:df04c64e58df96b4427db8d0559e95e2df3138c9916c96f9f6a4dd220db2fdb7"}, +] + +[package.extras] +toml = ["tomli"] + +[[package]] +name = "dill" +version = "0.3.7" +description = "serialize all of Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "dill-0.3.7-py3-none-any.whl", hash = "sha256:76b122c08ef4ce2eedcd4d1abd8e641114bfc6c2867f49f3c41facf65bf19f5e"}, + {file = "dill-0.3.7.tar.gz", hash = "sha256:cc1c8b182eb3013e24bd475ff2e9295af86c1a38eb1aff128dac8962a9ce3c03"}, +] + +[package.extras] +graph = ["objgraph (>=1.7.2)"] + +[[package]] +name = "distlib" +version = "0.3.8" +description = "Distribution utilities" +optional = false +python-versions = "*" +files = [ + {file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"}, + {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, +] + +[[package]] +name = "filelock" +version = "3.13.1" +description = "A platform independent file lock." +optional = false +python-versions = ">=3.8" +files = [ + {file = "filelock-3.13.1-py3-none-any.whl", hash = "sha256:57dbda9b35157b05fb3e58ee91448612eb674172fab98ee235ccb0b5bee19a1c"}, + {file = "filelock-3.13.1.tar.gz", hash = "sha256:521f5f56c50f8426f5e03ad3b281b490a87ef15bc6c526f168290f0c7148d44e"}, +] + +[package.extras] +docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.24)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"] +typing = ["typing-extensions (>=4.8)"] + +[[package]] +name = "flake8" +version = "6.1.0" +description = "the modular source code checker: pep8 pyflakes and co" +optional = false +python-versions = ">=3.8.1" +files = [ + {file = "flake8-6.1.0-py2.py3-none-any.whl", hash = "sha256:ffdfce58ea94c6580c77888a86506937f9a1a227dfcd15f245d694ae20a6b6e5"}, + {file = "flake8-6.1.0.tar.gz", hash = "sha256:d5b3857f07c030bdb5bf41c7f53799571d75c4491748a3adcd47de929e34cd23"}, +] + +[package.dependencies] +mccabe = ">=0.7.0,<0.8.0" +pycodestyle = ">=2.11.0,<2.12.0" +pyflakes = ">=3.1.0,<3.2.0" + +[[package]] +name = "identify" +version = "2.5.33" +description = "File identification library for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "identify-2.5.33-py2.py3-none-any.whl", hash = "sha256:d40ce5fcd762817627670da8a7d8d8e65f24342d14539c59488dc603bf662e34"}, + {file = "identify-2.5.33.tar.gz", hash = "sha256:161558f9fe4559e1557e1bff323e8631f6a0e4837f7497767c1782832f16b62d"}, +] + +[package.extras] +license = ["ukkonen"] + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[[package]] +name = "isort" +version = "5.13.2" +description = "A Python utility / library to sort Python imports." +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"}, + {file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"}, +] + +[package.extras] +colors = ["colorama (>=0.4.6)"] + +[[package]] +name = "lazy-object-proxy" +version = "1.9.0" +description = "A fast and thorough lazy object proxy." +optional = false +python-versions = ">=3.7" +files = [ + {file = "lazy-object-proxy-1.9.0.tar.gz", hash = "sha256:659fb5809fa4629b8a1ac5106f669cfc7bef26fbb389dda53b3e010d1ac4ebae"}, + {file = "lazy_object_proxy-1.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b40387277b0ed2d0602b8293b94d7257e17d1479e257b4de114ea11a8cb7f2d7"}, + {file = "lazy_object_proxy-1.9.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8c6cfb338b133fbdbc5cfaa10fe3c6aeea827db80c978dbd13bc9dd8526b7d4"}, + {file = "lazy_object_proxy-1.9.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:721532711daa7db0d8b779b0bb0318fa87af1c10d7fe5e52ef30f8eff254d0cd"}, + {file = "lazy_object_proxy-1.9.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:66a3de4a3ec06cd8af3f61b8e1ec67614fbb7c995d02fa224813cb7afefee701"}, + {file = "lazy_object_proxy-1.9.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1aa3de4088c89a1b69f8ec0dcc169aa725b0ff017899ac568fe44ddc1396df46"}, + {file = "lazy_object_proxy-1.9.0-cp310-cp310-win32.whl", hash = "sha256:f0705c376533ed2a9e5e97aacdbfe04cecd71e0aa84c7c0595d02ef93b6e4455"}, + {file = "lazy_object_proxy-1.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:ea806fd4c37bf7e7ad82537b0757999264d5f70c45468447bb2b91afdbe73a6e"}, + {file = "lazy_object_proxy-1.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:946d27deaff6cf8452ed0dba83ba38839a87f4f7a9732e8f9fd4107b21e6ff07"}, + {file = "lazy_object_proxy-1.9.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79a31b086e7e68b24b99b23d57723ef7e2c6d81ed21007b6281ebcd1688acb0a"}, + {file = "lazy_object_proxy-1.9.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f699ac1c768270c9e384e4cbd268d6e67aebcfae6cd623b4d7c3bfde5a35db59"}, + {file = "lazy_object_proxy-1.9.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bfb38f9ffb53b942f2b5954e0f610f1e721ccebe9cce9025a38c8ccf4a5183a4"}, + {file = "lazy_object_proxy-1.9.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:189bbd5d41ae7a498397287c408617fe5c48633e7755287b21d741f7db2706a9"}, + {file = "lazy_object_proxy-1.9.0-cp311-cp311-win32.whl", hash = "sha256:81fc4d08b062b535d95c9ea70dbe8a335c45c04029878e62d744bdced5141586"}, + {file = "lazy_object_proxy-1.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:f2457189d8257dd41ae9b434ba33298aec198e30adf2dcdaaa3a28b9994f6adb"}, + {file = "lazy_object_proxy-1.9.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d9e25ef10a39e8afe59a5c348a4dbf29b4868ab76269f81ce1674494e2565a6e"}, + {file = "lazy_object_proxy-1.9.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cbf9b082426036e19c6924a9ce90c740a9861e2bdc27a4834fd0a910742ac1e8"}, + {file = "lazy_object_proxy-1.9.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f5fa4a61ce2438267163891961cfd5e32ec97a2c444e5b842d574251ade27d2"}, + {file = "lazy_object_proxy-1.9.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:8fa02eaab317b1e9e03f69aab1f91e120e7899b392c4fc19807a8278a07a97e8"}, + {file = "lazy_object_proxy-1.9.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e7c21c95cae3c05c14aafffe2865bbd5e377cfc1348c4f7751d9dc9a48ca4bda"}, + {file = "lazy_object_proxy-1.9.0-cp37-cp37m-win32.whl", hash = "sha256:f12ad7126ae0c98d601a7ee504c1122bcef553d1d5e0c3bfa77b16b3968d2734"}, + {file = "lazy_object_proxy-1.9.0-cp37-cp37m-win_amd64.whl", hash = "sha256:edd20c5a55acb67c7ed471fa2b5fb66cb17f61430b7a6b9c3b4a1e40293b1671"}, + {file = "lazy_object_proxy-1.9.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2d0daa332786cf3bb49e10dc6a17a52f6a8f9601b4cf5c295a4f85854d61de63"}, + {file = "lazy_object_proxy-1.9.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cd077f3d04a58e83d04b20e334f678c2b0ff9879b9375ed107d5d07ff160171"}, + {file = "lazy_object_proxy-1.9.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:660c94ea760b3ce47d1855a30984c78327500493d396eac4dfd8bd82041b22be"}, + {file = "lazy_object_proxy-1.9.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:212774e4dfa851e74d393a2370871e174d7ff0ebc980907723bb67d25c8a7c30"}, + {file = "lazy_object_proxy-1.9.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:f0117049dd1d5635bbff65444496c90e0baa48ea405125c088e93d9cf4525b11"}, + {file = "lazy_object_proxy-1.9.0-cp38-cp38-win32.whl", hash = "sha256:0a891e4e41b54fd5b8313b96399f8b0e173bbbfc03c7631f01efbe29bb0bcf82"}, + {file = "lazy_object_proxy-1.9.0-cp38-cp38-win_amd64.whl", hash = "sha256:9990d8e71b9f6488e91ad25f322898c136b008d87bf852ff65391b004da5e17b"}, + {file = "lazy_object_proxy-1.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9e7551208b2aded9c1447453ee366f1c4070602b3d932ace044715d89666899b"}, + {file = "lazy_object_proxy-1.9.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f83ac4d83ef0ab017683d715ed356e30dd48a93746309c8f3517e1287523ef4"}, + {file = "lazy_object_proxy-1.9.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7322c3d6f1766d4ef1e51a465f47955f1e8123caee67dd641e67d539a534d006"}, + {file = "lazy_object_proxy-1.9.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:18b78ec83edbbeb69efdc0e9c1cb41a3b1b1ed11ddd8ded602464c3fc6020494"}, + {file = "lazy_object_proxy-1.9.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:09763491ce220c0299688940f8dc2c5d05fd1f45af1e42e636b2e8b2303e4382"}, + {file = "lazy_object_proxy-1.9.0-cp39-cp39-win32.whl", hash = "sha256:9090d8e53235aa280fc9239a86ae3ea8ac58eff66a705fa6aa2ec4968b95c821"}, + {file = "lazy_object_proxy-1.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:db1c1722726f47e10e0b5fdbf15ac3b8adb58c091d12b3ab713965795036985f"}, +] + +[[package]] +name = "mccabe" +version = "0.7.0" +description = "McCabe checker, plugin for flake8" +optional = false +python-versions = ">=3.6" +files = [ + {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, + {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, +] + +[[package]] +name = "mypy" +version = "1.7.1" +description = "Optional static typing for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "mypy-1.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:12cce78e329838d70a204293e7b29af9faa3ab14899aec397798a4b41be7f340"}, + {file = "mypy-1.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1484b8fa2c10adf4474f016e09d7a159602f3239075c7bf9f1627f5acf40ad49"}, + {file = "mypy-1.7.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31902408f4bf54108bbfb2e35369877c01c95adc6192958684473658c322c8a5"}, + {file = "mypy-1.7.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f2c2521a8e4d6d769e3234350ba7b65ff5d527137cdcde13ff4d99114b0c8e7d"}, + {file = "mypy-1.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:fcd2572dd4519e8a6642b733cd3a8cfc1ef94bafd0c1ceed9c94fe736cb65b6a"}, + {file = "mypy-1.7.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4b901927f16224d0d143b925ce9a4e6b3a758010673eeded9b748f250cf4e8f7"}, + {file = "mypy-1.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2f7f6985d05a4e3ce8255396df363046c28bea790e40617654e91ed580ca7c51"}, + {file = "mypy-1.7.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:944bdc21ebd620eafefc090cdf83158393ec2b1391578359776c00de00e8907a"}, + {file = "mypy-1.7.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9c7ac372232c928fff0645d85f273a726970c014749b924ce5710d7d89763a28"}, + {file = "mypy-1.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:f6efc9bd72258f89a3816e3a98c09d36f079c223aa345c659622f056b760ab42"}, + {file = "mypy-1.7.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6dbdec441c60699288adf051f51a5d512b0d818526d1dcfff5a41f8cd8b4aaf1"}, + {file = "mypy-1.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4fc3d14ee80cd22367caaaf6e014494415bf440980a3045bf5045b525680ac33"}, + {file = "mypy-1.7.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c6e4464ed5f01dc44dc9821caf67b60a4e5c3b04278286a85c067010653a0eb"}, + {file = "mypy-1.7.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:d9b338c19fa2412f76e17525c1b4f2c687a55b156320acb588df79f2e6fa9fea"}, + {file = "mypy-1.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:204e0d6de5fd2317394a4eff62065614c4892d5a4d1a7ee55b765d7a3d9e3f82"}, + {file = "mypy-1.7.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:84860e06ba363d9c0eeabd45ac0fde4b903ad7aa4f93cd8b648385a888e23200"}, + {file = "mypy-1.7.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8c5091ebd294f7628eb25ea554852a52058ac81472c921150e3a61cdd68f75a7"}, + {file = "mypy-1.7.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40716d1f821b89838589e5b3106ebbc23636ffdef5abc31f7cd0266db936067e"}, + {file = "mypy-1.7.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5cf3f0c5ac72139797953bd50bc6c95ac13075e62dbfcc923571180bebb662e9"}, + {file = "mypy-1.7.1-cp38-cp38-win_amd64.whl", hash = "sha256:78e25b2fd6cbb55ddfb8058417df193f0129cad5f4ee75d1502248e588d9e0d7"}, + {file = "mypy-1.7.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:75c4d2a6effd015786c87774e04331b6da863fc3fc4e8adfc3b40aa55ab516fe"}, + {file = "mypy-1.7.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2643d145af5292ee956aa0a83c2ce1038a3bdb26e033dadeb2f7066fb0c9abce"}, + {file = "mypy-1.7.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75aa828610b67462ffe3057d4d8a4112105ed211596b750b53cbfe182f44777a"}, + {file = "mypy-1.7.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ee5d62d28b854eb61889cde4e1dbc10fbaa5560cb39780c3995f6737f7e82120"}, + {file = "mypy-1.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:72cf32ce7dd3562373f78bd751f73c96cfb441de147cc2448a92c1a308bd0ca6"}, + {file = "mypy-1.7.1-py3-none-any.whl", hash = "sha256:f7c5d642db47376a0cc130f0de6d055056e010debdaf0707cd2b0fc7e7ef30ea"}, + {file = "mypy-1.7.1.tar.gz", hash = "sha256:fcb6d9afb1b6208b4c712af0dafdc650f518836065df0d4fb1d800f5d6773db2"}, +] + +[package.dependencies] +mypy-extensions = ">=1.0.0" +typing-extensions = ">=4.1.0" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +install-types = ["pip"] +mypyc = ["setuptools (>=50)"] +reports = ["lxml"] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.5" +files = [ + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, +] + +[[package]] +name = "nodeenv" +version = "1.8.0" +description = "Node.js virtual environment builder" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" +files = [ + {file = "nodeenv-1.8.0-py2.py3-none-any.whl", hash = "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec"}, + {file = "nodeenv-1.8.0.tar.gz", hash = "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2"}, +] + +[package.dependencies] +setuptools = "*" + +[[package]] +name = "packaging" +version = "23.2" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.7" +files = [ + {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, + {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, +] + +[[package]] +name = "pathspec" +version = "0.12.1" +description = "Utility library for gitignore style pattern matching of file paths." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, + {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, +] + +[[package]] +name = "platformdirs" +version = "4.1.0" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +optional = false +python-versions = ">=3.8" +files = [ + {file = "platformdirs-4.1.0-py3-none-any.whl", hash = "sha256:11c8f37bcca40db96d8144522d925583bdb7a31f7b0e37e3ed4318400a8e2380"}, + {file = "platformdirs-4.1.0.tar.gz", hash = "sha256:906d548203468492d432bcb294d4bc2fff751bf84971fbb2c10918cc206ee420"}, +] + +[package.extras] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.1)", "sphinx-autodoc-typehints (>=1.24)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)"] + +[[package]] +name = "pluggy" +version = "1.3.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pluggy-1.3.0-py3-none-any.whl", hash = "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7"}, + {file = "pluggy-1.3.0.tar.gz", hash = "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "pre-commit" +version = "3.6.0" +description = "A framework for managing and maintaining multi-language pre-commit hooks." +optional = false +python-versions = ">=3.9" +files = [ + {file = "pre_commit-3.6.0-py2.py3-none-any.whl", hash = "sha256:c255039ef399049a5544b6ce13d135caba8f2c28c3b4033277a788f434308376"}, + {file = "pre_commit-3.6.0.tar.gz", hash = "sha256:d30bad9abf165f7785c15a21a1f46da7d0677cb00ee7ff4c579fd38922efe15d"}, +] + +[package.dependencies] +cfgv = ">=2.0.0" +identify = ">=1.0.0" +nodeenv = ">=0.11.1" +pyyaml = ">=5.1" +virtualenv = ">=20.10.0" + +[[package]] +name = "pycodestyle" +version = "2.11.1" +description = "Python style guide checker" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pycodestyle-2.11.1-py2.py3-none-any.whl", hash = "sha256:44fe31000b2d866f2e41841b18528a505fbd7fef9017b04eff4e2648a0fadc67"}, + {file = "pycodestyle-2.11.1.tar.gz", hash = "sha256:41ba0e7afc9752dfb53ced5489e89f8186be00e599e712660695b7a75ff2663f"}, +] + +[[package]] +name = "pyflakes" +version = "3.1.0" +description = "passive checker of Python programs" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pyflakes-3.1.0-py2.py3-none-any.whl", hash = "sha256:4132f6d49cb4dae6819e5379898f2b8cce3c5f23994194c24b77d5da2e36f774"}, + {file = "pyflakes-3.1.0.tar.gz", hash = "sha256:a0aae034c444db0071aa077972ba4768d40c830d9539fd45bf4cd3f8f6992efc"}, +] + +[[package]] +name = "pylint" +version = "2.17.7" +description = "python code static checker" +optional = false +python-versions = ">=3.7.2" +files = [ + {file = "pylint-2.17.7-py3-none-any.whl", hash = "sha256:27a8d4c7ddc8c2f8c18aa0050148f89ffc09838142193fdbe98f172781a3ff87"}, + {file = "pylint-2.17.7.tar.gz", hash = "sha256:f4fcac7ae74cfe36bc8451e931d8438e4a476c20314b1101c458ad0f05191fad"}, +] + +[package.dependencies] +astroid = ">=2.15.8,<=2.17.0-dev0" +colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} +dill = {version = ">=0.3.6", markers = "python_version >= \"3.11\""} +isort = ">=4.2.5,<6" +mccabe = ">=0.6,<0.8" +platformdirs = ">=2.2.0" +tomlkit = ">=0.10.1" + +[package.extras] +spelling = ["pyenchant (>=3.2,<4.0)"] +testutils = ["gitpython (>3)"] + +[[package]] +name = "pytest" +version = "7.4.3" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-7.4.3-py3-none-any.whl", hash = "sha256:0d009c083ea859a71b76adf7c1d502e4bc170b80a8ef002da5806527b9591fac"}, + {file = "pytest-7.4.3.tar.gz", hash = "sha256:d989d136982de4e3b29dabcc838ad581c64e8ed52c11fbe86ddebd9da0818cd5"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" + +[package.extras] +testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytest-cov" +version = "4.1.0" +description = "Pytest plugin for measuring coverage." +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"}, + {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"}, +] + +[package.dependencies] +coverage = {version = ">=5.2.1", extras = ["toml"]} +pytest = ">=4.6" + +[package.extras] +testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] + +[[package]] +name = "pyyaml" +version = "6.0.1" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.6" +files = [ + {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, + {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, + {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, + {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, + {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, + {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, + {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, + {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, + {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, + {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, + {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, + {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, + {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, +] + +[[package]] +name = "quickfix" +version = "1.15.1" +description = "FIX (Financial Information eXchange) protocol implementation" +optional = false +python-versions = "*" +files = [ + {file = "quickfix-1.15.1.tar.gz", hash = "sha256:e3564ad2dd155892dc74496d6f4db1794bd8f9f74450ccbb45571ffaa2815da2"}, +] + +[[package]] +name = "setuptools" +version = "69.0.2" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "setuptools-69.0.2-py3-none-any.whl", hash = "sha256:1e8fdff6797d3865f37397be788a4e3cba233608e9b509382a2777d25ebde7f2"}, + {file = "setuptools-69.0.2.tar.gz", hash = "sha256:735896e78a4742605974de002ac60562d286fa8051a7e2299445e8e8fbb01aa6"}, +] + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.1)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] + +[[package]] +name = "tomlkit" +version = "0.12.3" +description = "Style preserving TOML library" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tomlkit-0.12.3-py3-none-any.whl", hash = "sha256:b0a645a9156dc7cb5d3a1f0d4bab66db287fcb8e0430bdd4664a095ea16414ba"}, + {file = "tomlkit-0.12.3.tar.gz", hash = "sha256:75baf5012d06501f07bee5bf8e801b9f343e7aac5a92581f20f80ce632e6b5a4"}, +] + +[[package]] +name = "types-pyyaml" +version = "6.0.12.12" +description = "Typing stubs for PyYAML" +optional = false +python-versions = "*" +files = [ + {file = "types-PyYAML-6.0.12.12.tar.gz", hash = "sha256:334373d392fde0fdf95af5c3f1661885fa10c52167b14593eb856289e1855062"}, + {file = "types_PyYAML-6.0.12.12-py3-none-any.whl", hash = "sha256:c05bc6c158facb0676674b7f11fe3960db4f389718e19e62bd2b84d6205cfd24"}, +] + +[[package]] +name = "typing-extensions" +version = "4.9.0" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +files = [ + {file = "typing_extensions-4.9.0-py3-none-any.whl", hash = "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd"}, + {file = "typing_extensions-4.9.0.tar.gz", hash = "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783"}, +] + +[[package]] +name = "virtualenv" +version = "20.25.0" +description = "Virtual Python Environment builder" +optional = false +python-versions = ">=3.7" +files = [ + {file = "virtualenv-20.25.0-py3-none-any.whl", hash = "sha256:4238949c5ffe6876362d9c0180fc6c3a824a7b12b80604eeb8085f2ed7460de3"}, + {file = "virtualenv-20.25.0.tar.gz", hash = "sha256:bf51c0d9c7dd63ea8e44086fa1e4fb1093a31e963b86959257378aef020e1f1b"}, +] + +[package.dependencies] +distlib = ">=0.3.7,<1" +filelock = ">=3.12.2,<4" +platformdirs = ">=3.9.1,<5" + +[package.extras] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] +test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] + +[[package]] +name = "wrapt" +version = "1.16.0" +description = "Module for decorators, wrappers and monkey patching." +optional = false +python-versions = ">=3.6" +files = [ + {file = "wrapt-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ffa565331890b90056c01db69c0fe634a776f8019c143a5ae265f9c6bc4bd6d4"}, + {file = "wrapt-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e4fdb9275308292e880dcbeb12546df7f3e0f96c6b41197e0cf37d2826359020"}, + {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb2dee3874a500de01c93d5c71415fcaef1d858370d405824783e7a8ef5db440"}, + {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2a88e6010048489cda82b1326889ec075a8c856c2e6a256072b28eaee3ccf487"}, + {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac83a914ebaf589b69f7d0a1277602ff494e21f4c2f743313414378f8f50a4cf"}, + {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:73aa7d98215d39b8455f103de64391cb79dfcad601701a3aa0dddacf74911d72"}, + {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:807cc8543a477ab7422f1120a217054f958a66ef7314f76dd9e77d3f02cdccd0"}, + {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bf5703fdeb350e36885f2875d853ce13172ae281c56e509f4e6eca049bdfb136"}, + {file = "wrapt-1.16.0-cp310-cp310-win32.whl", hash = "sha256:f6b2d0c6703c988d334f297aa5df18c45e97b0af3679bb75059e0e0bd8b1069d"}, + {file = "wrapt-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:decbfa2f618fa8ed81c95ee18a387ff973143c656ef800c9f24fb7e9c16054e2"}, + {file = "wrapt-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a5db485fe2de4403f13fafdc231b0dbae5eca4359232d2efc79025527375b09"}, + {file = "wrapt-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:75ea7d0ee2a15733684badb16de6794894ed9c55aa5e9903260922f0482e687d"}, + {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a452f9ca3e3267cd4d0fcf2edd0d035b1934ac2bd7e0e57ac91ad6b95c0c6389"}, + {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:43aa59eadec7890d9958748db829df269f0368521ba6dc68cc172d5d03ed8060"}, + {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72554a23c78a8e7aa02abbd699d129eead8b147a23c56e08d08dfc29cfdddca1"}, + {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d2efee35b4b0a347e0d99d28e884dfd82797852d62fcd7ebdeee26f3ceb72cf3"}, + {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:6dcfcffe73710be01d90cae08c3e548d90932d37b39ef83969ae135d36ef3956"}, + {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:eb6e651000a19c96f452c85132811d25e9264d836951022d6e81df2fff38337d"}, + {file = "wrapt-1.16.0-cp311-cp311-win32.whl", hash = "sha256:66027d667efe95cc4fa945af59f92c5a02c6f5bb6012bff9e60542c74c75c362"}, + {file = "wrapt-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:aefbc4cb0a54f91af643660a0a150ce2c090d3652cf4052a5397fb2de549cd89"}, + {file = "wrapt-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5eb404d89131ec9b4f748fa5cfb5346802e5ee8836f57d516576e61f304f3b7b"}, + {file = "wrapt-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9090c9e676d5236a6948330e83cb89969f433b1943a558968f659ead07cb3b36"}, + {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94265b00870aa407bd0cbcfd536f17ecde43b94fb8d228560a1e9d3041462d73"}, + {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2058f813d4f2b5e3a9eb2eb3faf8f1d99b81c3e51aeda4b168406443e8ba809"}, + {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98b5e1f498a8ca1858a1cdbffb023bfd954da4e3fa2c0cb5853d40014557248b"}, + {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:14d7dc606219cdd7405133c713f2c218d4252f2a469003f8c46bb92d5d095d81"}, + {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:49aac49dc4782cb04f58986e81ea0b4768e4ff197b57324dcbd7699c5dfb40b9"}, + {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:418abb18146475c310d7a6dc71143d6f7adec5b004ac9ce08dc7a34e2babdc5c"}, + {file = "wrapt-1.16.0-cp312-cp312-win32.whl", hash = "sha256:685f568fa5e627e93f3b52fda002c7ed2fa1800b50ce51f6ed1d572d8ab3e7fc"}, + {file = "wrapt-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:dcdba5c86e368442528f7060039eda390cc4091bfd1dca41e8046af7c910dda8"}, + {file = "wrapt-1.16.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d462f28826f4657968ae51d2181a074dfe03c200d6131690b7d65d55b0f360f8"}, + {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a33a747400b94b6d6b8a165e4480264a64a78c8a4c734b62136062e9a248dd39"}, + {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3646eefa23daeba62643a58aac816945cadc0afaf21800a1421eeba5f6cfb9c"}, + {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ebf019be5c09d400cf7b024aa52b1f3aeebeff51550d007e92c3c1c4afc2a40"}, + {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:0d2691979e93d06a95a26257adb7bfd0c93818e89b1406f5a28f36e0d8c1e1fc"}, + {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:1acd723ee2a8826f3d53910255643e33673e1d11db84ce5880675954183ec47e"}, + {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:bc57efac2da352a51cc4658878a68d2b1b67dbe9d33c36cb826ca449d80a8465"}, + {file = "wrapt-1.16.0-cp36-cp36m-win32.whl", hash = "sha256:da4813f751142436b075ed7aa012a8778aa43a99f7b36afe9b742d3ed8bdc95e"}, + {file = "wrapt-1.16.0-cp36-cp36m-win_amd64.whl", hash = "sha256:6f6eac2360f2d543cc875a0e5efd413b6cbd483cb3ad7ebf888884a6e0d2e966"}, + {file = "wrapt-1.16.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a0ea261ce52b5952bf669684a251a66df239ec6d441ccb59ec7afa882265d593"}, + {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bd2d7ff69a2cac767fbf7a2b206add2e9a210e57947dd7ce03e25d03d2de292"}, + {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9159485323798c8dc530a224bd3ffcf76659319ccc7bbd52e01e73bd0241a0c5"}, + {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a86373cf37cd7764f2201b76496aba58a52e76dedfaa698ef9e9688bfd9e41cf"}, + {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:73870c364c11f03ed072dda68ff7aea6d2a3a5c3fe250d917a429c7432e15228"}, + {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b935ae30c6e7400022b50f8d359c03ed233d45b725cfdd299462f41ee5ffba6f"}, + {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:db98ad84a55eb09b3c32a96c576476777e87c520a34e2519d3e59c44710c002c"}, + {file = "wrapt-1.16.0-cp37-cp37m-win32.whl", hash = "sha256:9153ed35fc5e4fa3b2fe97bddaa7cbec0ed22412b85bcdaf54aeba92ea37428c"}, + {file = "wrapt-1.16.0-cp37-cp37m-win_amd64.whl", hash = "sha256:66dfbaa7cfa3eb707bbfcd46dab2bc6207b005cbc9caa2199bcbc81d95071a00"}, + {file = "wrapt-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1dd50a2696ff89f57bd8847647a1c363b687d3d796dc30d4dd4a9d1689a706f0"}, + {file = "wrapt-1.16.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:44a2754372e32ab315734c6c73b24351d06e77ffff6ae27d2ecf14cf3d229202"}, + {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e9723528b9f787dc59168369e42ae1c3b0d3fadb2f1a71de14531d321ee05b0"}, + {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dbed418ba5c3dce92619656802cc5355cb679e58d0d89b50f116e4a9d5a9603e"}, + {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:941988b89b4fd6b41c3f0bfb20e92bd23746579736b7343283297c4c8cbae68f"}, + {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6a42cd0cfa8ffc1915aef79cb4284f6383d8a3e9dcca70c445dcfdd639d51267"}, + {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1ca9b6085e4f866bd584fb135a041bfc32cab916e69f714a7d1d397f8c4891ca"}, + {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d5e49454f19ef621089e204f862388d29e6e8d8b162efce05208913dde5b9ad6"}, + {file = "wrapt-1.16.0-cp38-cp38-win32.whl", hash = "sha256:c31f72b1b6624c9d863fc095da460802f43a7c6868c5dda140f51da24fd47d7b"}, + {file = "wrapt-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:490b0ee15c1a55be9c1bd8609b8cecd60e325f0575fc98f50058eae366e01f41"}, + {file = "wrapt-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9b201ae332c3637a42f02d1045e1d0cccfdc41f1f2f801dafbaa7e9b4797bfc2"}, + {file = "wrapt-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2076fad65c6736184e77d7d4729b63a6d1ae0b70da4868adeec40989858eb3fb"}, + {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5cd603b575ebceca7da5a3a251e69561bec509e0b46e4993e1cac402b7247b8"}, + {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b47cfad9e9bbbed2339081f4e346c93ecd7ab504299403320bf85f7f85c7d46c"}, + {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8212564d49c50eb4565e502814f694e240c55551a5f1bc841d4fcaabb0a9b8a"}, + {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5f15814a33e42b04e3de432e573aa557f9f0f56458745c2074952f564c50e664"}, + {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db2e408d983b0e61e238cf579c09ef7020560441906ca990fe8412153e3b291f"}, + {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:edfad1d29c73f9b863ebe7082ae9321374ccb10879eeabc84ba3b69f2579d537"}, + {file = "wrapt-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed867c42c268f876097248e05b6117a65bcd1e63b779e916fe2e33cd6fd0d3c3"}, + {file = "wrapt-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:eb1b046be06b0fce7249f1d025cd359b4b80fc1c3e24ad9eca33e0dcdb2e4a35"}, + {file = "wrapt-1.16.0-py3-none-any.whl", hash = "sha256:6906c4100a8fcbf2fa735f6059214bb13b97f75b1a61777fcf6432121ef12ef1"}, + {file = "wrapt-1.16.0.tar.gz", hash = "sha256:5f370f952971e7d17c7d1ead40e49f32345a7f7a5373571ef44d800d06b1899d"}, +] + +[metadata] +lock-version = "2.0" +python-versions = "~3.11" +content-hash = "379bdd5bb8b8cc37b99e49123a5ffcf96abd4fbdbedfd96b2125aa2d39ca6503" diff --git a/clients/quickfix-client/poetry.toml b/clients/quickfix-client/poetry.toml new file mode 100644 index 00000000..53b35d37 --- /dev/null +++ b/clients/quickfix-client/poetry.toml @@ -0,0 +1,3 @@ +[virtualenvs] +create = true +in-project = true diff --git a/clients/quickfix-client/project.json b/clients/quickfix-client/project.json new file mode 100644 index 00000000..44139c87 --- /dev/null +++ b/clients/quickfix-client/project.json @@ -0,0 +1,66 @@ +{ + "name": "quickfix-client", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "projectType": "application", + "sourceRoot": "./clients/quickfix-client/broker_quickfix_client", + "targets": { + "lock": { + "executor": "@nxlv/python:run-commands", + "options": { + "command": "poetry lock --no-update", + "cwd": "clients/quickfix-client" + } + }, + "add": { + "executor": "@nxlv/python:add", + "options": {} + }, + "update": { + "executor": "@nxlv/python:update", + "options": {} + }, + "remove": { + "executor": "@nxlv/python:remove", + "options": {} + }, + "build": { + "executor": "@nxlv/python:build", + "outputs": ["{projectRoot}/dist"], + "options": { + "outputPath": "quickfix-client/dist", + "publish": false, + "lockedVersions": true, + "bundleLocalDependencies": true + } + }, + "install": { + "executor": "@nxlv/python:install", + "options": { + "silent": false, + "args": "", + "cacheDir": ".cache/pypoetry", + "verbose": false, + "debug": false + } + }, + "lint": { + "executor": "@nxlv/python:flake8", + "outputs": ["{workspaceRoot}/reports/quickfix-client/pylint.txt"], + "options": { + "outputFile": "reports/quickfix-client/pylint.txt" + } + }, + "test": { + "executor": "@nxlv/python:run-commands", + "outputs": [ + "{workspaceRoot}/reports/quickfix-client/unittests", + "{workspaceRoot}/coverage/quickfix-client" + ], + "options": { + "command": "poetry run pytest tests/", + "cwd": "components/quickfix-client" + } + } + }, + "tags": ["lang:python", "type:app", "scope:quickfix-client"] +} diff --git a/clients/quickfix-client/pyproject.toml b/clients/quickfix-client/pyproject.toml new file mode 100644 index 00000000..f82a429b --- /dev/null +++ b/clients/quickfix-client/pyproject.toml @@ -0,0 +1,36 @@ +[tool.poetry] +name = "broker-quickfix-client" +version = "0.3.0" +description = "" +authors = ["Samuel Guillemet "] +readme = "README.md" +packages = [{ include = "broker_quickfix_client" }] + +[tool.poetry.dependencies] +python = "~3.11" +quickfix = "^1.15.1" + +[tool.poetry.group.dev.dependencies] +black = "^23.7.0" +flake8 = "^6.0.0" +isort = "^5.12.0" +pre-commit = "^3.3.3" +mypy = "^1.4.1" +mypy-extensions = "^1.0.0" +pylint = "^2.17.4" +pytest = "^7.4.0" +pytest-cov = "^4.1.0" +types-pyyaml = "^6.0.12.12" + + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" + +[tool.isort] +profile = "black" + +[tool.mypy] +no_strict_optional = true +ignore_missing_imports = true +files = "^(broker_quickfix_client/|tests/)" diff --git a/clients/quickfix-client/requirements.dev.txt b/clients/quickfix-client/requirements.dev.txt new file mode 100644 index 00000000..f4f2fb2c --- /dev/null +++ b/clients/quickfix-client/requirements.dev.txt @@ -0,0 +1,35 @@ +astroid==2.15.8 ; python_version >= "3.11" and python_version < "3.12" +black==23.12.0 ; python_version >= "3.11" and python_version < "3.12" +cfgv==3.4.0 ; python_version >= "3.11" and python_version < "3.12" +click==8.1.7 ; python_version >= "3.11" and python_version < "3.12" +colorama==0.4.6 ; python_version >= "3.11" and python_version < "3.12" and (sys_platform == "win32" or platform_system == "Windows") +coverage[toml]==7.3.2 ; python_version >= "3.11" and python_version < "3.12" +dill==0.3.7 ; python_version >= "3.11" and python_version < "3.12" +distlib==0.3.8 ; python_version >= "3.11" and python_version < "3.12" +filelock==3.13.1 ; python_version >= "3.11" and python_version < "3.12" +flake8==6.1.0 ; python_version >= "3.11" and python_version < "3.12" +identify==2.5.33 ; python_version >= "3.11" and python_version < "3.12" +iniconfig==2.0.0 ; python_version >= "3.11" and python_version < "3.12" +isort==5.13.1 ; python_version >= "3.11" and python_version < "3.12" +lazy-object-proxy==1.9.0 ; python_version >= "3.11" and python_version < "3.12" +mccabe==0.7.0 ; python_version >= "3.11" and python_version < "3.12" +mypy-extensions==1.0.0 ; python_version >= "3.11" and python_version < "3.12" +mypy==1.7.1 ; python_version >= "3.11" and python_version < "3.12" +nodeenv==1.8.0 ; python_version >= "3.11" and python_version < "3.12" +packaging==23.2 ; python_version >= "3.11" and python_version < "3.12" +pathspec==0.12.1 ; python_version >= "3.11" and python_version < "3.12" +platformdirs==4.1.0 ; python_version >= "3.11" and python_version < "3.12" +pluggy==1.3.0 ; python_version >= "3.11" and python_version < "3.12" +pre-commit==3.6.0 ; python_version >= "3.11" and python_version < "3.12" +pycodestyle==2.11.1 ; python_version >= "3.11" and python_version < "3.12" +pyflakes==3.1.0 ; python_version >= "3.11" and python_version < "3.12" +pylint==2.17.7 ; python_version >= "3.11" and python_version < "3.12" +pytest-cov==4.1.0 ; python_version >= "3.11" and python_version < "3.12" +pytest==7.4.3 ; python_version >= "3.11" and python_version < "3.12" +pyyaml==6.0.1 ; python_version >= "3.11" and python_version < "3.12" +setuptools==69.0.2 ; python_version >= "3.11" and python_version < "3.12" +tomlkit==0.12.3 ; python_version >= "3.11" and python_version < "3.12" +types-pyyaml==6.0.12.12 ; python_version >= "3.11" and python_version < "3.12" +typing-extensions==4.9.0 ; python_version >= "3.11" and python_version < "3.12" +virtualenv==20.25.0 ; python_version >= "3.11" and python_version < "3.12" +wrapt==1.16.0 ; python_version >= "3.11" and python_version < "3.12" diff --git a/clients/quickfix-client/requirements.txt b/clients/quickfix-client/requirements.txt new file mode 100644 index 00000000..9e73a46a --- /dev/null +++ b/clients/quickfix-client/requirements.txt @@ -0,0 +1 @@ +quickfix==1.15.1 ; python_version >= "3.11" and python_version < "3.12" diff --git a/clients/quickfix-client/tests/__init__.py b/clients/quickfix-client/tests/__init__.py new file mode 100644 index 00000000..d8e96c60 --- /dev/null +++ b/clients/quickfix-client/tests/__init__.py @@ -0,0 +1 @@ +"""unit tests.""" diff --git a/clients/quickfix-client/tests/conftest.py b/clients/quickfix-client/tests/conftest.py new file mode 100644 index 00000000..547aa995 --- /dev/null +++ b/clients/quickfix-client/tests/conftest.py @@ -0,0 +1,3 @@ +"""Unit tests configuration module.""" + +pytest_plugins = [] diff --git a/components/market-matcher/build.gradle b/components/market-matcher/build.gradle index 8f1d5f6e..eba25d8c 100644 --- a/components/market-matcher/build.gradle +++ b/components/market-matcher/build.gradle @@ -3,6 +3,10 @@ plugins { id "io.micronaut.application" } +ext { + configFiles = "classpath:application.yml,classpath:kafka.yml" +} + version = "${version}" group = "pfe_broker" @@ -23,13 +27,13 @@ dependencies { // Test dependencies testImplementation group: 'org.awaitility', name: 'awaitility', version: '4.2.0' - testImplementation group: 'org.assertj', name: 'assertj-core', version: '3.24.2' + testImplementation group: 'org.assertj', name: 'assertj-core', version: '3.25.1' testImplementation group: 'org.testcontainers', name: 'junit-jupiter', version: '1.19.3' // Log4J - implementation group: 'org.apache.logging.log4j', name: 'log4j-core', version: '2.22.0' - runtimeOnly group: 'org.apache.logging.log4j', name: 'log4j-api', version: '2.22.0' - runtimeOnly group: 'org.apache.logging.log4j', name: 'log4j-slf4j2-impl', version: '2.22.0' + implementation group: 'org.apache.logging.log4j', name: 'log4j-core', version: '2.22.1' + runtimeOnly group: 'org.apache.logging.log4j', name: 'log4j-api', version: '2.22.1' + runtimeOnly group: 'org.apache.logging.log4j', name: 'log4j-slf4j2-impl', version: '2.22.1' // Avro implementation group: 'io.confluent', name: 'kafka-avro-serializer', version: '7.5.1' @@ -73,3 +77,9 @@ test { testLogging.showStandardStreams = true testLogging.exceptionFormat = 'full' } + +[Test, JavaExec].each { targetType -> + tasks.withType(targetType) { task -> + task.systemProperty "micronaut.config.files", configFiles + } +} diff --git a/components/market-matcher/src/main/java/pfe_broker/market_matcher/Application.java b/components/market-matcher/src/main/java/pfe_broker/market_matcher/Application.java index 0d4e2e6a..0353f9bc 100644 --- a/components/market-matcher/src/main/java/pfe_broker/market_matcher/Application.java +++ b/components/market-matcher/src/main/java/pfe_broker/market_matcher/Application.java @@ -5,9 +5,6 @@ import org.slf4j.LoggerFactory; public class Application { - static { - setProperties(); - } private static Logger LOG = LoggerFactory.getLogger(Application.class); @@ -15,11 +12,4 @@ public static void main(String[] args) { LOG.info("Starting Market Matcher"); Micronaut.run(Application.class, args); } - - public static void setProperties() { - System.setProperty( - "micronaut.config.files", - "classpath:application.yml,classpath:kafka.yml" - ); - } } diff --git a/components/market-matcher/src/main/java/pfe_broker/market_matcher/MarketDataConsumer.java b/components/market-matcher/src/main/java/pfe_broker/market_matcher/MarketDataConsumer.java index 74d1de37..ec122715 100644 --- a/components/market-matcher/src/main/java/pfe_broker/market_matcher/MarketDataConsumer.java +++ b/components/market-matcher/src/main/java/pfe_broker/market_matcher/MarketDataConsumer.java @@ -2,11 +2,16 @@ import io.confluent.kafka.serializers.KafkaAvroDeserializer; import io.confluent.kafka.serializers.KafkaAvroDeserializerConfig; +import io.micronaut.configuration.kafka.annotation.KafkaListener; +import io.micronaut.configuration.kafka.annotation.Topic; import io.micronaut.context.annotation.Property; import io.micronaut.context.env.Environment; import jakarta.inject.Singleton; import java.time.Duration; +import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Properties; import org.apache.kafka.clients.consumer.ConsumerConfig; import org.apache.kafka.clients.consumer.ConsumerRecord; @@ -28,6 +33,8 @@ public class MarketDataConsumer { private final Environment environment; private final KafkaConsumer consumer; + private Map marketDataMap; + @Property(name = "kafka.common.symbol-topic-prefix") private String symbolTopicPrefix; @@ -58,9 +65,33 @@ protected Properties buildProperties() { public MarketDataConsumer(Environment environment) { this.environment = environment; this.consumer = new KafkaConsumer<>(this.buildProperties()); + this.marketDataMap = Collections.synchronizedMap(new HashMap<>()); + } + + @KafkaListener( + groupId = "market-matcher-market-data", + batch = true, + threadsValue = "${kafka.common.market-data-thread-pool-size}" + ) + @Topic(patterns = "${kafka.common.symbol-topic-prefix}[A-Z]+") + public void receiveMarketData( + List> records + ) { + records.forEach(record -> { + MarketData marketData = record.value(); + String symbol = record.topic().substring(symbolTopicPrefix.length()); + marketDataMap.put(symbol, marketData); + }); } public MarketData readLastStockData(String symbol) { + if (!marketDataMap.containsKey(symbol)) { + readLastStockDataBis(symbol); + } + return marketDataMap.get(symbol); + } + + private void readLastStockDataBis(String symbol) { LOG.debug("Reading last stock data for {}", symbol); List partitions = consumer @@ -76,6 +107,9 @@ public MarketData readLastStockData(String symbol) { for (TopicPartition partition : partitions) { long offset = consumer.position(partition) - 1; + if (offset < 0) { + return; + } consumer.seek(partition, offset); } @@ -90,6 +124,7 @@ public MarketData readLastStockData(String symbol) { } } - return stockData == null ? null : stockData.value(); + MarketData marketData = stockData == null ? null : stockData.value(); + marketDataMap.put(symbol, marketData); } } diff --git a/components/market-matcher/src/test/java/pfe_broker/market_matcher/MarketMatcherTest.java b/components/market-matcher/src/test/java/pfe_broker/market_matcher/MarketMatcherTest.java index 83dd4467..0abfc233 100644 --- a/components/market-matcher/src/test/java/pfe_broker/market_matcher/MarketMatcherTest.java +++ b/components/market-matcher/src/test/java/pfe_broker/market_matcher/MarketMatcherTest.java @@ -18,6 +18,7 @@ import pfe_broker.avro.MarketData; import pfe_broker.avro.Order; import pfe_broker.avro.Side; +import pfe_broker.avro.Type; import pfe_broker.common.utils.KafkaTestContainer; import pfe_broker.market_matcher.mocks.MockMarketDataProducer; import pfe_broker.market_matcher.mocks.MockOrderProducer; @@ -27,9 +28,6 @@ @Testcontainers(disabledWithoutDocker = true) @TestInstance(TestInstance.Lifecycle.PER_CLASS) class MarketMatcherTest implements TestPropertyProvider { - static { - Application.setProperties(); - } @Container static final KafkaTestContainer kafka = new KafkaTestContainer(); @@ -44,6 +42,7 @@ class MarketMatcherTest implements TestPropertyProvider { if (!kafka.isRunning()) { kafka.start(); } + kafka.registerTopics("market-data.AAPL", "accepted-orders", "trades"); return Map.of( "kafka.bootstrap.servers", kafka.getBootstrapServers(), @@ -83,7 +82,15 @@ void testOrderConsumer( // Assert that AAPL is in the list of symbols of the order consumer assertThat(marketMatcher.symbols).contains("AAPL"); // Given - Order order = new Order("user", "AAPL", 10, Side.BUY); + Order order = new Order( + "user", + "AAPL", + 10, + Side.BUY, + Type.MARKET, + null, + "1" + ); // When mockOrderProducer.sendOrder("user", order); @@ -106,7 +113,15 @@ void testOrderConsumerWithUnknownSymbol( MockOrderProducer mockOrderProducer ) { // Given - Order order = new Order("user", "UNKNOWN", 10, Side.BUY); + Order order = new Order( + "user", + "UNKNOWN", + 10, + Side.BUY, + Type.MARKET, + null, + "1" + ); // When mockOrderProducer.sendOrder("user", order); diff --git a/components/order-book/build.gradle b/components/order-book/build.gradle new file mode 100644 index 00000000..48518423 --- /dev/null +++ b/components/order-book/build.gradle @@ -0,0 +1,99 @@ +plugins { + id "com.github.johnrengelman.shadow" + id "io.micronaut.application" + id "jacoco" +} + +ext { + configFiles = "classpath:application.yml,classpath:kafka.yml,classpath:redis.yml" +} + +version = "${version}" +group = "pfe_broker" + +repositories { + mavenCentral() + maven { url confluentUrl } +} + +dependencies { + implementation project(":libs:log") + implementation project(":libs:avro") + implementation project(":libs:common") + + runtimeOnly group: 'org.yaml', name: 'snakeyaml', version: '2.2' + implementation group: 'io.micronaut.kafka', name: 'micronaut-kafka', version: '5.2.0' + implementation group: 'io.micronaut.redis', name: 'micronaut-redis-lettuce', version: '6.2.0' + + // Test dependencies + testImplementation group: 'org.awaitility', name: 'awaitility', version: '4.2.0' + testImplementation group: 'org.assertj', name: 'assertj-core', version: '3.25.1' + testImplementation group: 'org.testcontainers', name: 'junit-jupiter', version: '1.19.3' + + // Log4J + implementation group: 'org.apache.logging.log4j', name: 'log4j-core', version: '2.22.1' + runtimeOnly group: 'org.apache.logging.log4j', name: 'log4j-api', version: '2.22.1' + runtimeOnly group: 'org.apache.logging.log4j', name: 'log4j-slf4j2-impl', version: '2.22.1' + + // Avro + implementation group: 'io.confluent', name: 'kafka-avro-serializer', version: '7.5.1' +} + +application { + mainClass.set("pfe_broker.order_book.Application") +} + +sourceSets { + main { + resources { + srcDirs = ["src/main/resources", project(":").file("config/common").path] + } + } + test { + resources { + srcDirs = ["src/test/resources", project(":").file("config/common").path] + } + } +} + +java { + sourceCompatibility = JavaVersion.toVersion("${javaVersion}") + targetCompatibility = JavaVersion.toVersion("${javaVersion}") +} + +graalvmNative.toolchainDetection = false + +micronaut { + testRuntime("junit5") + processing { + incremental(true) + annotations("pfe_broker.order_book.*") + } +} + + +test { + testLogging.showStandardStreams = true + testLogging.exceptionFormat = 'full' +} + +[Test, JavaExec].each { targetType -> + tasks.withType(targetType) { task -> + task.systemProperty "micronaut.config.files", configFiles + } +} + +jacocoTestReport { + dependsOn test + reports { + xml.required = false + csv.required = false + } + afterEvaluate { + classDirectories.setFrom(files(classDirectories.files.collect { + fileTree(dir: it, exclude: [ + '**/Application.class', + ]) + })) + } +} diff --git a/components/order-book/project.json b/components/order-book/project.json new file mode 100644 index 00000000..46468cd9 --- /dev/null +++ b/components/order-book/project.json @@ -0,0 +1,35 @@ +{ + "name": "order-book", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "projectType": "application", + "sourceRoot": "./components/order-book/src", + "targets": { + "build": { + "executor": "@jnxplus/nx-gradle:run-task", + "outputs": ["{projectRoot}/build"], + "options": { + "task": "build" + } + }, + "build-image": { + "executor": "@jnxplus/nx-gradle:run-task", + "options": { + "task": "dockerBuild" + } + }, + "serve": { + "executor": "@jnxplus/nx-gradle:run-task", + "options": { + "task": "run", + "keepItRunning": true + } + }, + "test": { + "executor": "@jnxplus/nx-gradle:run-task", + "options": { + "task": "test" + } + } + }, + "tags": [] +} diff --git a/components/order-book/src/main/java/pfe_broker/order_book/Application.java b/components/order-book/src/main/java/pfe_broker/order_book/Application.java new file mode 100644 index 00000000..cd391a7e --- /dev/null +++ b/components/order-book/src/main/java/pfe_broker/order_book/Application.java @@ -0,0 +1,15 @@ +package pfe_broker.order_book; + +import io.micronaut.runtime.Micronaut; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class Application { + + private static final Logger LOG = LoggerFactory.getLogger(Application.class); + + public static void main(String[] args) { + LOG.info("Starting Order Book"); + Micronaut.run(Application.class, args); + } +} diff --git a/components/order-book/src/main/java/pfe_broker/order_book/IntegrityCheckService.java b/components/order-book/src/main/java/pfe_broker/order_book/IntegrityCheckService.java new file mode 100644 index 00000000..25b64756 --- /dev/null +++ b/components/order-book/src/main/java/pfe_broker/order_book/IntegrityCheckService.java @@ -0,0 +1,128 @@ +package pfe_broker.order_book; + +import io.lettuce.core.RedisClient; +import io.lettuce.core.api.StatefulRedisConnection; +import io.lettuce.core.api.sync.RedisCommands; +import io.micronaut.context.annotation.Property; +import jakarta.annotation.PostConstruct; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import pfe_broker.avro.Order; +import pfe_broker.avro.Side; +import pfe_broker.common.UtilsRunning; + +@Singleton +public class IntegrityCheckService { + + private static final Logger LOG = LoggerFactory.getLogger( + IntegrityCheckService.class + ); + + @Inject + private RedisClient redisClient; + + @Property(name = "redis.uri") + private String redisUri; + + private StatefulRedisConnection redisConnection; + + @PostConstruct + void init() { + if (UtilsRunning.isRedisRunning(redisUri)) { + this.redisConnection = redisClient.connect(); + } else { + LOG.error("Redis is not running"); + } + } + + public boolean replaceCancelOrder(Order oldOrder, Order newOrder) { + RedisCommands syncCommands = redisConnection.sync(); + + if (oldOrder.getSide() == Side.BUY) { + return replaceBuyLimitOrder(syncCommands, oldOrder, newOrder); + } else { + return replaceSellLimitOrder(syncCommands, oldOrder, newOrder); + } + } + + /** + * Replace a buy limit order + */ + private boolean replaceBuyLimitOrder( + RedisCommands syncCommands, + Order oldOrder, + Order newOrder + ) { + String username = oldOrder.getUsername().toString(); + + String balanceKey = username + ":balance"; + + // Side is buy we need to modify the balance + Double oldTotalPrice = oldOrder.getPrice() * oldOrder.getQuantity(); + Double newTotalPrice = newOrder.getPrice() * newOrder.getQuantity(); + Double modification = oldTotalPrice - newTotalPrice; + + syncCommands.watch(balanceKey); + int countdown = 10; + while (countdown-- > 0) { + Double balance = Double.parseDouble(syncCommands.get(balanceKey)); + if (balance + modification < 0) { + syncCommands.unwatch(); + return false; + } + syncCommands.multi(); + syncCommands.incrbyfloat(balanceKey, modification); + try { + syncCommands.exec(); + syncCommands.unwatch(); + return true; + } catch (Exception e) { + LOG.debug("Retrying..."); + } + } + LOG.error("Failed to modify the balance"); + return false; + } + + /** + * Replace a sell limit order + */ + private boolean replaceSellLimitOrder( + RedisCommands syncCommands, + Order oldOrder, + Order newOrder + ) { + String username = oldOrder.getUsername().toString(); + String symbol = oldOrder.getSymbol().toString(); + + String stockKey = username + ":" + symbol; + + // Side is sell we need to modify the stock + Integer oldQuantity = oldOrder.getQuantity(); + Integer newQuantity = newOrder.getQuantity(); + Integer modification = oldQuantity - newQuantity; + + syncCommands.watch(stockKey); + int countdown = 10; + while (countdown-- > 0) { + Integer stockQuantity = Integer.parseInt(syncCommands.get(stockKey)); + if (stockQuantity + modification < 0) { + syncCommands.unwatch(); + return false; + } + syncCommands.multi(); + syncCommands.incrby(stockKey, modification); + try { + syncCommands.exec(); + syncCommands.unwatch(); + return true; + } catch (Exception e) { + LOG.debug("Retrying..."); + } + } + LOG.error("Failed to modify the stock"); + return false; + } +} diff --git a/components/order-book/src/main/java/pfe_broker/order_book/LimitOrderBook.java b/components/order-book/src/main/java/pfe_broker/order_book/LimitOrderBook.java new file mode 100644 index 00000000..4f3e15c1 --- /dev/null +++ b/components/order-book/src/main/java/pfe_broker/order_book/LimitOrderBook.java @@ -0,0 +1,118 @@ +package pfe_broker.order_book; + +import java.util.HashMap; +import java.util.Map; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import pfe_broker.avro.MarketData; +import pfe_broker.avro.Order; +import pfe_broker.avro.Side; +import pfe_broker.avro.Trade; + +public class LimitOrderBook { + + private static final Logger LOG = LoggerFactory.getLogger( + LimitOrderBook.class + ); + + private final String symbol; + + private final OrderTree buyOrderTree; + + private final OrderTree sellOrderTree; + + public LimitOrderBook(String symbol) { + this.symbol = symbol; + this.buyOrderTree = new OrderTree(Side.BUY); + this.sellOrderTree = new OrderTree(Side.SELL); + } + + public void addOrder(String id, Order order) { + LOG.debug("Add order [{}]{} to order book {}", id, order, symbol); + if (order.getSide() == Side.BUY) { + buyOrderTree.addOrder(id, order); + } else { + sellOrderTree.addOrder(id, order); + } + } + + public Order removeOrder(String id) { + LOG.debug("Remove order [{}] from order book {}", id, symbol); + Order order = null; + if (buyOrderTree.contains(id)) { + order = buyOrderTree.removeOrder(id); + } else if (sellOrderTree.contains(id)) { + order = sellOrderTree.removeOrder(id); + } + return order; + } + + public Order replaceOrder(String id, Order order) { + LOG.debug("Replace order [{}]{} in order book {}", id, order, symbol); + Order oldOrder = null; + if (buyOrderTree.contains(id)) { + oldOrder = buyOrderTree.replaceOrder(id, order); + } else if (sellOrderTree.contains(id)) { + oldOrder = sellOrderTree.replaceOrder(id, order); + } + return oldOrder; + } + + public Order getOrder(String id) { + Order order = null; + if (buyOrderTree.contains(id)) { + order = buyOrderTree.getOrder(id); + } else if (sellOrderTree.contains(id)) { + order = sellOrderTree.getOrder(id); + } + return order; + } + + public Map matchOrdersToTrade(MarketData marketData) { + LOG.trace( + "Match orders to trade in order book {} with market data {}", + symbol, + marketData + ); + Double low = marketData.getLow(); + Double high = marketData.getHigh(); + + Map orders = new HashMap<>(); + orders.putAll(buyOrderTree.matchOrders(low)); + orders.putAll(sellOrderTree.matchOrders(high)); + + Map trades = new HashMap<>(); + orders.forEach((id, order) -> { + Trade trade = new Trade( + order, + symbol, + order.getPrice(), + order.getQuantity() + ); + trades.put(id, trade); + }); + return trades; + } + + public Map getBuyOrders() { + return buyOrderTree.getOrders(); + } + + public Map getSellOrders() { + return sellOrderTree.getOrders(); + } + + public String getSymbol() { + return symbol; + } + + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("LimitOrderBook: ").append(symbol).append("\n"); + sb.append("Buy Orders: ").append("\n"); + sb.append(buyOrderTree.toString()).append("\n"); + sb.append("Sell Orders: ").append("\n"); + sb.append(sellOrderTree.toString()).append("\n"); + return sb.toString(); + } +} diff --git a/components/order-book/src/main/java/pfe_broker/order_book/MarketDataListener.java b/components/order-book/src/main/java/pfe_broker/order_book/MarketDataListener.java new file mode 100644 index 00000000..be40bcd4 --- /dev/null +++ b/components/order-book/src/main/java/pfe_broker/order_book/MarketDataListener.java @@ -0,0 +1,59 @@ +package pfe_broker.order_book; + +import io.micronaut.configuration.kafka.annotation.KafkaListener; +import io.micronaut.configuration.kafka.annotation.Topic; +import io.micronaut.context.annotation.Property; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; +import java.util.List; +import java.util.Map; +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import pfe_broker.avro.MarketData; +import pfe_broker.avro.Trade; + +@Singleton +public class MarketDataListener { + + private static final Logger LOG = LoggerFactory.getLogger( + MarketDataListener.class + ); + + @Property(name = "kafka.common.symbol-topic-prefix") + private String symbolTopicPrefix; + + @Inject + private OrderBookCatalog orderBooks; + + @Inject + private TradeProducer tradeProducer; + + @KafkaListener( + groupId = "order-book-market-data", + batch = true, + threadsValue = "${kafka.common.market-data-thread-pool-size}" + ) + @Topic(patterns = "${kafka.common.symbol-topic-prefix}[A-Z]+") + public void receiveMarketData( + List> records + ) { + records.forEach(record -> { + MarketData marketData = record.value(); + String symbol = record.topic().substring(symbolTopicPrefix.length()); + LimitOrderBook orderBook = orderBooks.getOrderBook(symbol); + if (orderBook == null) { + return; + } + + Map trades = orderBook.matchOrdersToTrade(marketData); + if (trades.isEmpty()) { + return; + } + LOG.debug("Sending {} trades to Kafka", trades.size()); + trades.forEach((key, trade) -> { + tradeProducer.sendTrade(key, trade); + }); + }); + } +} diff --git a/components/order-book/src/main/java/pfe_broker/order_book/OrderBookCatalog.java b/components/order-book/src/main/java/pfe_broker/order_book/OrderBookCatalog.java new file mode 100644 index 00000000..1a9d97f3 --- /dev/null +++ b/components/order-book/src/main/java/pfe_broker/order_book/OrderBookCatalog.java @@ -0,0 +1,31 @@ +package pfe_broker.order_book; + +import jakarta.inject.Singleton; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +@Singleton +public class OrderBookCatalog { + + private Map orderBooks; + + public OrderBookCatalog() { + this.orderBooks = Collections.synchronizedMap(new HashMap<>()); + } + + public void addOrderBook(String symbol) { + if (orderBooks.containsKey(symbol)) { + return; + } + orderBooks.put(symbol, new LimitOrderBook(symbol)); + } + + public LimitOrderBook getOrderBook(String symbol) { + return orderBooks.get(symbol); + } + + public void clear() { + orderBooks.clear(); + } +} diff --git a/components/order-book/src/main/java/pfe_broker/order_book/OrderList.java b/components/order-book/src/main/java/pfe_broker/order_book/OrderList.java new file mode 100644 index 00000000..7d10288b --- /dev/null +++ b/components/order-book/src/main/java/pfe_broker/order_book/OrderList.java @@ -0,0 +1,56 @@ +package pfe_broker.order_book; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import pfe_broker.avro.Order; + +public class OrderList { + + private Double price; + private long volume; + private Map orders; + + public OrderList(Double price) { + this.price = price; + orders = Collections.synchronizedMap(new HashMap<>()); + } + + public void addOrder(String id, Order order) { + orders.put(id, order); + volume += order.getQuantity(); + } + + public Order removeOrder(String id) { + Order order = orders.get(id); + volume -= order.getQuantity(); + return orders.remove(id); + } + + public void replaceOrder(String id, Order order) { + Order oldOrder = orders.get(id); + volume -= oldOrder.getQuantity(); + volume += order.getQuantity(); + orders.put(id, order); + } + + public Order getOrder(String id) { + return orders.get(id); + } + + public double getPrice() { + return price; + } + + public long getVolume() { + return volume; + } + + public Map getOrders() { + return Map.copyOf(orders); + } + + public String toString() { + return "Price: " + price + " |Volume: " + volume + " |Orders: " + orders; + } +} diff --git a/components/order-book/src/main/java/pfe_broker/order_book/OrderListener.java b/components/order-book/src/main/java/pfe_broker/order_book/OrderListener.java new file mode 100644 index 00000000..46decc1a --- /dev/null +++ b/components/order-book/src/main/java/pfe_broker/order_book/OrderListener.java @@ -0,0 +1,135 @@ +package pfe_broker.order_book; + +import io.micronaut.configuration.kafka.annotation.KafkaListener; +import io.micronaut.configuration.kafka.annotation.OffsetReset; +import io.micronaut.configuration.kafka.annotation.Topic; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; +import java.util.List; +import org.apache.kafka.clients.consumer.ConsumerRecord; +import pfe_broker.avro.Order; +import pfe_broker.avro.OrderBookRequest; +import pfe_broker.avro.OrderBookRequestType; + +@Singleton +public class OrderListener { + + private static final org.slf4j.Logger LOG = org.slf4j.LoggerFactory.getLogger( + OrderListener.class + ); + + @Inject + private OrderBookCatalog orderBookCatalog; + + @Inject + private IntegrityCheckService integrityCheckService; + + @Inject + private TradeProducer tradeProducer; + + @KafkaListener( + groupId = "order-book-orders", + batch = true, + offsetReset = OffsetReset.EARLIEST, + pollTimeout = "0ms" + ) + @Topic(patterns = "${kafka.topics.order-book-request}") + public void receiveOrder( + List> records + ) { + records.forEach(record -> { + handleOrderBookRequest(record.key(), record.value()); + }); + } + + private void handleOrderBookRequest( + String key, + OrderBookRequest orderBookRequest + ) { + Order order = orderBookRequest.getOrder(); + String symbol = order.getSymbol().toString(); + LOG.debug( + "Received order book request: {} for symbol: {}", + orderBookRequest, + symbol + ); + + LimitOrderBook orderBook = orderBookCatalog.getOrderBook(symbol); + if (orderBook == null) { + orderBookCatalog.addOrderBook(symbol); + orderBook = orderBookCatalog.getOrderBook(symbol); + } + + if (orderBookRequest.getType() == OrderBookRequestType.NEW) { + orderBook.addOrder(key, order); + tradeProducer.sendOrderBookResponse(key, orderBookRequest); + return; + } + + Order oldOrder = orderBook.getOrder(key); + + if (oldOrder == null) { + LOG.error( + "Order {} could not be replaced/cancelled by {} because it does not exist", + oldOrder, + order + ); + tradeProducer.sendOrderBookRejected(key, orderBookRequest); + return; + } + if ( + !oldOrder.getClOrderID().equals((orderBookRequest.getOrigClOrderID())) + ) { + LOG.error("Matching of the clOrderID is wrong"); + tradeProducer.sendOrderBookRejected(key, orderBookRequest); + return; + } + + if (orderBookRequest.getType() == OrderBookRequestType.CANCEL) { + Boolean integrityCheck = integrityCheckService.replaceCancelOrder( + oldOrder, + order + ); + + if (!integrityCheck) { + LOG.error("Order {} could not be cancelled by {}", oldOrder, order); + tradeProducer.sendOrderBookRejected(key, orderBookRequest); + return; + } + + LOG.debug("Order {} cancelled by {}", oldOrder, order); + orderBook.removeOrder(key); + tradeProducer.sendOrderBookResponse(key, orderBookRequest); + return; + } + + if (orderBookRequest.getType() == OrderBookRequestType.REPLACE) { + if (oldOrder.getSide() != order.getSide()) { + LOG.error("Modifification of the side is not allowed"); + tradeProducer.sendOrderBookRejected(key, orderBookRequest); + return; + } + if (oldOrder.getType() != order.getType()) { + LOG.error("Modifification of the type is not allowed"); + tradeProducer.sendOrderBookRejected(key, orderBookRequest); + return; + } + + Boolean integrityCheck = integrityCheckService.replaceCancelOrder( + oldOrder, + order + ); + + if (!integrityCheck) { + LOG.error("Order {} could not be replaced by {}", oldOrder, order); + tradeProducer.sendOrderBookRejected(key, orderBookRequest); + return; + } + + LOG.debug("Order {} replaced by {}", oldOrder, order); + orderBook.replaceOrder(key, order); + tradeProducer.sendOrderBookResponse(key, orderBookRequest); + return; + } + } +} diff --git a/components/order-book/src/main/java/pfe_broker/order_book/OrderTree.java b/components/order-book/src/main/java/pfe_broker/order_book/OrderTree.java new file mode 100644 index 00000000..71f43d7e --- /dev/null +++ b/components/order-book/src/main/java/pfe_broker/order_book/OrderTree.java @@ -0,0 +1,166 @@ +package pfe_broker.order_book; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.SortedMap; +import java.util.TreeMap; +import java.util.stream.Collectors; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import pfe_broker.avro.Order; +import pfe_broker.avro.Side; + +public class OrderTree { + + private static final Logger LOG = LoggerFactory.getLogger(OrderTree.class); + + // All order of the side, map (id, price) + private final Map orders; + + // Map of price and orderList + private final SortedMap priceMap; + + // Side of the tree + private final Side side; + + public OrderTree(final Side side) { + this.orders = Collections.synchronizedMap(new HashMap<>()); + this.priceMap = Collections.synchronizedSortedMap(new TreeMap<>()); + this.side = side; + } + + public Order addOrder(String id, Order order) { + LOG.trace("Add order [{}]{} to order tree {}", id, order, this.side); + Double price = order.getPrice(); + if (priceMap.containsKey(price)) { + OrderList orderList = priceMap.get(price); + orderList.addOrder(id, order); + } else { + OrderList orderList = new OrderList(price); + orderList.addOrder(id, order); + priceMap.put(price, orderList); + } + orders.put(id, price); + return order; + } + + public Order removeOrder(String id) { + LOG.trace("Remove order [{}] from order tree {}", id, this.side); + Double price = orders.get(id); + if (price == null) { + return null; + } + OrderList orderList = priceMap.get(price); + Order order = orderList.removeOrder(id); + if (orderList.getVolume() == 0) { + priceMap.remove(price); + } + orders.remove(id); + return order; + } + + public Order replaceOrder(String id, Order order) { + LOG.trace("Replace order [{}]{} in order tree {}", id, order, this.side); + Double oldPrice = orders.get(id); + if (oldPrice == null) { + return null; + } + + Double newPrice = order.getPrice(); + + if (oldPrice.equals(newPrice)) { + OrderList orderList = priceMap.get(oldPrice); + orderList.replaceOrder(id, order); + } else { + OrderList oldOrderList = priceMap.get(oldPrice); + OrderList newOrderList = priceMap.get(newPrice); + + oldOrderList.removeOrder(id); + newOrderList.addOrder(id, order); + + if (oldOrderList.getVolume() == 0) { + priceMap.remove(oldPrice); + } + } + orders.put(id, newPrice); + return order; + } + + /** + * Match orders with market data price + * @param price + * @return Map of order id and order + */ + public Map matchOrders(Double price) { + LOG.trace("Match orders in order tree {} with price {}", this.side, price); + Map ordersMap = null; + if (side == Side.BUY) { + ordersMap = matchBuyOrders(price); + } else { + ordersMap = matchSellOrders(price); + } + ordersMap.forEach((id, order) -> removeOrder(id)); + return ordersMap; + } + + /** + * Match all order which are above a certain price + * @param price + * @return + */ + private Map matchBuyOrders(Double price) { + return priceMap + .tailMap(price) + .values() + .stream() + .map(OrderList::getOrders) + .flatMap(map -> map.entrySet().stream()) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + } + + /** + * Match all order which are bellow a certain price + * @param price + * @return + */ + private Map matchSellOrders(Double price) { + return priceMap + .headMap(price) + .values() + .stream() + .map(OrderList::getOrders) + .flatMap(map -> map.entrySet().stream()) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + } + + public Map getOrders() { + Map ordersMap = new HashMap<>(); + priceMap.forEach((price, orderList) -> { + Map orderMap = orderList.getOrders(); + ordersMap.putAll(orderMap); + }); + return ordersMap; + } + + public Order getOrder(String id) { + Double price = orders.get(id); + if (price == null) { + return null; + } + OrderList orderList = priceMap.get(price); + return orderList.getOrder(id); + } + + public boolean contains(String id) { + return orders.containsKey(id); + } + + public Side getSide() { + return side; + } + + public String toString() { + return priceMap.toString(); + } +} diff --git a/components/order-book/src/main/java/pfe_broker/order_book/TradeProducer.java b/components/order-book/src/main/java/pfe_broker/order_book/TradeProducer.java new file mode 100644 index 00000000..aab984cf --- /dev/null +++ b/components/order-book/src/main/java/pfe_broker/order_book/TradeProducer.java @@ -0,0 +1,25 @@ +package pfe_broker.order_book; + +import io.micronaut.configuration.kafka.annotation.KafkaClient; +import io.micronaut.configuration.kafka.annotation.KafkaKey; +import io.micronaut.configuration.kafka.annotation.Topic; +import pfe_broker.avro.OrderBookRequest; +import pfe_broker.avro.Trade; + +@KafkaClient +public interface TradeProducer { + @Topic("${kafka.topics.trades}") + void sendTrade(@KafkaKey String key, Trade trade); + + @Topic("${kafka.topics.order-book-response}") + void sendOrderBookResponse( + @KafkaKey String key, + OrderBookRequest orderBookRequest + ); + + @Topic("${kafka.topics.order-book-rejected}") + void sendOrderBookRejected( + @KafkaKey String key, + OrderBookRequest orderBookRequest + ); +} diff --git a/components/order-book/src/main/resources/application.yml b/components/order-book/src/main/resources/application.yml new file mode 100644 index 00000000..11a8036e --- /dev/null +++ b/components/order-book/src/main/resources/application.yml @@ -0,0 +1,3 @@ +micronaut: + application: + name: OrderBook diff --git a/components/quickfix-server/src/test/java/pfe_broker/quickfix_server/ComponentTest.java b/components/order-book/src/test/java/pfe_broker/order_book/ComponentTest.java similarity index 91% rename from components/quickfix-server/src/test/java/pfe_broker/quickfix_server/ComponentTest.java rename to components/order-book/src/test/java/pfe_broker/order_book/ComponentTest.java index b62de375..9cf7a99b 100644 --- a/components/quickfix-server/src/test/java/pfe_broker/quickfix_server/ComponentTest.java +++ b/components/order-book/src/test/java/pfe_broker/order_book/ComponentTest.java @@ -1,4 +1,4 @@ -package pfe_broker.quickfix_server; +package pfe_broker.order_book; import io.micronaut.runtime.EmbeddedApplication; import io.micronaut.test.extensions.junit5.annotation.MicronautTest; diff --git a/components/order-book/src/test/java/pfe_broker/order_book/LimitOrderBookMatchingTest.java b/components/order-book/src/test/java/pfe_broker/order_book/LimitOrderBookMatchingTest.java new file mode 100644 index 00000000..9b06a1ff --- /dev/null +++ b/components/order-book/src/test/java/pfe_broker/order_book/LimitOrderBookMatchingTest.java @@ -0,0 +1,287 @@ +package pfe_broker.order_book; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +import io.micronaut.core.annotation.NonNull; +import io.micronaut.test.extensions.junit5.annotation.MicronautTest; +import io.micronaut.test.support.TestPropertyProvider; +import jakarta.inject.Inject; +import java.time.Duration; +import java.util.Map; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import pfe_broker.avro.MarketData; +import pfe_broker.avro.Order; +import pfe_broker.avro.OrderBookRequest; +import pfe_broker.avro.OrderBookRequestType; +import pfe_broker.avro.Side; +import pfe_broker.avro.Type; +import pfe_broker.common.utils.KafkaTestContainer; +import pfe_broker.order_book.mocks.MockMarketDataProducer; +import pfe_broker.order_book.mocks.MockOrderProducer; +import pfe_broker.order_book.mocks.MockTradeListener; + +@MicronautTest(transactional = false) +@Testcontainers(disabledWithoutDocker = true) +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class LimitOrderBookMatchingTest implements TestPropertyProvider { + + @Container + static final KafkaTestContainer kafka = new KafkaTestContainer(); + + @Inject + OrderBookCatalog orderBooks; + + private final Double lowValue = 90.0; + private final Double highValue = 120.0; + private final String symbol = "AAPL"; + + @Inject + MockTradeListener mockTradeListener; + + @Inject + MockMarketDataProducer mockMarketDataProducer; + + @Inject + MockOrderProducer mockOrderProducer; + + @Override + public @NonNull Map getProperties() { + if (!kafka.isRunning()) { + kafka.start(); + } + kafka.registerTopics("market-data.AAPL", "order-book-request", "trades"); + return Map.of( + "kafka.bootstrap.servers", + kafka.getBootstrapServers(), + "kafka.schema.registry.url", + kafka.getSchemaRegistryUrl() + ); + } + + @BeforeEach + void reset(MockTradeListener mockTradeListener) { + mockTradeListener.trades.clear(); + orderBooks.clear(); + } + + @Test + void testMarketDataWithoutMatchingOrdersBuy() { + Double limitPrice = lowValue - 1; + + Order order = new Order( + "user", + symbol, + 10, + Side.BUY, + Type.LIMIT, + limitPrice, + "1" + ); + + OrderBookRequest orderBookRequest = new OrderBookRequest( + OrderBookRequestType.NEW, + order, + null + ); + + mockOrderProducer.sendOrderBookRequest("user:1", orderBookRequest); + + await() + .atMost(Duration.ofSeconds(5)) + .pollInterval(Duration.ofSeconds(1)) + .untilAsserted(() -> { + LimitOrderBook orderBook = orderBooks.getOrderBook(symbol); + assertThat(orderBook).isNotNull(); + assertThat(orderBook.getBuyOrders().size()).isEqualTo(1); + assertThat(orderBook.getSellOrders().size()).isEqualTo(0); + }); + + MarketData marketData = new MarketData( + 100.0, + highValue, + lowValue, + 100.0, + 10 + ); + mockMarketDataProducer.sendMarketData(marketData); + + await() + .atMost(Duration.ofSeconds(5)) + .pollInterval(Duration.ofSeconds(1)) + .untilAsserted(() -> { + LimitOrderBook orderBook = orderBooks.getOrderBook(symbol); + assertThat(orderBook).isNotNull(); + assertThat(orderBook.getBuyOrders().size()).isEqualTo(1); + assertThat(orderBook.getSellOrders().size()).isEqualTo(0); + }); + } + + @Test + void testMarketDataWithMatchingOrdersBuy() { + Double limitPrice = lowValue + 1; + + Order order = new Order( + "user", + symbol, + 10, + Side.BUY, + Type.LIMIT, + limitPrice, + "1" + ); + + OrderBookRequest orderBookRequest = new OrderBookRequest( + OrderBookRequestType.NEW, + order, + null + ); + + mockOrderProducer.sendOrderBookRequest("user:1", orderBookRequest); + + await() + .atMost(Duration.ofSeconds(5)) + .pollInterval(Duration.ofSeconds(1)) + .untilAsserted(() -> { + LimitOrderBook orderBook = orderBooks.getOrderBook(symbol); + assertThat(orderBook).isNotNull(); + assertThat(orderBook.getBuyOrders().size()).isEqualTo(1); + assertThat(orderBook.getSellOrders().size()).isEqualTo(0); + }); + + MarketData marketData = new MarketData( + 100.0, + highValue, + lowValue, + 100.0, + 10 + ); + mockMarketDataProducer.sendMarketData(marketData); + + await() + .atMost(Duration.ofSeconds(5)) + .pollInterval(Duration.ofSeconds(1)) + .untilAsserted(() -> { + LimitOrderBook orderBook = orderBooks.getOrderBook(symbol); + assertThat(orderBook).isNotNull(); + assertThat(orderBook.getBuyOrders().size()).isEqualTo(0); + assertThat(orderBook.getSellOrders().size()).isEqualTo(0); + assertThat(mockTradeListener.trades).hasSize(1); + assertThat(mockTradeListener.trades.get(0).getOrder()).isEqualTo(order); + assertThat(mockTradeListener.trades.get(0).getPrice()) + .isEqualTo(limitPrice); + }); + } + + @Test + void testMarketDataWithoutMatchingOrdersSell() { + Double limitPrice = highValue + 1; + + Order order = new Order( + "user", + symbol, + 10, + Side.SELL, + Type.LIMIT, + limitPrice, + "1" + ); + + OrderBookRequest orderBookRequest = new OrderBookRequest( + OrderBookRequestType.NEW, + order, + null + ); + + mockOrderProducer.sendOrderBookRequest("user:1", orderBookRequest); + + await() + .atMost(Duration.ofSeconds(5)) + .pollInterval(Duration.ofSeconds(1)) + .untilAsserted(() -> { + LimitOrderBook orderBook = orderBooks.getOrderBook(symbol); + assertThat(orderBook).isNotNull(); + assertThat(orderBook.getBuyOrders().size()).isEqualTo(0); + assertThat(orderBook.getSellOrders().size()).isEqualTo(1); + }); + + MarketData marketData = new MarketData( + 100.0, + highValue, + lowValue, + 100.0, + 10 + ); + mockMarketDataProducer.sendMarketData(marketData); + + await() + .atMost(Duration.ofSeconds(5)) + .pollInterval(Duration.ofSeconds(1)) + .untilAsserted(() -> { + LimitOrderBook orderBook = orderBooks.getOrderBook(symbol); + assertThat(orderBook).isNotNull(); + assertThat(orderBook.getBuyOrders().size()).isEqualTo(0); + assertThat(orderBook.getSellOrders().size()).isEqualTo(1); + }); + } + + @Test + void testMarketDataWithMatchingOrdersSell() { + Double limitPrice = highValue - 1; + + Order order = new Order( + "user", + symbol, + 10, + Side.SELL, + Type.LIMIT, + limitPrice, + "1" + ); + + OrderBookRequest orderBookRequest = new OrderBookRequest( + OrderBookRequestType.NEW, + order, + null + ); + + mockOrderProducer.sendOrderBookRequest("user:1", orderBookRequest); + + await() + .atMost(Duration.ofSeconds(5)) + .pollInterval(Duration.ofSeconds(1)) + .untilAsserted(() -> { + LimitOrderBook orderBook = orderBooks.getOrderBook(symbol); + assertThat(orderBook).isNotNull(); + assertThat(orderBook.getBuyOrders().size()).isEqualTo(0); + assertThat(orderBook.getSellOrders().size()).isEqualTo(1); + }); + + MarketData marketData = new MarketData( + 100.0, + highValue, + lowValue, + 100.0, + 10 + ); + mockMarketDataProducer.sendMarketData(marketData); + + await() + .atMost(Duration.ofSeconds(5)) + .pollInterval(Duration.ofSeconds(1)) + .untilAsserted(() -> { + LimitOrderBook orderBook = orderBooks.getOrderBook(symbol); + assertThat(orderBook).isNotNull(); + assertThat(orderBook.getBuyOrders().size()).isEqualTo(0); + assertThat(orderBook.getSellOrders().size()).isEqualTo(0); + assertThat(mockTradeListener.trades).hasSize(1); + assertThat(mockTradeListener.trades.get(0).getOrder()).isEqualTo(order); + assertThat(mockTradeListener.trades.get(0).getPrice()) + .isEqualTo(limitPrice); + }); + } +} diff --git a/components/order-book/src/test/java/pfe_broker/order_book/LimitOrderBookOperationsTest.java b/components/order-book/src/test/java/pfe_broker/order_book/LimitOrderBookOperationsTest.java new file mode 100644 index 00000000..81a85cff --- /dev/null +++ b/components/order-book/src/test/java/pfe_broker/order_book/LimitOrderBookOperationsTest.java @@ -0,0 +1,409 @@ +package pfe_broker.order_book; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +import io.lettuce.core.api.StatefulRedisConnection; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.test.extensions.junit5.annotation.MicronautTest; +import io.micronaut.test.support.TestPropertyProvider; +import jakarta.inject.Inject; +import java.time.Duration; +import java.util.Map; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import pfe_broker.avro.Order; +import pfe_broker.avro.OrderBookRequest; +import pfe_broker.avro.OrderBookRequestType; +import pfe_broker.avro.Side; +import pfe_broker.avro.Type; +import pfe_broker.common.utils.KafkaTestContainer; +import pfe_broker.common.utils.RedisTestContainer; +import pfe_broker.order_book.mocks.MockMarketDataProducer; +import pfe_broker.order_book.mocks.MockOrderProducer; +import pfe_broker.order_book.mocks.MockTradeListener; + +@MicronautTest(transactional = false) +@Testcontainers(disabledWithoutDocker = true) +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class LimitOrderBookOperationsTest implements TestPropertyProvider { + + @Container + static final KafkaTestContainer kafka = new KafkaTestContainer(); + + @Container + static final RedisTestContainer redis = new RedisTestContainer(); + + @Inject + OrderBookCatalog orderBooks; + + private final String symbol = "AAPL"; + + @Inject + MockTradeListener mockTradeListener; + + @Inject + MockMarketDataProducer mockMarketDataProducer; + + @Inject + MockOrderProducer mockOrderProducer; + + @Override + public @NonNull Map getProperties() { + if (!kafka.isRunning()) { + kafka.start(); + } + kafka.registerTopics("order-book-request"); + if (!redis.isRunning()) { + redis.start(); + } + return Map.of( + "kafka.bootstrap.servers", + kafka.getBootstrapServers(), + "kafka.schema.registry.url", + kafka.getSchemaRegistryUrl(), + "redis.uri", + redis.getRedisUrl() + ); + } + + @BeforeEach + void reset(MockTradeListener mockTradeListener) { + mockTradeListener.trades.clear(); + orderBooks.clear(); + } + + @Test + void testAddBuyOrder() { + assertThat(orderBooks.getOrderBook(symbol)).isNull(); + + Order order = new Order( + "user", + symbol, + 10, + Side.BUY, + Type.LIMIT, + 80.0, + "1" + ); + + OrderBookRequest orderBookRequest = new OrderBookRequest( + OrderBookRequestType.NEW, + order, + null + ); + + mockOrderProducer.sendOrderBookRequest("user:1", orderBookRequest); + + await() + .atMost(Duration.ofSeconds(5)) + .pollInterval(Duration.ofSeconds(1)) + .untilAsserted(() -> { + LimitOrderBook orderBook = orderBooks.getOrderBook(symbol); + assertThat(orderBook).isNotNull(); + assertThat(orderBook.getBuyOrders().size()).isEqualTo(1); + assertThat(orderBook.getSellOrders().size()).isEqualTo(0); + }); + } + + @Test + void testReplaceBuyOrder( + StatefulRedisConnection redisConnection + ) { + assertThat(orderBooks.getOrderBook(symbol)).isNull(); + + redisConnection.sync().set("user:balance", "800"); + + Order order = new Order( + "user", + symbol, + 10, + Side.BUY, + Type.LIMIT, + 80.0, + "1" + ); + + OrderBookRequest orderBookRequest = new OrderBookRequest( + OrderBookRequestType.NEW, + order, + null + ); + + mockOrderProducer.sendOrderBookRequest("user:1", orderBookRequest); + + await() + .atMost(Duration.ofSeconds(5)) + .pollInterval(Duration.ofSeconds(1)) + .untilAsserted(() -> { + LimitOrderBook orderBook = orderBooks.getOrderBook(symbol); + assertThat(orderBook).isNotNull(); + assertThat(orderBook.getBuyOrders().size()).isEqualTo(1); + assertThat(orderBook.getSellOrders().size()).isEqualTo(0); + }); + + Order newOrder = new Order( + "user", + symbol, + 20, + Side.BUY, + Type.LIMIT, + 80.0, + "2" + ); + + OrderBookRequest newOrderBookRequest = new OrderBookRequest( + OrderBookRequestType.REPLACE, + newOrder, + "1" + ); + + mockOrderProducer.sendOrderBookRequest("user:1", newOrderBookRequest); + + await() + .atMost(Duration.ofSeconds(5)) + .pollInterval(Duration.ofSeconds(1)) + .untilAsserted(() -> { + LimitOrderBook orderBook = orderBooks.getOrderBook(symbol); + assertThat(orderBook).isNotNull(); + assertThat(orderBook.getBuyOrders().size()).isEqualTo(1); + assertThat(orderBook.getSellOrders().size()).isEqualTo(0); + assertThat(redisConnection.sync().get("user:balance")).isEqualTo("0"); + }); + } + + @Test + void testCancelBuyOrder( + StatefulRedisConnection redisConnection + ) { + assertThat(orderBooks.getOrderBook(symbol)).isNull(); + + redisConnection.sync().set("user:balance", "800"); + + Order order = new Order( + "user", + symbol, + 10, + Side.BUY, + Type.LIMIT, + 80.0, + "1" + ); + + OrderBookRequest orderBookRequest = new OrderBookRequest( + OrderBookRequestType.NEW, + order, + null + ); + + mockOrderProducer.sendOrderBookRequest("user:1", orderBookRequest); + + await() + .atMost(Duration.ofSeconds(5)) + .pollInterval(Duration.ofSeconds(1)) + .untilAsserted(() -> { + LimitOrderBook orderBook = orderBooks.getOrderBook(symbol); + assertThat(orderBook).isNotNull(); + assertThat(orderBook.getBuyOrders().size()).isEqualTo(1); + assertThat(orderBook.getSellOrders().size()).isEqualTo(0); + }); + + Order cancelOrder = new Order( + "user", + symbol, + 0, + Side.BUY, + Type.LIMIT, + 0.0, + "2" + ); + + OrderBookRequest cancelOrderBookRequest = new OrderBookRequest( + OrderBookRequestType.CANCEL, + cancelOrder, + "1" + ); + + mockOrderProducer.sendOrderBookRequest("user:1", cancelOrderBookRequest); + + await() + .atMost(Duration.ofSeconds(5)) + .pollInterval(Duration.ofSeconds(1)) + .untilAsserted(() -> { + LimitOrderBook orderBook = orderBooks.getOrderBook(symbol); + assertThat(orderBook).isNotNull(); + assertThat(orderBook.getBuyOrders().size()).isEqualTo(0); + assertThat(orderBook.getSellOrders().size()).isEqualTo(0); + assertThat(redisConnection.sync().get("user:balance")) + .isEqualTo("1600"); + }); + } + + @Test + void testAddSellOrder() { + assertThat(orderBooks.getOrderBook(symbol)).isNull(); + + Order order = new Order( + "user", + symbol, + 10, + Side.SELL, + Type.LIMIT, + 80.0, + "1" + ); + + OrderBookRequest orderBookRequest = new OrderBookRequest( + OrderBookRequestType.NEW, + order, + null + ); + + mockOrderProducer.sendOrderBookRequest("user:1", orderBookRequest); + + await() + .atMost(Duration.ofSeconds(5)) + .pollInterval(Duration.ofSeconds(1)) + .untilAsserted(() -> { + LimitOrderBook orderBook = orderBooks.getOrderBook(symbol); + assertThat(orderBook).isNotNull(); + assertThat(orderBook.getBuyOrders().size()).isEqualTo(0); + assertThat(orderBook.getSellOrders().size()).isEqualTo(1); + }); + } + + @Test + void testReplaceSellOrder( + StatefulRedisConnection redisConnection + ) { + assertThat(orderBooks.getOrderBook(symbol)).isNull(); + + redisConnection.sync().set("user:AAPL", "20"); + + Order order = new Order( + "user", + symbol, + 10, + Side.SELL, + Type.LIMIT, + 80.0, + "1" + ); + + OrderBookRequest orderBookRequest = new OrderBookRequest( + OrderBookRequestType.NEW, + order, + null + ); + + mockOrderProducer.sendOrderBookRequest("user:1", orderBookRequest); + + await() + .atMost(Duration.ofSeconds(5)) + .pollInterval(Duration.ofSeconds(1)) + .untilAsserted(() -> { + LimitOrderBook orderBook = orderBooks.getOrderBook(symbol); + assertThat(orderBook).isNotNull(); + assertThat(orderBook.getBuyOrders().size()).isEqualTo(0); + assertThat(orderBook.getSellOrders().size()).isEqualTo(1); + }); + + Order newOrder = new Order( + "user", + symbol, + 20, + Side.SELL, + Type.LIMIT, + 80.0, + "2" + ); + + OrderBookRequest newOrderBookRequest = new OrderBookRequest( + OrderBookRequestType.REPLACE, + newOrder, + "1" + ); + + mockOrderProducer.sendOrderBookRequest("user:1", newOrderBookRequest); + + await() + .atMost(Duration.ofSeconds(5)) + .pollInterval(Duration.ofSeconds(1)) + .untilAsserted(() -> { + LimitOrderBook orderBook = orderBooks.getOrderBook(symbol); + assertThat(orderBook).isNotNull(); + assertThat(orderBook.getBuyOrders().size()).isEqualTo(0); + assertThat(orderBook.getSellOrders().size()).isEqualTo(1); + assertThat(redisConnection.sync().get("user:AAPL")).isEqualTo("10"); + }); + } + + @Test + void testCancelSellOrder( + StatefulRedisConnection redisConnection + ) { + assertThat(orderBooks.getOrderBook(symbol)).isNull(); + + redisConnection.sync().set("user:AAPL", "20"); + + Order order = new Order( + "user", + symbol, + 10, + Side.SELL, + Type.LIMIT, + 80.0, + "1" + ); + + OrderBookRequest orderBookRequest = new OrderBookRequest( + OrderBookRequestType.NEW, + order, + null + ); + + mockOrderProducer.sendOrderBookRequest("user:1", orderBookRequest); + + await() + .atMost(Duration.ofSeconds(5)) + .pollInterval(Duration.ofSeconds(1)) + .untilAsserted(() -> { + LimitOrderBook orderBook = orderBooks.getOrderBook(symbol); + assertThat(orderBook).isNotNull(); + assertThat(orderBook.getBuyOrders().size()).isEqualTo(0); + assertThat(orderBook.getSellOrders().size()).isEqualTo(1); + }); + + Order cancelOrder = new Order( + "user", + symbol, + 0, + Side.SELL, + Type.LIMIT, + 0.0, + "2" + ); + + OrderBookRequest cancelOrderBookRequest = new OrderBookRequest( + OrderBookRequestType.CANCEL, + cancelOrder, + "1" + ); + + mockOrderProducer.sendOrderBookRequest("user:1", cancelOrderBookRequest); + + await() + .atMost(Duration.ofSeconds(5)) + .pollInterval(Duration.ofSeconds(1)) + .untilAsserted(() -> { + LimitOrderBook orderBook = orderBooks.getOrderBook(symbol); + assertThat(orderBook).isNotNull(); + assertThat(orderBook.getBuyOrders().size()).isEqualTo(0); + assertThat(orderBook.getSellOrders().size()).isEqualTo(0); + assertThat(redisConnection.sync().get("user:AAPL")).isEqualTo("30"); + }); + } +} diff --git a/components/order-book/src/test/java/pfe_broker/order_book/mocks/MockMarketDataProducer.java b/components/order-book/src/test/java/pfe_broker/order_book/mocks/MockMarketDataProducer.java new file mode 100644 index 00000000..e8e21e23 --- /dev/null +++ b/components/order-book/src/test/java/pfe_broker/order_book/mocks/MockMarketDataProducer.java @@ -0,0 +1,11 @@ +package pfe_broker.order_book.mocks; + +import io.micronaut.configuration.kafka.annotation.KafkaClient; +import io.micronaut.configuration.kafka.annotation.Topic; +import pfe_broker.avro.MarketData; + +@KafkaClient +public interface MockMarketDataProducer { + @Topic("market-data.AAPL") + void sendMarketData(MarketData marketData); +} diff --git a/components/order-book/src/test/java/pfe_broker/order_book/mocks/MockOrderProducer.java b/components/order-book/src/test/java/pfe_broker/order_book/mocks/MockOrderProducer.java new file mode 100644 index 00000000..1c757974 --- /dev/null +++ b/components/order-book/src/test/java/pfe_broker/order_book/mocks/MockOrderProducer.java @@ -0,0 +1,12 @@ +package pfe_broker.order_book.mocks; + +import io.micronaut.configuration.kafka.annotation.KafkaClient; +import io.micronaut.configuration.kafka.annotation.KafkaKey; +import io.micronaut.configuration.kafka.annotation.Topic; +import pfe_broker.avro.OrderBookRequest; + +@KafkaClient +public interface MockOrderProducer { + @Topic("${kafka.topics.order-book-request}") + void sendOrderBookRequest(@KafkaKey String key, OrderBookRequest order); +} diff --git a/components/order-book/src/test/java/pfe_broker/order_book/mocks/MockTradeListener.java b/components/order-book/src/test/java/pfe_broker/order_book/mocks/MockTradeListener.java new file mode 100644 index 00000000..476cfdfd --- /dev/null +++ b/components/order-book/src/test/java/pfe_broker/order_book/mocks/MockTradeListener.java @@ -0,0 +1,21 @@ +package pfe_broker.order_book.mocks; + +import io.micronaut.configuration.kafka.annotation.KafkaKey; +import io.micronaut.configuration.kafka.annotation.KafkaListener; +import io.micronaut.configuration.kafka.annotation.Topic; +import jakarta.inject.Singleton; +import java.util.ArrayList; +import java.util.List; +import pfe_broker.avro.Trade; + +@Singleton +public class MockTradeListener { + + public List trades = new ArrayList<>(); + + @KafkaListener("mock-trades-consumer") + @Topic("${kafka.topics.trades}") + void receiveTrade(@KafkaKey String key, Trade trade) { + trades.add(trade); + } +} diff --git a/components/order-stream/build.gradle b/components/order-stream/build.gradle index 5ba007e2..87f608da 100644 --- a/components/order-stream/build.gradle +++ b/components/order-stream/build.gradle @@ -1,6 +1,11 @@ plugins { id "com.github.johnrengelman.shadow" id "io.micronaut.application" + id "jacoco" +} + +ext { + configFiles = "classpath:application.yml,classpath:kafka.yml,classpath:redis.yml" } version = "${version}" @@ -18,17 +23,17 @@ dependencies { runtimeOnly group: 'org.yaml', name: 'snakeyaml', version: '2.2' implementation group: 'io.micronaut.kafka', name: 'micronaut-kafka-streams', version: '5.2.0' - implementation group: 'io.micronaut.redis', name: 'micronaut-redis-lettuce', version: '6.1.0' + implementation group: 'io.micronaut.redis', name: 'micronaut-redis-lettuce', version: '6.2.0' // Test dependencies testImplementation group: 'org.awaitility', name: 'awaitility', version: '4.2.0' - testImplementation group: 'org.assertj', name: 'assertj-core', version: '3.24.2' + testImplementation group: 'org.assertj', name: 'assertj-core', version: '3.25.1' testImplementation group: 'org.testcontainers', name: 'junit-jupiter', version: '1.19.3' // Log4J - implementation group: 'org.apache.logging.log4j', name: 'log4j-core', version: '2.22.0' - runtimeOnly group: 'org.apache.logging.log4j', name: 'log4j-api', version: '2.22.0' - runtimeOnly group: 'org.apache.logging.log4j', name: 'log4j-slf4j2-impl', version: '2.22.0' + implementation group: 'org.apache.logging.log4j', name: 'log4j-core', version: '2.22.1' + runtimeOnly group: 'org.apache.logging.log4j', name: 'log4j-api', version: '2.22.1' + runtimeOnly group: 'org.apache.logging.log4j', name: 'log4j-slf4j2-impl', version: '2.22.1' // Avro implementation group: 'io.confluent', name: 'kafka-streams-avro-serde', version: '7.5.1' @@ -71,3 +76,25 @@ test { testLogging.showStandardStreams = true testLogging.exceptionFormat = 'full' } + +[Test, JavaExec].each { targetType -> + tasks.withType(targetType) { task -> + task.systemProperty "micronaut.config.files", configFiles + } +} + +jacocoTestReport { + dependsOn test + reports { + xml.required = false + csv.required = false + } + + afterEvaluate { + classDirectories.setFrom(files(classDirectories.files.collect { + fileTree(dir: it, exclude: [ + '**/Application.class', + ]) + })) + } +} diff --git a/components/order-stream/src/main/java/pfe_broker/order_stream/Application.java b/components/order-stream/src/main/java/pfe_broker/order_stream/Application.java index 34401cd2..fdda9f1b 100644 --- a/components/order-stream/src/main/java/pfe_broker/order_stream/Application.java +++ b/components/order-stream/src/main/java/pfe_broker/order_stream/Application.java @@ -5,9 +5,6 @@ import org.slf4j.LoggerFactory; public class Application { - static { - setProperties(); - } private static final Logger LOG = LoggerFactory.getLogger(Application.class); @@ -15,11 +12,4 @@ public static void main(String[] args) { LOG.info("Starting Order Stream"); Micronaut.run(Application.class, args); } - - public static void setProperties() { - System.setProperty( - "micronaut.config.files", - "classpath:application.yml,classpath:kafka.yml,classpath:redis.yml" - ); - } } diff --git a/components/order-stream/src/main/java/pfe_broker/order_stream/BeanFactory.java b/components/order-stream/src/main/java/pfe_broker/order_stream/BeanFactory.java index 2d4e01e2..d7708818 100644 --- a/components/order-stream/src/main/java/pfe_broker/order_stream/BeanFactory.java +++ b/components/order-stream/src/main/java/pfe_broker/order_stream/BeanFactory.java @@ -7,6 +7,10 @@ import org.apache.kafka.clients.admin.AdminClient; import org.apache.kafka.clients.admin.CreateTopicsOptions; import org.apache.kafka.clients.admin.NewTopic; +import pfe_broker.avro.Order; +import pfe_broker.avro.OrderBookRequest; +import pfe_broker.avro.RejectedOrder; +import pfe_broker.avro.utils.SchemaRecord; @Requires(bean = AdminClient.class) @Factory @@ -34,10 +38,45 @@ NewTopic acceptedOrdersTopic( return new NewTopic(topicName, 2, (short) 1); } + @Bean + NewTopic acceptedOrdersOrderBookTopic( + @Property(name = "kafka.topics.order-book-response") String topicName + ) { + return new NewTopic(topicName, 2, (short) 1); + } + @Bean NewTopic rejectedOrdersTopic( @Property(name = "kafka.topics.rejected-orders") String topicName ) { return new NewTopic(topicName, 2, (short) 1); } + + @Bean + public SchemaRecord ordersSchema( + @Property(name = "kafka.topics.orders") String topicName + ) { + return new SchemaRecord(Order.getClassSchema(), topicName); + } + + @Bean + public SchemaRecord acceptedOrdersSchema( + @Property(name = "kafka.topics.accepted-orders") String topicName + ) { + return new SchemaRecord(Order.getClassSchema(), topicName); + } + + @Bean + public SchemaRecord acceptedOrdersOrderBookSchema( + @Property(name = "kafka.topics.order-book-response") String topicName + ) { + return new SchemaRecord(OrderBookRequest.getClassSchema(), topicName); + } + + @Bean + public SchemaRecord rejectedOrdersSchema( + @Property(name = "kafka.topics.rejected-orders") String topicName + ) { + return new SchemaRecord(RejectedOrder.getClassSchema(), topicName); + } } diff --git a/components/order-stream/src/main/java/pfe_broker/order_stream/OrderIntegrityCheckService.java b/components/order-stream/src/main/java/pfe_broker/order_stream/OrderIntegrityCheckService.java index 35854ae4..e43ed5c5 100644 --- a/components/order-stream/src/main/java/pfe_broker/order_stream/OrderIntegrityCheckService.java +++ b/components/order-stream/src/main/java/pfe_broker/order_stream/OrderIntegrityCheckService.java @@ -14,6 +14,7 @@ import pfe_broker.avro.Order; import pfe_broker.avro.OrderRejectReason; import pfe_broker.avro.Side; +import pfe_broker.avro.Type; import pfe_broker.common.SymbolReader; import pfe_broker.common.UtilsRunning; @@ -52,23 +53,32 @@ void init() { } private boolean verifyUserExistInRedis(String username) { + Boolean userExists = + redisConnection.sync().exists(username + ":balance") == 1; + if (userExists) { + return true; + } else { + // Create the user in redis + redisConnection.sync().set(username + ":balance", "10000"); + } return redisConnection.sync().exists(username + ":balance") == 1; } - private OrderRejectReason marketOrderCheckIntegrity(Order order) { - RedisCommands syncCommands = redisConnection.sync(); - + /** + * Check the integrity of a sell order (market or limit): + * @param order the order to check + * @return null if the order is valid, the reason why it is not valid otherwise + */ + private OrderRejectReason sellVerification( + Order order, + RedisCommands syncCommands + ) { String username = order.getUsername().toString(); String symbol = order.getSymbol().toString(); Integer quantity = order.getQuantity(); - Side side = order.getSide(); String stockKey = username + ":" + symbol; - if (side == Side.BUY) { - return null; - } - syncCommands.watch(stockKey); int countdown = 10; while (countdown-- > 0) { @@ -102,12 +112,103 @@ private OrderRejectReason marketOrderCheckIntegrity(Order order) { return OrderRejectReason.INCORRECT_QUANTITY; } + /** + * Check the integrity of a buy limit order: + * @param order the order to check + * @return null if the order is valid, the reason why it is not valid otherwise + */ + private OrderRejectReason buyLimitVerification( + Order order, + RedisCommands syncCommands + ) { + String username = order.getUsername().toString(); + Integer quantity = order.getQuantity(); + Double price = order.getPrice(); + + Double orderTotalPrice = quantity * price; + + String balanceKey = username + ":balance"; + + syncCommands.watch(balanceKey); + int countdown = 10; + while (countdown-- > 0) { + Double balance = Double.parseDouble(syncCommands.get(balanceKey)); + if (balance < orderTotalPrice) { + LOG.debug("Order {} rejected because of insufficient balance", order); + syncCommands.unwatch(); + return OrderRejectReason.INCORRECT_QUANTITY; + } + syncCommands.multi(); + syncCommands.incrbyfloat(balanceKey, -orderTotalPrice); + try { + syncCommands.exec(); + syncCommands.unwatch(); + return null; + } catch (Exception e) { + LOG.debug("Retrying order {}", order); + } + } + LOG.debug("Order {} rejected because of insufficient stocks", order); + syncCommands.unwatch(); + return OrderRejectReason.INCORRECT_QUANTITY; + } + + /** + * Check the integrity of a market order: + * @param order the order to check + * @return null if the order is valid, the reason why it is not valid otherwise + */ + private OrderRejectReason marketOrderCheckIntegrity(Order order) { + RedisCommands syncCommands = redisConnection.sync(); + + Side side = order.getSide(); + + // No need to check for BUY market orders + if (side == Side.BUY) { + return null; + } + + return sellVerification(order, syncCommands); + } + + /** + * Check the integrity of a limit order: + * @param order the order to check + * @return null if the order is valid, the reason why it is not valid otherwise + */ + private OrderRejectReason limitOrderCheckIntegrity(Order order) { + RedisCommands syncCommands = redisConnection.sync(); + + Side side = order.getSide(); + + if (side == Side.BUY) { + return buyLimitVerification(order, syncCommands); + } + + return sellVerification(order, syncCommands); + } + + /** + * Check the integrity of an order: + * + * What needs to be checked with redis: + * + * Market order: + * - BUY: nothing + * - SELL: check if the user has enough stocks + * + * Limit order: + * - BUY: check if the user has enough balance + * - SELL: check if the user has enough stocks + * + */ public OrderRejectReason checkIntegrity(Order order) { LOG.debug("Checking integrity of order {}", order); String username = order.getUsername().toString(); String symbol = order.getSymbol().toString(); Integer quantity = order.getQuantity(); + Type type = order.getType(); if (username == null || username.isEmpty()) { LOG.debug("Order {} rejected because of empty username", order); @@ -127,14 +228,18 @@ public OrderRejectReason checkIntegrity(Order order) { return OrderRejectReason.UNKNOWN_ACCOUNT; } - OrderRejectReason marketOrderCheckIntegrityResult = - marketOrderCheckIntegrity(order); + OrderRejectReason orderCheckIntegrityResult = OrderRejectReason.OTHER; + if (type == Type.MARKET) { + orderCheckIntegrityResult = marketOrderCheckIntegrity(order); + } else if (type == Type.LIMIT) { + orderCheckIntegrityResult = limitOrderCheckIntegrity(order); + } - if (marketOrderCheckIntegrityResult == null) { - LOG.debug("Market order {} accepted", order); + if (orderCheckIntegrityResult == null) { + LOG.debug("Order {} accepted", order); } - return marketOrderCheckIntegrityResult; + return orderCheckIntegrityResult; } /** diff --git a/components/order-stream/src/main/java/pfe_broker/order_stream/OrderStream.java b/components/order-stream/src/main/java/pfe_broker/order_stream/OrderStream.java index e0e88023..2286b189 100644 --- a/components/order-stream/src/main/java/pfe_broker/order_stream/OrderStream.java +++ b/components/order-stream/src/main/java/pfe_broker/order_stream/OrderStream.java @@ -17,7 +17,10 @@ import org.apache.kafka.streams.kstream.KStream; import org.apache.kafka.streams.kstream.Produced; import pfe_broker.avro.Order; +import pfe_broker.avro.OrderBookRequest; +import pfe_broker.avro.OrderBookRequestType; import pfe_broker.avro.RejectedOrder; +import pfe_broker.avro.Type; @Factory public class OrderStream { @@ -37,6 +40,9 @@ public class OrderStream { @Property(name = "kafka.topics.rejected-orders") private String rejectedOrdersTopic; + @Property(name = "kafka.topics.order-book-request") + private String orderBookRequestTopic; + private final Serdes.StringSerde keySerde = new Serdes.StringSerde(); @Singleton @@ -73,20 +79,37 @@ KStream orderStreamIntegrity(ConfiguredStreamBuilder builder) { private void processAcceptedAndRejectedOrders( KStream integrityCheckedOrdersStream ) { - KStream acceptedOrders = integrityCheckedOrdersStream - .filter((key, value) -> value.orderRejectReason() == null) + KStream acceptedOrdersMarket = integrityCheckedOrdersStream + .filter((key, value) -> + value.orderRejectReason() == null && + value.order().getType() == Type.MARKET + ) .mapValues(OrderIntegrityCheckRecord::order); + KStream acceptedOrdersLimit = + integrityCheckedOrdersStream + .filter((key, value) -> + value.orderRejectReason() == null && + value.order().getType() == Type.LIMIT + ) + .mapValues(value -> + new OrderBookRequest(OrderBookRequestType.NEW, value.order(), null) + ); + KStream rejectedOrders = integrityCheckedOrdersStream .filter((key, value) -> value.orderRejectReason() != null) .mapValues(value -> new RejectedOrder(value.order(), value.orderRejectReason()) ); - acceptedOrders.to( + acceptedOrdersMarket.to( acceptedOrdersTopic, Produced.with(keySerde, this.orderAvroSerde()) ); + acceptedOrdersLimit.to( + orderBookRequestTopic, + Produced.with(keySerde, this.orderBookRequestAvroSerde()) + ); rejectedOrders.to( rejectedOrdersTopic, Produced.with(keySerde, this.rejectedOrderAvroSerde()) @@ -105,6 +128,19 @@ private SpecificAvroSerde orderAvroSerde() { return orderAvroSerde; } + private SpecificAvroSerde orderBookRequestAvroSerde() { + SpecificAvroSerde orderBookRequestAvroSerde = + new SpecificAvroSerde<>(); + + Map serdeConfig = new HashMap<>(); + serdeConfig.put( + AbstractKafkaSchemaSerDeConfig.SCHEMA_REGISTRY_URL_CONFIG, + schemaRegistryUrl + ); + orderBookRequestAvroSerde.configure(serdeConfig, false); + return orderBookRequestAvroSerde; + } + private SpecificAvroSerde rejectedOrderAvroSerde() { SpecificAvroSerde rejectedOrderAvroSerde = new SpecificAvroSerde<>(); diff --git a/components/order-stream/src/test/java/pfe_broker/order_stream/OrderStreamTest.java b/components/order-stream/src/test/java/pfe_broker/order_stream/OrderStreamTest.java index bdb3cd44..1c901921 100644 --- a/components/order-stream/src/test/java/pfe_broker/order_stream/OrderStreamTest.java +++ b/components/order-stream/src/test/java/pfe_broker/order_stream/OrderStreamTest.java @@ -17,6 +17,7 @@ import org.testcontainers.junit.jupiter.Testcontainers; import pfe_broker.avro.Order; import pfe_broker.avro.Side; +import pfe_broker.avro.Type; import pfe_broker.common.utils.KafkaTestContainer; import pfe_broker.common.utils.RedisTestContainer; import pfe_broker.order_stream.mocks.MockOrderListener; @@ -26,9 +27,6 @@ @Testcontainers(disabledWithoutDocker = true) @TestInstance(TestInstance.Lifecycle.PER_CLASS) public class OrderStreamTest implements TestPropertyProvider { - static { - Application.setProperties(); - } @Container static final KafkaTestContainer kafka = new KafkaTestContainer(); @@ -44,12 +42,7 @@ public class OrderStreamTest implements TestPropertyProvider { if (!kafka.isRunning()) { kafka.start(); } - kafka.registerTopics( - "orders", - "accepted-orders", - "rejected-orders", - "market-data.AAPL" - ); + kafka.registerTopics("market-data.AAPL"); if (!redis.isRunning()) { redis.start(); } @@ -71,6 +64,7 @@ void setup( orderIntegrityCheckService.retreiveSymbols(); mockOrderListener.acceptedOrders.clear(); mockOrderListener.rejectedOrders.clear(); + mockOrderListener.orderBookRequests.clear(); redisConnection.sync().flushall(); // Register user redisConnection.sync().set("user:balance", "100000"); @@ -82,7 +76,15 @@ void testOrderStreamBuyMarketOrder( MockOrderListener mockOrderListener ) { // Given - Order order = new Order("user", "AAPL", 10, Side.BUY); + Order order = new Order( + "user", + "AAPL", + 10, + Side.BUY, + Type.MARKET, + null, + "1" + ); // When mockOrderProducer.sendOrder("user", order); @@ -104,7 +106,15 @@ void testOrderStreamSellMarketOrder( ) { // Given redisConnection.sync().set("user:AAPL", "10"); - Order order = new Order("user", "AAPL", 7, Side.SELL); + Order order = new Order( + "user", + "AAPL", + 7, + Side.SELL, + Type.MARKET, + null, + "1" + ); // When mockOrderProducer.sendOrder("user", order); @@ -127,7 +137,15 @@ void testOrderStreamSellMarketOrderInsufficientStocks( ) { // Given redisConnection.sync().set("user:AAPL", "9"); - Order order = new Order("user", "AAPL", 10, Side.SELL); + Order order = new Order( + "user", + "AAPL", + 10, + Side.SELL, + Type.MARKET, + null, + "1" + ); // When mockOrderProducer.sendOrder("user", order); @@ -142,4 +160,70 @@ void testOrderStreamSellMarketOrderInsufficientStocks( assertThat(redisConnection.sync().get("user:AAPL")).isEqualTo("9"); }); } + + @Test + void testOrderStreamBuyLimitOrder( + MockOrderProducer mockOrderProducer, + MockOrderListener mockOrderListener, + StatefulRedisConnection redisConnection + ) { + // Given + Order order = new Order( + "user", + "AAPL", + 10, + Side.BUY, + Type.LIMIT, + 100.0, + "1" + ); + + // When + mockOrderProducer.sendOrder("user", order); + + // Then + await() + .pollInterval(Duration.ofSeconds(1)) + .atMost(Duration.ofSeconds(10)) + .untilAsserted(() -> { + assertThat(mockOrderListener.acceptedOrders).hasSize(0); + assertThat(mockOrderListener.rejectedOrders).hasSize(0); + assertThat(redisConnection.sync().get("user:balance")) + .isEqualTo("99000"); + assertThat(mockOrderListener.orderBookRequests).hasSize(1); + }); + } + + @Test + void testOrderStreamSellLimitOrder( + MockOrderProducer mockOrderProducer, + MockOrderListener mockOrderListener, + StatefulRedisConnection redisConnection + ) { + // Given + redisConnection.sync().set("user:AAPL", "10"); + Order order = new Order( + "user", + "AAPL", + 7, + Side.SELL, + Type.LIMIT, + 100.0, + "1" + ); + + // When + mockOrderProducer.sendOrder("user", order); + + // Then + await() + .pollInterval(Duration.ofSeconds(1)) + .atMost(Duration.ofSeconds(10)) + .untilAsserted(() -> { + assertThat(mockOrderListener.acceptedOrders).hasSize(0); + assertThat(mockOrderListener.rejectedOrders).hasSize(0); + assertThat(redisConnection.sync().get("user:AAPL")).isEqualTo("3"); + assertThat(mockOrderListener.orderBookRequests).hasSize(1); + }); + } } diff --git a/components/order-stream/src/test/java/pfe_broker/order_stream/mocks/MockOrderListener.java b/components/order-stream/src/test/java/pfe_broker/order_stream/mocks/MockOrderListener.java index 4d03c947..68b3fa2b 100644 --- a/components/order-stream/src/test/java/pfe_broker/order_stream/mocks/MockOrderListener.java +++ b/components/order-stream/src/test/java/pfe_broker/order_stream/mocks/MockOrderListener.java @@ -2,11 +2,13 @@ import io.micronaut.configuration.kafka.annotation.KafkaKey; import io.micronaut.configuration.kafka.annotation.KafkaListener; +import io.micronaut.configuration.kafka.annotation.OffsetReset; import io.micronaut.configuration.kafka.annotation.Topic; import jakarta.inject.Singleton; import java.util.ArrayList; import java.util.List; import pfe_broker.avro.Order; +import pfe_broker.avro.OrderBookRequest; import pfe_broker.avro.RejectedOrder; @Singleton @@ -14,16 +16,32 @@ public class MockOrderListener { public List acceptedOrders = new ArrayList<>(); public List rejectedOrders = new ArrayList<>(); + public List orderBookRequests = new ArrayList<>(); - @KafkaListener("mock-orders-consumer") + @KafkaListener( + groupId = "mock-orders-consumer", + offsetReset = OffsetReset.EARLIEST + ) @Topic("${kafka.topics.accepted-orders}") void receiveAcceptedOrder(@KafkaKey String key, Order order) { acceptedOrders.add(order); } - @KafkaListener("mock-rejected-orders-consumer") + @KafkaListener( + groupId = "mock-rejected-orders-consumer", + offsetReset = OffsetReset.EARLIEST + ) @Topic("${kafka.topics.rejected-orders}") void receiveRejectedOrder(@KafkaKey String key, RejectedOrder order) { rejectedOrders.add(order); } + + @KafkaListener( + groupId = "mock-order-book-request-consumer", + offsetReset = OffsetReset.EARLIEST + ) + @Topic("${kafka.topics.order-book-request}") + void receiveOrderBookRequest(@KafkaKey String key, OrderBookRequest order) { + orderBookRequests.add(order); + } } diff --git a/components/pre-processing/pre_processing/constant.py b/components/pre-processing/pre_processing/constant.py index 73def44c..1df57d21 100644 --- a/components/pre-processing/pre_processing/constant.py +++ b/components/pre-processing/pre_processing/constant.py @@ -6,6 +6,8 @@ TARGET_DATE_REGEX = r"\d{4}-\d{2}-\d{2}" +MARKET_DATA_PARTIONS = 3 + CSV_COLUMNS = [ "date", "X", diff --git a/components/pre-processing/pre_processing/kafka/admin.py b/components/pre-processing/pre_processing/kafka/admin.py index a90f6967..8a8414b7 100644 --- a/components/pre-processing/pre_processing/kafka/admin.py +++ b/components/pre-processing/pre_processing/kafka/admin.py @@ -3,6 +3,8 @@ from confluent_kafka.admin import AdminClient as _AdminClient from confluent_kafka.admin import NewTopic +from pre_processing.constant import MARKET_DATA_PARTIONS + logger = logging.getLogger("pre_processing.kafka.admin") @@ -14,7 +16,8 @@ def create_topics(self, topics: list[NewTopic]): """Create topics""" new_topics = [ - NewTopic(topic, num_partitions=3, replication_factor=1) for topic in topics + NewTopic(topic, num_partitions=MARKET_DATA_PARTIONS, replication_factor=1) + for topic in topics ] # Call create_topics to asynchronously create topics, a dict # of is returned. diff --git a/components/pre-processing/pre_processing/kafka/data_pipeline.py b/components/pre-processing/pre_processing/kafka/data_pipeline.py index 57f75cc1..1ac1161c 100644 --- a/components/pre-processing/pre_processing/kafka/data_pipeline.py +++ b/components/pre-processing/pre_processing/kafka/data_pipeline.py @@ -8,6 +8,8 @@ from typing import Dict, Generator, List, cast import pandas as pd + +from pre_processing.constant import MARKET_DATA_PARTIONS from pre_processing.dataframe_extraction.hour import complete_a_day_hour_by_hour from pre_processing.decorators import performance_timer_decorator from pre_processing.kafka.producer import AIOProducer @@ -161,12 +163,16 @@ def consumer(self, process_num: int, entries_per_process: int): seg_iter_dict: dict[str, tuple[int, int]] = { ticker: (0, 0) for ticker in process_data } + counter = 0 while True: # Wait for the synchronization event to be set self.main_sync_event.wait() start_time = time.perf_counter() + partition_number = (process_num + counter) % MARKET_DATA_PARTIONS + counter += 1 + last_key: str = "" for index, ticker_name in enumerate(process_data): data, seg_iter_dict[ticker_name] = self.recover_data( @@ -176,7 +182,9 @@ def consumer(self, process_num: int, entries_per_process: int): key, value = data if index == 0: last_key = key - kafka_producer.produce(self.topic_prefix + ticker_name, value, key) + kafka_producer.produce( + self.topic_prefix + ticker_name, value, key, partition_number + ) else: logger.warning(f"Data is None for {ticker_name}") @@ -291,7 +299,7 @@ def run(self): overflow_exec_time = 0 while not self.exited: start_time = time.perf_counter() - logger.info("-------------------------------") + logger.debug("-------------------------------") # Set the synchronization event self.main_sync_event.set() @@ -320,7 +328,7 @@ def run(self): ) ) else: - logger.info( + logger.debug( ( "Global sending time is lower than interval time:" f"{end_time - start_time} < {self.interval_seconds}" diff --git a/components/pre-processing/pre_processing/kafka/producer.py b/components/pre-processing/pre_processing/kafka/producer.py index 643315cc..c6bcd7fd 100644 --- a/components/pre-processing/pre_processing/kafka/producer.py +++ b/components/pre-processing/pre_processing/kafka/producer.py @@ -29,12 +29,18 @@ def on_delivery(self, err, msg): else: pass - def produce(self, topic: str, value: Any, key: Any = None): + def produce( + self, topic: str, value: Any, key: Any = None, partition: int = -1 + ) -> None: """ An awaitable produce method. """ self._producer.produce( - topic, key=key, value=value, on_delivery=self.on_delivery + topic, + key=key, + value=value, + on_delivery=self.on_delivery, + partition=partition, ) def flush(self, timeout: float = 5.0) -> None: diff --git a/components/pre-processing/pre_processing/main.py b/components/pre-processing/pre_processing/main.py index 69dbb18e..3beddb91 100644 --- a/components/pre-processing/pre_processing/main.py +++ b/components/pre-processing/pre_processing/main.py @@ -84,7 +84,7 @@ def main( """ Main function to feed kafka with data from 2022-04-05 """ - setup_logs("pre_processing") + setup_logs("pre_processing", level=logging.DEBUG) if not verify_day_extracted(day): extract_day(day=day) diff --git a/components/pre-processing/project.json b/components/pre-processing/project.json index 2de70846..b1b574a6 100644 --- a/components/pre-processing/project.json +++ b/components/pre-processing/project.json @@ -64,7 +64,7 @@ "serve": { "executor": "@nxlv/python:run-commands", "options": { - "command": "poetry run python pre_processing/main.py", + "command": "poetry run python pre_processing/main.py --skip-schema-creation --skip-topic-creation", "cwd": "components/pre-processing" } } diff --git a/components/pre-processing/pyproject.toml b/components/pre-processing/pyproject.toml index f777a008..c9b7f104 100644 --- a/components/pre-processing/pyproject.toml +++ b/components/pre-processing/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "pre-processing" -version = "0.2.0" +version = "0.3.0" description = "" authors = ["Samuel Guillemet "] readme = "README.md" diff --git a/components/quickfix-server/build.gradle b/components/quickfix-server/build.gradle index c8584478..3e429cc7 100644 --- a/components/quickfix-server/build.gradle +++ b/components/quickfix-server/build.gradle @@ -1,6 +1,11 @@ plugins { id "com.github.johnrengelman.shadow" id "io.micronaut.application" + id "jacoco" +} + +ext { + configFiles = "classpath:application.yml,classpath:kafka.yml,classpath:quickfix.yml,classpath:data.yml" } version = "${version}" @@ -21,12 +26,12 @@ dependencies { implementation group: 'io.micronaut.kafka', name: 'micronaut-kafka', version: '5.2.0' // Database - implementation group: 'io.micronaut.data', name: 'micronaut-data-hibernate-jpa', version: '4.4.0' + implementation group: 'io.micronaut.data', name: 'micronaut-data-hibernate-jpa', version: '4.4.1' // Log4J - implementation group: 'org.apache.logging.log4j', name: 'log4j-core', version: '2.22.0' - runtimeOnly group: 'org.apache.logging.log4j', name: 'log4j-api', version: '2.22.0' - runtimeOnly group: 'org.apache.logging.log4j', name: 'log4j-slf4j2-impl', version: '2.22.0' + implementation group: 'org.apache.logging.log4j', name: 'log4j-core', version: '2.22.1' + runtimeOnly group: 'org.apache.logging.log4j', name: 'log4j-api', version: '2.22.1' + runtimeOnly group: 'org.apache.logging.log4j', name: 'log4j-slf4j2-impl', version: '2.22.1' // Avro implementation group: 'io.confluent', name: 'kafka-avro-serializer', version: '7.5.1' @@ -36,7 +41,7 @@ dependencies { implementation group: 'org.quickfixj', name: 'quickfixj-messages-all', version: '2.3.1' // Test implementation - testImplementation group: 'org.assertj', name: 'assertj-core', version: '3.24.2' + testImplementation group: 'org.assertj', name: 'assertj-core', version: '3.25.1' testImplementation group: 'org.testcontainers', name: 'postgresql', version: '1.19.3' testImplementation group: 'org.testcontainers', name: 'junit-jupiter', version: '1.19.3' testImplementation group: 'org.awaitility', name: 'awaitility', version: '4.2.0' @@ -88,3 +93,25 @@ test { testLogging.showStandardStreams = true testLogging.exceptionFormat = 'full' } + +[Test, JavaExec].each { targetType -> + tasks.withType(targetType) { task -> + task.systemProperty "micronaut.config.files", configFiles + } +} + +jacocoTestReport { + dependsOn test + reports { + xml.required = false + csv.required = false + } + + afterEvaluate { + classDirectories.setFrom(files(classDirectories.files.collect { + fileTree(dir: it, exclude: [ + '**/Application.class', + ]) + })) + } +} diff --git a/components/quickfix-server/src/main/java/pfe_broker/quickfix_server/Application.java b/components/quickfix-server/src/main/java/pfe_broker/quickfix_server/Application.java index 3f8542f9..2a38348c 100644 --- a/components/quickfix-server/src/main/java/pfe_broker/quickfix_server/Application.java +++ b/components/quickfix-server/src/main/java/pfe_broker/quickfix_server/Application.java @@ -5,9 +5,6 @@ import org.slf4j.LoggerFactory; public class Application { - static { - setProperties(); - } private static final Logger LOG = LoggerFactory.getLogger(Application.class); @@ -15,11 +12,4 @@ public static void main(String[] args) { LOG.info("Starting QuickFIX/J server"); Micronaut.run(Application.class, args); } - - public static void setProperties() { - System.setProperty( - "micronaut.config.files", - "classpath:application.yml,classpath:kafka.yml,classpath:quickfix.yml,classpath:data.yml" - ); - } } diff --git a/components/quickfix-server/src/main/java/pfe_broker/quickfix_server/MessageSender.java b/components/quickfix-server/src/main/java/pfe_broker/quickfix_server/MessageSender.java new file mode 100644 index 00000000..a6e458d1 --- /dev/null +++ b/components/quickfix-server/src/main/java/pfe_broker/quickfix_server/MessageSender.java @@ -0,0 +1,42 @@ +package pfe_broker.quickfix_server; + +import jakarta.inject.Inject; +import jakarta.inject.Singleton; +import java.util.HashMap; +import java.util.Map; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import pfe_broker.quickfix_server.interfaces.IMessageSender; +import quickfix.Message; +import quickfix.Session; +import quickfix.SessionID; +import quickfix.SessionNotFound; + +@Singleton +public class MessageSender implements IMessageSender { + + private static final Logger LOG = LoggerFactory.getLogger( + MessageSender.class + ); + + private Map sessionIDMap = new HashMap<>(); + + @Inject + private QuickFixLogger quickFixLogger; + + @Override + public void registerNewUser(String username, SessionID sessionID) { + sessionIDMap.put(username, sessionID); + } + + @Override + public void sendMessage(Message message, String username) { + quickFixLogger.logQuickFixJMessage(message, "Sending message"); + SessionID sessionID = sessionIDMap.get(username); + try { + Session.sendToTarget(message, sessionID); + } catch (SessionNotFound | NullPointerException e) { + LOG.error("Session not found for user [{}]({})", username, sessionID); + } + } +} diff --git a/components/quickfix-server/src/main/java/pfe_broker/quickfix_server/OrderProducer.java b/components/quickfix-server/src/main/java/pfe_broker/quickfix_server/OrderProducer.java index 94aeba99..646cc799 100644 --- a/components/quickfix-server/src/main/java/pfe_broker/quickfix_server/OrderProducer.java +++ b/components/quickfix-server/src/main/java/pfe_broker/quickfix_server/OrderProducer.java @@ -4,9 +4,13 @@ import io.micronaut.configuration.kafka.annotation.KafkaKey; import io.micronaut.configuration.kafka.annotation.Topic; import pfe_broker.avro.Order; +import pfe_broker.avro.OrderBookRequest; @KafkaClient(id = "quickfix-order-producer") public interface OrderProducer { @Topic("${kafka.topics.orders}") void sendOrder(@KafkaKey String key, Order order); + + @Topic("${kafka.topics.order-book-request}") + void sendOrderBookRequest(@KafkaKey String key, OrderBookRequest order); } diff --git a/components/quickfix-server/src/main/java/pfe_broker/quickfix_server/QuickFixLogger.java b/components/quickfix-server/src/main/java/pfe_broker/quickfix_server/QuickFixLogger.java new file mode 100644 index 00000000..02593a1e --- /dev/null +++ b/components/quickfix-server/src/main/java/pfe_broker/quickfix_server/QuickFixLogger.java @@ -0,0 +1,76 @@ +package pfe_broker.quickfix_server; + +import io.micronaut.context.annotation.Property; +import io.micronaut.core.io.ResourceLoader; +import jakarta.annotation.PostConstruct; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; +import java.util.Arrays; +import java.util.List; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import quickfix.ConfigError; +import quickfix.DataDictionary; +import quickfix.Message; + +@Singleton +public class QuickFixLogger { + + private static final Logger LOG = LoggerFactory.getLogger( + QuickFixLogger.class + ); + + @Property(name = "quickfix.config.data_dictionary") + private String dataDictionaryPath; + + @Inject + private ResourceLoader resourceLoader; + + private DataDictionary dataDictionary; + + @PostConstruct + public void init() { + try { + dataDictionary = + new DataDictionary( + resourceLoader + .getResourceAsStream("classpath:" + dataDictionaryPath) + .get() + ); + } catch (ConfigError configError) { + configError.printStackTrace(); + } + } + + public void logQuickFixJMessage(Message message, String prefix) { + List messageParts = Arrays + .stream(message.toString().split("\u0001")) + .map(s -> { + String[] split = s.split("="); + if (split.length != 2) { + return s; + } + String key = split[0]; + String value = split[1]; + + String fieldName = dataDictionary.getFieldName(Integer.parseInt(key)); + String fieldValue = dataDictionary.getValueName( + Integer.parseInt(key), + value + ); + + if (fieldName == null) { + fieldName = key; + } + if (fieldValue == null) { + fieldValue = value; + } + + return fieldName + "=" + fieldValue; + }) + .toList(); + + String messageString = String.join("|", messageParts); + LOG.debug("{}: {}", prefix, messageString); + } +} diff --git a/components/quickfix-server/src/main/java/pfe_broker/quickfix_server/ReportListener.java b/components/quickfix-server/src/main/java/pfe_broker/quickfix_server/ReportListener.java index 162ac3ae..1d232c3b 100644 --- a/components/quickfix-server/src/main/java/pfe_broker/quickfix_server/ReportListener.java +++ b/components/quickfix-server/src/main/java/pfe_broker/quickfix_server/ReportListener.java @@ -7,6 +7,7 @@ import jakarta.inject.Singleton; import java.util.List; import org.apache.kafka.clients.consumer.ConsumerRecord; +import pfe_broker.avro.OrderBookRequest; import pfe_broker.avro.RejectedOrder; import pfe_broker.avro.Trade; @@ -33,4 +34,22 @@ void receiveAcceptedTrade(List> records) { void receiveRejectedOrder(@KafkaKey String key, RejectedOrder rejectedOrder) { serverApplication.sendRejectedOrderReport(key, rejectedOrder); } + + @KafkaListener("quickfix-order-book-response") + @Topic("${kafka.topics.order-book-response}") + void receiveOrderBookResponse( + @KafkaKey String key, + OrderBookRequest orderBookRequest + ) { + serverApplication.sendOrderBookReport(key, orderBookRequest); + } + + @KafkaListener("quickfix-order-book-rejected") + @Topic("${kafka.topics.order-book-rejected}") + void receiveOrderBookRejected( + @KafkaKey String key, + OrderBookRequest orderBookRequest + ) { + serverApplication.sendOrderBookRejected(key, orderBookRequest); + } } diff --git a/components/quickfix-server/src/main/java/pfe_broker/quickfix_server/ServerApplication.java b/components/quickfix-server/src/main/java/pfe_broker/quickfix_server/ServerApplication.java index 5a7089be..f59b426a 100644 --- a/components/quickfix-server/src/main/java/pfe_broker/quickfix_server/ServerApplication.java +++ b/components/quickfix-server/src/main/java/pfe_broker/quickfix_server/ServerApplication.java @@ -1,25 +1,20 @@ package pfe_broker.quickfix_server; -import io.micronaut.context.annotation.Property; -import io.micronaut.core.io.ResourceLoader; -import jakarta.annotation.PostConstruct; import jakarta.inject.Inject; import jakarta.inject.Singleton; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import pfe_broker.avro.Order; +import pfe_broker.avro.OrderBookRequest; +import pfe_broker.avro.OrderBookRequestType; import pfe_broker.avro.RejectedOrder; import pfe_broker.avro.Trade; +import pfe_broker.avro.Type; import pfe_broker.avro.utils.Converters; import pfe_broker.models.domains.User; import pfe_broker.models.repositories.UserRepository; +import pfe_broker.quickfix_server.interfaces.IMessageSender; import quickfix.Application; -import quickfix.ConfigError; -import quickfix.DataDictionary; import quickfix.DoNotSend; import quickfix.FieldNotFound; import quickfix.IncorrectDataFormat; @@ -27,35 +22,33 @@ import quickfix.Message; import quickfix.MessageCracker; import quickfix.RejectLogon; -import quickfix.Session; import quickfix.SessionID; -import quickfix.SessionNotFound; import quickfix.UnsupportedMessageType; import quickfix.field.AvgPx; +import quickfix.field.ClOrdID; import quickfix.field.CumQty; +import quickfix.field.CxlRejResponseTo; import quickfix.field.ExecID; import quickfix.field.ExecType; import quickfix.field.LeavesQty; -import quickfix.field.MDEntryPx; -import quickfix.field.MDEntrySize; -import quickfix.field.MDEntryType; -import quickfix.field.MDReqID; -import quickfix.field.NoRelatedSym; import quickfix.field.OrdRejReason; import quickfix.field.OrdStatus; +import quickfix.field.OrdType; import quickfix.field.OrderID; import quickfix.field.OrderQty; +import quickfix.field.OrigClOrdID; import quickfix.field.Password; import quickfix.field.SenderCompID; import quickfix.field.Side; import quickfix.field.Symbol; -import quickfix.field.TargetCompID; import quickfix.field.Username; import quickfix.fix44.ExecutionReport; import quickfix.fix44.Logon; import quickfix.fix44.MarketDataRequest; -import quickfix.fix44.MarketDataSnapshotFullRefresh; import quickfix.fix44.NewOrderSingle; +import quickfix.fix44.OrderCancelReject; +import quickfix.fix44.OrderCancelReplaceRequest; +import quickfix.fix44.OrderCancelRequest; @Singleton public class ServerApplication extends MessageCracker implements Application { @@ -64,35 +57,17 @@ public class ServerApplication extends MessageCracker implements Application { ServerApplication.class ); - private Map sessionIDMap = new HashMap<>(); - @Inject - private OrderProducer orderProducer; + private QuickFixLogger quickFixLogger; @Inject - private UserRepository userRepository; + private IMessageSender messageSender; - @Property(name = "quickfix.config.data_dictionary") - private String dataDictionaryPath; + @Inject + private OrderProducer orderProducer; @Inject - ResourceLoader resourceLoader; - - DataDictionary dataDictionary; - - @PostConstruct - public void init() { - try { - dataDictionary = - new DataDictionary( - resourceLoader - .getResourceAsStream("classpath:" + dataDictionaryPath) - .get() - ); - } catch (ConfigError configError) { - configError.printStackTrace(); - } - } + private UserRepository userRepository; private Integer orderKey = 0; private Integer executionKey = 0; @@ -120,11 +95,11 @@ public void toAdmin(Message message, SessionID sessionId) {} @Override public void fromAdmin(Message message, SessionID sessionId) throws FieldNotFound, IncorrectDataFormat, IncorrectTagValue, RejectLogon { - try { - String sender = message - .getHeader() - .getString(quickfix.field.SenderCompID.FIELD); - if (message.isAdmin() && message instanceof Logon) { + if (message.isAdmin() && message instanceof Logon) { + try { + String sender = message + .getHeader() + .getString(quickfix.field.SenderCompID.FIELD); String username = message.getString(Username.FIELD); String password = message.getString(Password.FIELD); @@ -136,10 +111,11 @@ public void fromAdmin(Message message, SessionID sessionId) throw new RejectLogon("Invalid username or password"); } - sessionIDMap.put(sender, sessionId); + messageSender.registerNewUser(sender, sessionId); + } catch (FieldNotFound e) { + e.printStackTrace(); + throw new RejectLogon("Invalid username or password"); } - } catch (FieldNotFound e) { - e.printStackTrace(); } } @@ -149,142 +125,332 @@ public void toApp(Message message, SessionID sessionId) throws DoNotSend {} @Override public void fromApp(Message message, SessionID sessionId) throws FieldNotFound, IncorrectDataFormat, IncorrectTagValue, UnsupportedMessageType { - logQuickFixJMessage(message, "Received message"); + quickFixLogger.logQuickFixJMessage(message, "Received message"); crack(message, sessionId); } + /** + * This method is called when a NewOrderSingle message is received + * @param message + * @param sessionID + * @throws FieldNotFound + * @throws UnsupportedMessageType + * @throws IncorrectTagValue + */ public void onMessage(NewOrderSingle message, SessionID sessionID) throws FieldNotFound, UnsupportedMessageType, IncorrectTagValue { - LOG.debug("Received new Single Order"); - Order avroOrder = new Order( - message.getHeader().getString(SenderCompID.FIELD), - message.getString(Symbol.FIELD), - message.getInt(OrderQty.FIELD), - Converters.Side.toAvro(message.getSide()) - ); - String key = - avroOrder.getUsername() + - ":" + - message.getString(quickfix.field.ClOrdID.FIELD); + String username = message.getHeader().getString(SenderCompID.FIELD); + String symbol = message.getString(Symbol.FIELD); + int quantity = message.getInt(OrderQty.FIELD); + pfe_broker.avro.Side side = Converters.Side.toAvro(message.getSide()); + pfe_broker.avro.Type type = Converters.Type.toAvro(message.getOrdType()); + String clOrdID = message.getString(ClOrdID.FIELD); + + Order order; + switch (type) { + case MARKET: + order = + new Order(username, symbol, quantity, side, type, null, clOrdID); + break; + case LIMIT: + double price = message.getDouble(quickfix.field.Price.FIELD); + order = + new Order(username, symbol, quantity, side, type, price, clOrdID); + break; + default: + throw new IncorrectTagValue(quickfix.field.OrdType.FIELD); + } + + String key = username + ":" + orderKey.toString(); orderKey++; - orderProducer.sendOrder(key, avroOrder); + orderProducer.sendOrder(key, order); } - public void onMessage(MarketDataRequest message, SessionID sessionID) { - try { - sendMarketDataSnapshot(message); - } catch (quickfix.FieldNotFound e) { - e.printStackTrace(); + /** + * This method is called when a OrderCancelReplaceRequest message is received + * @param message + * @param sessionID + * @throws FieldNotFound + * @throws UnsupportedMessageType + * @throws IncorrectTagValue + */ + + public void onMessage(OrderCancelReplaceRequest message, SessionID sessionID) + throws FieldNotFound, UnsupportedMessageType, IncorrectTagValue { + String username = message.getHeader().getString(SenderCompID.FIELD); + String symbol = message.getString(Symbol.FIELD); + int quantity = message.getInt(OrderQty.FIELD); + pfe_broker.avro.Side side = Converters.Side.toAvro(message.getSide()); + pfe_broker.avro.Type type = Converters.Type.toAvro(message.getOrdType()); + String clOrdID = message.getString(ClOrdID.FIELD); + String origClOrdID = message.getString(OrigClOrdID.FIELD); + String orderId = message.getString(OrderID.FIELD); + + if (type != pfe_broker.avro.Type.LIMIT) { + throw new IncorrectTagValue(quickfix.field.OrdType.FIELD); } - } - public void sendMarketDataSnapshot(MarketDataRequest message) - throws FieldNotFound { - MarketDataSnapshotFullRefresh fixMD = createMarketDataSnapshot(message); + double price = message.getDouble(quickfix.field.Price.FIELD); + Order order = new Order( + username, + symbol, + quantity, + side, + type, + price, + clOrdID + ); - String senderCompId = message.getHeader().getString(SenderCompID.FIELD); - String targetCompId = message.getHeader().getString(TargetCompID.FIELD); - fixMD.getHeader().setString(SenderCompID.FIELD, targetCompId); - fixMD.getHeader().setString(TargetCompID.FIELD, senderCompId); + String key = username + ":" + orderId; + OrderBookRequest orderBookRequest = new OrderBookRequest( + OrderBookRequestType.REPLACE, + order, + origClOrdID + ); - sendMessage(message, targetCompId); + orderProducer.sendOrderBookRequest(key, orderBookRequest); } - public MarketDataSnapshotFullRefresh createMarketDataSnapshot( - MarketDataRequest message - ) throws FieldNotFound { - MarketDataRequest.NoRelatedSym noRelatedSyms = - new MarketDataRequest.NoRelatedSym(); - - int relatedSymbolCount = message.getInt(NoRelatedSym.FIELD); - - MarketDataSnapshotFullRefresh fixMD = new MarketDataSnapshotFullRefresh(); - fixMD.setString(MDReqID.FIELD, message.getString(MDReqID.FIELD)); - - for (int i = 1; i <= relatedSymbolCount; ++i) { - message.getGroup(i, noRelatedSyms); - String symbol = noRelatedSyms.getString(Symbol.FIELD); - fixMD.setString(Symbol.FIELD, symbol); - - double symbolPrice = 0.0; - int symbolVolume = 0; + /** + * This method is called when a OrderCancelRequest message is received + * @param message + * @param sessionID + * @throws FieldNotFound + * @throws UnsupportedMessageType + * @throws IncorrectTagValue + */ + public void onMessage(OrderCancelRequest message, SessionID sessionID) + throws FieldNotFound, UnsupportedMessageType, IncorrectTagValue { + String username = message.getHeader().getString(SenderCompID.FIELD); + String symbol = message.getString(Symbol.FIELD); + pfe_broker.avro.Side side = Converters.Side.toAvro(message.getSide()); + String clOrdID = message.getString(ClOrdID.FIELD); + String origClOrdID = message.getString(OrigClOrdID.FIELD); + String orderId = message.getString(OrderID.FIELD); + + Order order = new Order( + username, + symbol, + 0, + side, + Type.LIMIT, + 0.0, + clOrdID + ); - if (symbol.equals("GOOGL")) { - symbolPrice = 123.45; - symbolVolume = 1000; - } else if (symbol.equals("AAPL")) { - symbolPrice = 456.78; - symbolVolume = 1000; - } + String key = username + ":" + orderId; + OrderBookRequest orderBookRequest = new OrderBookRequest( + OrderBookRequestType.CANCEL, + order, + origClOrdID + ); - MarketDataSnapshotFullRefresh.NoMDEntries noMDEntries = - new MarketDataSnapshotFullRefresh.NoMDEntries(); - noMDEntries.setChar(MDEntryType.FIELD, '0'); - noMDEntries.setDouble(MDEntryPx.FIELD, symbolPrice); - noMDEntries.setInt(MDEntrySize.FIELD, symbolVolume); - fixMD.addGroup(noMDEntries); - } + orderProducer.sendOrderBookRequest(key, orderBookRequest); + } - return fixMD; + /** + * This method is called when a MarketDataRequest message is received + * @param message + * @param sessionID + */ + public void onMessage(MarketDataRequest message, SessionID sessionID) { + throw new UnsupportedOperationException( + "Unimplemented method 'onMessage(MarketDataRequest message, SessionID sessionID)'" + ); } + /** + * This method is called to send a trade report + * @param key + * @param trade + */ public void sendTradeReport(String key, Trade trade) { Order order = trade.getOrder(); - String symbol = order.getSymbol().toString(); - String execId = executionKey.toString(); - String clOrdID = key.split(":")[1]; - char side = Converters.Side.charFromAvro(order.getSide()); int tradeQuantity = trade.getQuantity(); int baseQuantity = order.getQuantity(); - double price = trade.getPrice(); - ExecutionReport executionReport = new ExecutionReport( - new OrderID(clOrdID), - new ExecID(execId), - new ExecType(ExecType.TRADE), - new OrdStatus(OrdStatus.FILLED), - new Side(side), - new LeavesQty(baseQuantity - tradeQuantity), - new CumQty(0), - new AvgPx(price) + ExecutionReport executionReport = buildExecutionReport( + key, + order, + OrdStatus.FILLED, + ExecType.TRADE, + baseQuantity - tradeQuantity, + tradeQuantity, + trade.getPrice() ); - executionReport.set(new Symbol(symbol)); - executionReport.set(new OrderQty(tradeQuantity)); - executionKey++; - - sendMessage(executionReport, order.getUsername().toString()); + messageSender.sendMessage(executionReport, order.getUsername().toString()); } + /** + * This method is called to send a rejected order report + * @param key + * @param rejectedOrder + */ public void sendRejectedOrderReport(String key, RejectedOrder rejectedOrder) { Order order = rejectedOrder.getOrder(); + int rejectReason = Converters.OrderRejectReason.intFromAvro( + rejectedOrder.getReason() + ); + + ExecutionReport executionReport = buildExecutionReport( + key, + order, + OrdStatus.REJECTED, + ExecType.REJECTED, + 0, + 0, + 0.0 + ); + executionReport.set(new OrdRejReason(rejectReason)); + + messageSender.sendMessage(executionReport, order.getUsername().toString()); + } + + /** + * This method is called to send an order book report + * @param key + * @param orderBookRequest + */ + public void sendOrderBookReport( + String key, + OrderBookRequest orderBookRequest + ) { + Order order = orderBookRequest.getOrder(); + char execType; + char ordStatus; + + switch (orderBookRequest.getType()) { + case NEW: + execType = ExecType.NEW; + ordStatus = OrdStatus.NEW; + break; + case CANCEL: + execType = ExecType.CANCELED; + ordStatus = OrdStatus.CANCELED; + break; + case REPLACE: + execType = ExecType.REPLACED; + ordStatus = OrdStatus.NEW; + break; + default: + throw new UnsupportedOperationException( + "Unimplemented OrderBookRequestType" + ); + } + + ExecutionReport executionReport = buildExecutionReport( + key, + order, + ordStatus, + execType, + order.getQuantity(), + 0, + order.getPrice() + ); + + if ( + orderBookRequest.getType() == OrderBookRequestType.REPLACE || + orderBookRequest.getType() == OrderBookRequestType.CANCEL + ) { + String origClOrdID = orderBookRequest.getOrigClOrderID().toString(); + executionReport.set(new OrigClOrdID(origClOrdID)); + } + + messageSender.sendMessage(executionReport, order.getUsername().toString()); + } + + /** + * This method is called to send an order book rejected report + * @param key + * @param orderBookRequest + */ + public void sendOrderBookRejected( + String key, + OrderBookRequest orderBookRequest + ) { + Order order = orderBookRequest.getOrder(); + String orderID = key.split(":")[1]; + String clOrdID = order.getClOrderID().toString(); + String origClOrdID = orderBookRequest.getOrigClOrderID().toString(); + OrderBookRequestType orderBookRequestType = orderBookRequest.getType(); + + char cxlRejResponseTo; + switch (orderBookRequestType) { + case CANCEL: + cxlRejResponseTo = CxlRejResponseTo.ORDER_CANCEL_REQUEST; + break; + case REPLACE: + cxlRejResponseTo = CxlRejResponseTo.ORDER_CANCEL_REPLACE_REQUEST; + break; + default: + throw new UnsupportedOperationException( + "Unimplemented OrderBookRequestType" + ); + } + + OrderCancelReject orderCancelReject = new OrderCancelReject( + new OrderID(orderID), + new ClOrdID(clOrdID), + new OrigClOrdID(origClOrdID), + new OrdStatus(OrdStatus.REJECTED), + new CxlRejResponseTo(cxlRejResponseTo) + ); + + messageSender.sendMessage( + orderCancelReject, + order.getUsername().toString() + ); + } + + /** + * This method is called to build an execution report + * @param key the kafka key + * @param order the order + * @param ordStatus + * @param execType + * @param leavesQty + * @param cumQty + * @param avgPx + * @return + */ + private ExecutionReport buildExecutionReport( + String key, + Order order, + char ordStatus, + char execType, + int leavesQty, + int cumQty, + Double avgPx + ) { String symbol = order.getSymbol().toString(); String execId = executionKey.toString(); - String clOrdID = key.split(":")[1]; + String orderID = key.split(":")[1]; char side = Converters.Side.charFromAvro(order.getSide()); + char type = Converters.Type.charFromAvro(order.getType()); int quantity = order.getQuantity(); - OrdRejReason rejectReason = Converters.OrderRejectReason.fromAvro( - rejectedOrder.getReason() - ); + String clOrdID = order.getClOrderID().toString(); + ExecutionReport executionReport = new ExecutionReport( - new OrderID(clOrdID), + new OrderID(orderID), new ExecID(execId), - new ExecType(ExecType.REJECTED), - new OrdStatus(OrdStatus.REJECTED), + new ExecType(execType), + new OrdStatus(ordStatus), new Side(side), - new LeavesQty(quantity), - new CumQty(0), - new AvgPx(0) + new LeavesQty(leavesQty), + new CumQty(cumQty), + new AvgPx(avgPx) ); executionReport.set(new Symbol(symbol)); executionReport.set(new OrderQty(quantity)); - executionReport.set(rejectReason); + executionReport.set(new ClOrdID(clOrdID)); + executionReport.set(new OrdType(type)); executionKey++; - sendMessage(executionReport, order.getUsername().toString()); + return executionReport; } /** @@ -294,53 +460,11 @@ private boolean checkCredentials(String username, String password) { User user; User userMatch = userRepository.findByUsername(username).orElse(null); if (userMatch == null) { - user = new User("user1", "password", 1000.0); + user = new User(username, password, 1000.0); userRepository.save(user); } else { user = userMatch; } return user != null && user.getPassword().equals(password); } - - protected void sendMessage(Message message, String username) { - logQuickFixJMessage(message, "Sending message"); - SessionID sessionID = sessionIDMap.get(username); - try { - Session.sendToTarget(message, sessionID); - } catch (SessionNotFound | NullPointerException e) { - LOG.error("Session not found for user [{}]({})", username, sessionID); - } - } - - private void logQuickFixJMessage(Message message, String prefix) { - List messageParts = Arrays - .stream(message.toString().split("\u0001")) - .map(s -> { - String[] split = s.split("="); - if (split.length != 2) { - return s; - } - String key = split[0]; - String value = split[1]; - - String fieldName = dataDictionary.getFieldName(Integer.parseInt(key)); - String fieldValue = dataDictionary.getValueName( - Integer.parseInt(key), - value - ); - - if (fieldName == null) { - fieldName = key; - } - if (fieldValue == null) { - fieldValue = value; - } - - return fieldName + "=" + fieldValue; - }) - .toList(); - - String messageString = String.join("|", messageParts); - LOG.debug("{}: {}", prefix, messageString); - } } diff --git a/components/quickfix-server/src/main/java/pfe_broker/quickfix_server/interfaces/IMessageSender.java b/components/quickfix-server/src/main/java/pfe_broker/quickfix_server/interfaces/IMessageSender.java new file mode 100644 index 00000000..0db727d7 --- /dev/null +++ b/components/quickfix-server/src/main/java/pfe_broker/quickfix_server/interfaces/IMessageSender.java @@ -0,0 +1,10 @@ +package pfe_broker.quickfix_server.interfaces; + +import quickfix.Message; +import quickfix.SessionID; + +public interface IMessageSender { + public void registerNewUser(String username, SessionID sessionID); + + public void sendMessage(Message message, String username); +} diff --git a/components/quickfix-server/src/test/java/pfe_broker/quickfix_server/ReportListenerTest.java b/components/quickfix-server/src/test/java/pfe_broker/quickfix_server/ReportListenerTest.java index 721b4978..9100ebdf 100644 --- a/components/quickfix-server/src/test/java/pfe_broker/quickfix_server/ReportListenerTest.java +++ b/components/quickfix-server/src/test/java/pfe_broker/quickfix_server/ReportListenerTest.java @@ -10,15 +10,28 @@ import io.micronaut.test.support.TestPropertyProvider; import java.util.Map; import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; import pfe_broker.avro.Order; +import pfe_broker.avro.OrderBookRequest; +import pfe_broker.avro.OrderBookRequestType; +import pfe_broker.avro.OrderRejectReason; +import pfe_broker.avro.RejectedOrder; import pfe_broker.avro.Side; import pfe_broker.avro.Trade; +import pfe_broker.avro.Type; import pfe_broker.common.utils.KafkaTestContainer; +import pfe_broker.quickfix_server.mocks.MockMessageSender; import pfe_broker.quickfix_server.mocks.MockReportProducer; +import quickfix.FieldNotFound; +import quickfix.Message; +import quickfix.field.CxlRejResponseTo; +import quickfix.field.ExecType; +import quickfix.field.OrdStatus; +import quickfix.field.OrderID; @MicronautTest( rollback = false, @@ -35,10 +48,7 @@ ) @Testcontainers(disabledWithoutDocker = true) @TestInstance(TestInstance.Lifecycle.PER_CLASS) -public class ReportListenerTest implements TestPropertyProvider { - static { - Application.setProperties(); - } +class ReportListenerTest implements TestPropertyProvider { @Container static final KafkaTestContainer kafka = new KafkaTestContainer(); @@ -57,12 +67,26 @@ public class ReportListenerTest implements TestPropertyProvider { ); } + @BeforeEach + public void clearMessages(MockMessageSender mockMessageSender) { + mockMessageSender.messages.clear(); + } + @Test - public void testReportListener( + void testAcceptedTrade( MockReportProducer mockReportProducer, - ServerApplication serverApplication - ) { - Order order = new Order("testuser", "AAPL", 10, Side.BUY); + ServerApplication serverApplication, + MockMessageSender mockMessageSender + ) throws InterruptedException, FieldNotFound { + Order order = new Order( + "testuser", + "AAPL", + 10, + Side.BUY, + Type.MARKET, + null, + "0" + ); Trade trade = new Trade(order, "APPL", 100.0, 10); mockReportProducer.sendTrade("testuser:1", trade); @@ -70,7 +94,222 @@ public void testReportListener( await() .atMost(5, TimeUnit.SECONDS) .untilAsserted(() -> { - assertEquals(1, serverApplication.getExecutionKey()); + assertEquals(1, mockMessageSender.messages.size()); + }); + Message message = mockMessageSender.messages.take(); + assertEquals('1', message.getChar(OrderID.FIELD)); + } + + @Test + void testRejectedOrder( + MockReportProducer mockReportProducer, + ServerApplication serverApplication, + MockMessageSender mockMessageSender + ) throws InterruptedException, FieldNotFound { + Order order = new Order( + "testuser", + "AAPL", + 10, + Side.BUY, + Type.MARKET, + null, + "0" + ); + RejectedOrder rejectedOrder = new RejectedOrder( + order, + OrderRejectReason.OTHER + ); + + mockReportProducer.sendRejectedOrder("testuser:1", rejectedOrder); + + await() + .atMost(5, TimeUnit.SECONDS) + .untilAsserted(() -> { + assertEquals(1, mockMessageSender.messages.size()); + }); + + Message message = mockMessageSender.messages.take(); + assertEquals('1', message.getChar(OrderID.FIELD)); + } + + @Test + void testOrderBookResponseNew( + MockReportProducer mockReportProducer, + ServerApplication serverApplication, + MockMessageSender mockMessageSender + ) throws InterruptedException, FieldNotFound { + Order order = new Order( + "testuser", + "AAPL", + 10, + Side.BUY, + Type.LIMIT, + 100.0, + "0" + ); + OrderBookRequest orderBookRequest = new OrderBookRequest( + OrderBookRequestType.NEW, + order, + null + ); + + mockReportProducer.sendOrderBookResponse("testuser:1", orderBookRequest); + + await() + .atMost(5, TimeUnit.SECONDS) + .untilAsserted(() -> { + assertEquals(1, mockMessageSender.messages.size()); + }); + + Message message = mockMessageSender.messages.take(); + assertEquals('1', message.getChar(OrderID.FIELD)); + assertEquals(ExecType.NEW, message.getChar(ExecType.FIELD)); + assertEquals(OrdStatus.NEW, message.getChar(OrdStatus.FIELD)); + } + + @Test + void testOrderBookResponseCancel( + MockReportProducer mockReportProducer, + ServerApplication serverApplication, + MockMessageSender mockMessageSender + ) throws FieldNotFound, InterruptedException { + Order order = new Order( + "testuser", + "AAPL", + 0, + Side.BUY, + Type.LIMIT, + 0.0, + "1" + ); + OrderBookRequest orderBookRequest = new OrderBookRequest( + OrderBookRequestType.CANCEL, + order, + "0" + ); + + mockReportProducer.sendOrderBookResponse("testuser:1", orderBookRequest); + + await() + .atMost(5, TimeUnit.SECONDS) + .untilAsserted(() -> { + assertEquals(1, mockMessageSender.messages.size()); }); + + Message message = mockMessageSender.messages.take(); + assertEquals('1', message.getChar(OrderID.FIELD)); + assertEquals(ExecType.CANCELED, message.getChar(ExecType.FIELD)); + assertEquals(OrdStatus.CANCELED, message.getChar(OrdStatus.FIELD)); + } + + @Test + void testOrderBookReplace( + MockReportProducer mockReportProducer, + ServerApplication serverApplication, + MockMessageSender mockMessageSender + ) throws InterruptedException, FieldNotFound { + Order order = new Order( + "testuser", + "AAPL", + 10, + Side.BUY, + Type.LIMIT, + 90.0, + "1" + ); + OrderBookRequest orderBookRequest = new OrderBookRequest( + OrderBookRequestType.REPLACE, + order, + "0" + ); + + mockReportProducer.sendOrderBookResponse("testuser:1", orderBookRequest); + + await() + .atMost(5, TimeUnit.SECONDS) + .untilAsserted(() -> { + assertEquals(1, mockMessageSender.messages.size()); + }); + + Message message = mockMessageSender.messages.take(); + assertEquals('1', message.getChar(OrderID.FIELD)); + assertEquals(ExecType.REPLACED, message.getChar(ExecType.FIELD)); + assertEquals(OrdStatus.NEW, message.getChar(OrdStatus.FIELD)); + } + + @Test + void testOrderBookRejectedCancel( + MockReportProducer mockReportProducer, + ServerApplication serverApplication, + MockMessageSender mockMessageSender + ) throws FieldNotFound, InterruptedException { + Order order = new Order( + "testuser", + "AAPL", + 0, + Side.BUY, + Type.LIMIT, + 0.0, + "1" + ); + OrderBookRequest orderBookRequest = new OrderBookRequest( + OrderBookRequestType.CANCEL, + order, + "0" + ); + + mockReportProducer.sendOrderBookRejected("testuser:1", orderBookRequest); + + await() + .atMost(5, TimeUnit.SECONDS) + .untilAsserted(() -> { + assertEquals(1, mockMessageSender.messages.size()); + }); + + Message message = mockMessageSender.messages.take(); + assertEquals('1', message.getChar(OrderID.FIELD)); + assertEquals( + CxlRejResponseTo.ORDER_CANCEL_REQUEST, + message.getChar(CxlRejResponseTo.FIELD) + ); + assertEquals(OrdStatus.REJECTED, message.getChar(OrdStatus.FIELD)); + } + + @Test + void testOrderBookRejectedReplace( + MockReportProducer mockReportProducer, + ServerApplication serverApplication, + MockMessageSender mockMessageSender + ) throws InterruptedException, FieldNotFound { + Order order = new Order( + "testuser", + "AAPL", + 10, + Side.BUY, + Type.LIMIT, + 90.0, + "1" + ); + OrderBookRequest orderBookRequest = new OrderBookRequest( + OrderBookRequestType.REPLACE, + order, + "0" + ); + + mockReportProducer.sendOrderBookRejected("testuser:1", orderBookRequest); + + await() + .atMost(5, TimeUnit.SECONDS) + .untilAsserted(() -> { + assertEquals(1, mockMessageSender.messages.size()); + }); + + Message message = mockMessageSender.messages.take(); + assertEquals('1', message.getChar(OrderID.FIELD)); + assertEquals( + CxlRejResponseTo.ORDER_CANCEL_REPLACE_REQUEST, + message.getChar(CxlRejResponseTo.FIELD) + ); + assertEquals(OrdStatus.REJECTED, message.getChar(OrdStatus.FIELD)); } } diff --git a/components/quickfix-server/src/test/java/pfe_broker/quickfix_server/ServerApplicationTest.java b/components/quickfix-server/src/test/java/pfe_broker/quickfix_server/ServerApplicationTest.java index 8758aa66..28bd19f4 100644 --- a/components/quickfix-server/src/test/java/pfe_broker/quickfix_server/ServerApplicationTest.java +++ b/components/quickfix-server/src/test/java/pfe_broker/quickfix_server/ServerApplicationTest.java @@ -2,8 +2,6 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; import io.micronaut.context.annotation.Property; import io.micronaut.core.annotation.NonNull; @@ -13,30 +11,21 @@ import jakarta.inject.Inject; import java.time.Duration; import java.util.Map; -import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; import pfe_broker.common.utils.KafkaTestContainer; -import pfe_broker.models.domains.User; -import pfe_broker.models.repositories.UserRepository; import pfe_broker.quickfix_server.mocks.MockOrderListener; import quickfix.FieldNotFound; import quickfix.IncorrectTagValue; import quickfix.SessionID; import quickfix.UnsupportedMessageType; -import quickfix.field.MDEntryType; -import quickfix.field.MDReqID; -import quickfix.field.MDUpdateType; -import quickfix.field.MarketDepth; import quickfix.field.SenderCompID; -import quickfix.field.SubscriptionRequestType; -import quickfix.field.Symbol; -import quickfix.field.TargetCompID; -import quickfix.fix44.MarketDataRequest; -import quickfix.fix44.MarketDataSnapshotFullRefresh; import quickfix.fix44.NewOrderSingle; +import quickfix.fix44.OrderCancelReplaceRequest; +import quickfix.fix44.OrderCancelRequest; @MicronautTest( rollback = false, @@ -53,10 +42,7 @@ ) @Testcontainers(disabledWithoutDocker = true) @TestInstance(TestInstance.Lifecycle.PER_CLASS) -public class ServerApplicationTest implements TestPropertyProvider { - static { - Application.setProperties(); - } +class ServerApplicationTest implements TestPropertyProvider { @Container static final KafkaTestContainer kafka = new KafkaTestContainer(); @@ -64,11 +50,6 @@ public class ServerApplicationTest implements TestPropertyProvider { @Inject private ServerApplication serverApplication; - @Inject - private UserRepository userRepository; - - private User user; - @Override public @NonNull Map getProperties() { if (!kafka.isRunning()) { @@ -83,14 +64,14 @@ public class ServerApplicationTest implements TestPropertyProvider { ); } - @BeforeAll - void setup() { - user = new User("testuser", "testpassword", 1000.0); - userRepository.save(user); + @AfterEach + void cleanup(MockOrderListener mockOrderListener) { + mockOrderListener.receivedOrders.clear(); + mockOrderListener.receivedOrderBookRequests.clear(); } @Test - public void testOnMessageNewOrderSingle(MockOrderListener mockOrderListener) + void testOnMessageNewOrderSingleMarket(MockOrderListener mockOrderListener) throws FieldNotFound, UnsupportedMessageType, IncorrectTagValue { NewOrderSingle newOrderSingle = new NewOrderSingle( new quickfix.field.ClOrdID("1"), @@ -116,43 +97,89 @@ public void testOnMessageNewOrderSingle(MockOrderListener mockOrderListener) } @Test - void createMarketDataSnapshotTest() throws FieldNotFound { - MarketDataRequest marketDataRequest = new MarketDataRequest(); + void testOnMessageNewOrderSingleLimit(MockOrderListener mockOrderListener) + throws FieldNotFound, UnsupportedMessageType, IncorrectTagValue { + NewOrderSingle newOrderSingle = new NewOrderSingle( + new quickfix.field.ClOrdID("1"), + new quickfix.field.Side(quickfix.field.Side.BUY), + new quickfix.field.TransactTime(), + new quickfix.field.OrdType(quickfix.field.OrdType.LIMIT) + ); + newOrderSingle.set(new quickfix.field.Symbol("AAPL")); + newOrderSingle.set(new quickfix.field.OrderQty(10)); + newOrderSingle.set(new quickfix.field.Price(100.0)); + newOrderSingle.getHeader().setString(SenderCompID.FIELD, "testuser"); - marketDataRequest.set(new MDReqID("1")); - marketDataRequest.set( - new SubscriptionRequestType(SubscriptionRequestType.SNAPSHOT_UPDATES) + serverApplication.onMessage( + newOrderSingle, + new SessionID("FIX.4.4", "testuser", "SERVER") ); - marketDataRequest.set(new MarketDepth(0)); - marketDataRequest.set(new MDUpdateType(MDUpdateType.FULL_REFRESH)); - marketDataRequest.getHeader().setField(new SenderCompID("user1")); - marketDataRequest.getHeader().setField(new TargetCompID("SERVER")); - - MarketDataRequest.NoRelatedSym relatedSymbolGroup1 = - new MarketDataRequest.NoRelatedSym(); - relatedSymbolGroup1.set(new Symbol("GOOGL")); - marketDataRequest.addGroup(relatedSymbolGroup1); - - MarketDataRequest.NoMDEntryTypes entryTypeGroup1 = - new MarketDataRequest.NoMDEntryTypes(); - entryTypeGroup1.set(new MDEntryType(MDEntryType.BID)); - marketDataRequest.addGroup(entryTypeGroup1); - - MarketDataRequest.NoRelatedSym relatedSymbolGroup2 = - new MarketDataRequest.NoRelatedSym(); - relatedSymbolGroup2.set(new Symbol("AAPL")); - marketDataRequest.addGroup(relatedSymbolGroup2); - - MarketDataRequest.NoMDEntryTypes entryTypeGroup2 = - new MarketDataRequest.NoMDEntryTypes(); - entryTypeGroup2.set(new MDEntryType(MDEntryType.BID)); - marketDataRequest.addGroup(entryTypeGroup2); - - MarketDataSnapshotFullRefresh snapshot = null; - snapshot = serverApplication.createMarketDataSnapshot(marketDataRequest); - - assertNotNull(snapshot); - assertEquals("1", snapshot.getMDReqID().getValue()); - assertEquals(2, snapshot.getNoMDEntries().getValue()); + + await() + .pollInterval(Duration.ofSeconds(1)) + .atMost(Duration.ofSeconds(10)) + .untilAsserted(() -> { + assertThat(mockOrderListener.receivedOrders).hasSize(1); + }); + } + + @Test + void testOnMessageOrderCancelRequest(MockOrderListener mockOrderListener) + throws FieldNotFound, UnsupportedMessageType, IncorrectTagValue { + OrderCancelRequest orderCancelRequest = new OrderCancelRequest( + new quickfix.field.OrigClOrdID("1"), + new quickfix.field.ClOrdID("2"), + new quickfix.field.Side(quickfix.field.Side.BUY), + new quickfix.field.TransactTime() + ); + + orderCancelRequest.set(new quickfix.field.OrderID("1")); + orderCancelRequest.set(new quickfix.field.Symbol("AAPL")); + orderCancelRequest.getHeader().setString(SenderCompID.FIELD, "testuser"); + + serverApplication.onMessage( + orderCancelRequest, + new SessionID("FIX.4.4", "testuser", "SERVER") + ); + + await() + .pollInterval(Duration.ofSeconds(1)) + .atMost(Duration.ofSeconds(10)) + .untilAsserted(() -> { + assertThat(mockOrderListener.receivedOrderBookRequests).hasSize(1); + }); + } + + @Test + void testOnMessageOrderCancelReplaceRequest( + MockOrderListener mockOrderListener + ) throws FieldNotFound, UnsupportedMessageType, IncorrectTagValue { + OrderCancelReplaceRequest orderCancelReplaceRequest = + new OrderCancelReplaceRequest( + new quickfix.field.OrigClOrdID("1"), + new quickfix.field.ClOrdID("2"), + new quickfix.field.Side(quickfix.field.Side.BUY), + new quickfix.field.TransactTime(), + new quickfix.field.OrdType(quickfix.field.OrdType.LIMIT) + ); + orderCancelReplaceRequest.set(new quickfix.field.OrderID("1")); + orderCancelReplaceRequest.set(new quickfix.field.Symbol("AAPL")); + orderCancelReplaceRequest.set(new quickfix.field.OrderQty(10)); + orderCancelReplaceRequest.set(new quickfix.field.Price(100.0)); + orderCancelReplaceRequest + .getHeader() + .setString(SenderCompID.FIELD, "testuser"); + + serverApplication.onMessage( + orderCancelReplaceRequest, + new SessionID("FIX.4.4", "testuser", "SERVER") + ); + + await() + .pollInterval(Duration.ofSeconds(1)) + .atMost(Duration.ofSeconds(10)) + .untilAsserted(() -> { + assertThat(mockOrderListener.receivedOrderBookRequests).hasSize(1); + }); } } diff --git a/components/quickfix-server/src/test/java/pfe_broker/quickfix_server/mocks/MockMessageSender.java b/components/quickfix-server/src/test/java/pfe_broker/quickfix_server/mocks/MockMessageSender.java new file mode 100644 index 00000000..ae2daa71 --- /dev/null +++ b/components/quickfix-server/src/test/java/pfe_broker/quickfix_server/mocks/MockMessageSender.java @@ -0,0 +1,33 @@ +package pfe_broker.quickfix_server.mocks; + +import io.micronaut.context.annotation.Replaces; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import pfe_broker.quickfix_server.MessageSender; +import pfe_broker.quickfix_server.QuickFixLogger; +import pfe_broker.quickfix_server.interfaces.IMessageSender; +import quickfix.Message; +import quickfix.SessionID; + +@Replaces(MessageSender.class) +@Singleton +public class MockMessageSender implements IMessageSender { + + @Inject + private QuickFixLogger quickFixLogger; + + public BlockingQueue messages = new LinkedBlockingQueue<>(); + + @Override + public void sendMessage(Message message, String username) { + quickFixLogger.logQuickFixJMessage(message, "Sending message"); + messages.add(message); + } + + @Override + public void registerNewUser(String username, SessionID sessionID) {} +} diff --git a/components/quickfix-server/src/test/java/pfe_broker/quickfix_server/mocks/MockOrderListener.java b/components/quickfix-server/src/test/java/pfe_broker/quickfix_server/mocks/MockOrderListener.java index 4256f61b..3191607e 100644 --- a/components/quickfix-server/src/test/java/pfe_broker/quickfix_server/mocks/MockOrderListener.java +++ b/components/quickfix-server/src/test/java/pfe_broker/quickfix_server/mocks/MockOrderListener.java @@ -7,15 +7,23 @@ import java.util.ArrayList; import java.util.List; import pfe_broker.avro.Order; +import pfe_broker.avro.OrderBookRequest; @Singleton public class MockOrderListener { public List receivedOrders = new ArrayList<>(); + public List receivedOrderBookRequests = new ArrayList<>(); @KafkaListener("mock-orders-consumer") @Topic("${kafka.topics.orders}") void receiveOrder(@KafkaKey String key, Order order) { receivedOrders.add(order); } + + @KafkaListener("mock-order-book-request-consumer") + @Topic("${kafka.topics.order-book-request}") + void receiveOrderBookRequest(@KafkaKey String key, OrderBookRequest order) { + receivedOrderBookRequests.add(order); + } } diff --git a/components/quickfix-server/src/test/java/pfe_broker/quickfix_server/mocks/MockReportProducer.java b/components/quickfix-server/src/test/java/pfe_broker/quickfix_server/mocks/MockReportProducer.java index f1eababa..87caac4b 100644 --- a/components/quickfix-server/src/test/java/pfe_broker/quickfix_server/mocks/MockReportProducer.java +++ b/components/quickfix-server/src/test/java/pfe_broker/quickfix_server/mocks/MockReportProducer.java @@ -3,6 +3,7 @@ import io.micronaut.configuration.kafka.annotation.KafkaClient; import io.micronaut.configuration.kafka.annotation.KafkaKey; import io.micronaut.configuration.kafka.annotation.Topic; +import pfe_broker.avro.OrderBookRequest; import pfe_broker.avro.RejectedOrder; import pfe_broker.avro.Trade; @@ -13,4 +14,16 @@ public interface MockReportProducer { @Topic("${kafka.topics.rejected-orders}") void sendRejectedOrder(@KafkaKey String key, RejectedOrder rejectedOrder); + + @Topic("${kafka.topics.order-book-response}") + void sendOrderBookResponse( + @KafkaKey String key, + OrderBookRequest orderBookRequest + ); + + @Topic("${kafka.topics.order-book-rejected}") + void sendOrderBookRejected( + @KafkaKey String key, + OrderBookRequest orderBookRequest + ); } diff --git a/components/quickfix-server/src/test/resources/application.yml b/components/quickfix-server/src/test/resources/application.yml deleted file mode 100644 index 2616758e..00000000 --- a/components/quickfix-server/src/test/resources/application.yml +++ /dev/null @@ -1,5 +0,0 @@ -quickfix: - config: - executor_dynamic: executor_dynamic.cfg # Needed for the test because the bean is initialized before the config is loaded - version: 4.4 - data_dictionary: FIX44.xml diff --git a/components/trade-stream/build.gradle b/components/trade-stream/build.gradle index 856a5825..2cbd7e14 100644 --- a/components/trade-stream/build.gradle +++ b/components/trade-stream/build.gradle @@ -3,6 +3,10 @@ plugins { id "io.micronaut.application" } +ext { + configFiles = "classpath:application.yml,classpath:kafka.yml,classpath:redis.yml" +} + version = "${version}" group = "pfe_broker" @@ -18,17 +22,17 @@ dependencies { runtimeOnly group: 'org.yaml', name: 'snakeyaml', version: '2.2' implementation group: 'io.micronaut.kafka', name: 'micronaut-kafka-streams', version: '5.2.0' - implementation group: 'io.micronaut.redis', name: 'micronaut-redis-lettuce', version: '6.1.0' + implementation group: 'io.micronaut.redis', name: 'micronaut-redis-lettuce', version: '6.2.0' // Test dependencies testImplementation group: 'org.awaitility', name: 'awaitility', version: '4.2.0' - testImplementation group: 'org.assertj', name: 'assertj-core', version: '3.24.2' + testImplementation group: 'org.assertj', name: 'assertj-core', version: '3.25.1' testImplementation group: 'org.testcontainers', name: 'junit-jupiter', version: '1.19.3' // Log4J - implementation group: 'org.apache.logging.log4j', name: 'log4j-core', version: '2.22.0' - runtimeOnly group: 'org.apache.logging.log4j', name: 'log4j-api', version: '2.22.0' - runtimeOnly group: 'org.apache.logging.log4j', name: 'log4j-slf4j2-impl', version: '2.22.0' + implementation group: 'org.apache.logging.log4j', name: 'log4j-core', version: '2.22.1' + runtimeOnly group: 'org.apache.logging.log4j', name: 'log4j-api', version: '2.22.1' + runtimeOnly group: 'org.apache.logging.log4j', name: 'log4j-slf4j2-impl', version: '2.22.1' // Avro implementation group: 'io.confluent', name: 'kafka-streams-avro-serde', version: '7.5.1' @@ -70,3 +74,9 @@ test { testLogging.showStandardStreams = true testLogging.exceptionFormat = 'full' } + +[Test, JavaExec].each { targetType -> + tasks.withType(targetType) { task -> + task.systemProperty "micronaut.config.files", configFiles + } +} diff --git a/components/trade-stream/src/main/java/pfe_broker/trade_stream/Application.java b/components/trade-stream/src/main/java/pfe_broker/trade_stream/Application.java index 9f117c56..c93a62ac 100644 --- a/components/trade-stream/src/main/java/pfe_broker/trade_stream/Application.java +++ b/components/trade-stream/src/main/java/pfe_broker/trade_stream/Application.java @@ -5,9 +5,6 @@ import org.slf4j.LoggerFactory; public class Application { - static { - setProperties(); - } private static final Logger LOG = LoggerFactory.getLogger(Application.class); @@ -15,11 +12,4 @@ public static void main(String[] args) { LOG.info("Starting Trade Stream"); Micronaut.run(Application.class, args); } - - public static void setProperties() { - System.setProperty( - "micronaut.config.files", - "classpath:application.yml,classpath:kafka.yml,classpath:redis.yml" - ); - } } diff --git a/components/trade-stream/src/main/java/pfe_broker/trade_stream/BeanFactory.java b/components/trade-stream/src/main/java/pfe_broker/trade_stream/BeanFactory.java index da3af0e2..59f7d34c 100644 --- a/components/trade-stream/src/main/java/pfe_broker/trade_stream/BeanFactory.java +++ b/components/trade-stream/src/main/java/pfe_broker/trade_stream/BeanFactory.java @@ -7,6 +7,9 @@ import org.apache.kafka.clients.admin.AdminClient; import org.apache.kafka.clients.admin.CreateTopicsOptions; import org.apache.kafka.clients.admin.NewTopic; +import pfe_broker.avro.RejectedOrder; +import pfe_broker.avro.Trade; +import pfe_broker.avro.utils.SchemaRecord; @Requires(bean = AdminClient.class) @Factory @@ -40,4 +43,25 @@ NewTopic rejectedOrdersTopic( ) { return new NewTopic(topicName, 2, (short) 1); } + + @Bean + public SchemaRecord tradesSchema( + @Property(name = "kafka.topics.trades") String topicName + ) { + return new SchemaRecord(Trade.getClassSchema(), topicName); + } + + @Bean + public SchemaRecord acceptedTradessSchema( + @Property(name = "kafka.topics.accepted-trades") String topicName + ) { + return new SchemaRecord(Trade.getClassSchema(), topicName); + } + + @Bean + public SchemaRecord rejectedOrdersSchema( + @Property(name = "kafka.topics.rejected-orders") String topicName + ) { + return new SchemaRecord(RejectedOrder.getClassSchema(), topicName); + } } diff --git a/components/trade-stream/src/main/java/pfe_broker/trade_stream/TradeIntegrityCheckService.java b/components/trade-stream/src/main/java/pfe_broker/trade_stream/TradeIntegrityCheckService.java index b0799ab7..1833887d 100644 --- a/components/trade-stream/src/main/java/pfe_broker/trade_stream/TradeIntegrityCheckService.java +++ b/components/trade-stream/src/main/java/pfe_broker/trade_stream/TradeIntegrityCheckService.java @@ -11,6 +11,7 @@ import pfe_broker.avro.OrderRejectReason; import pfe_broker.avro.Side; import pfe_broker.avro.Trade; +import pfe_broker.avro.Type; import pfe_broker.common.UtilsRunning; public class TradeIntegrityCheckService { @@ -36,24 +37,49 @@ void init() { } } - private OrderRejectReason marketOrderCheckIntegrity(Trade trade) { - RedisCommands syncCommands = redisConnection.sync(); + private OrderRejectReason sellVerification( + Trade trade, + RedisCommands syncCommands + ) { + String username = trade.getOrder().getUsername().toString(); + Integer quantity = trade.getQuantity(); + Double price = trade.getPrice(); + Double amount = price * quantity; + String balanceKey = username + ":balance"; + + syncCommands.incrbyfloat(balanceKey, amount); + return null; + } + + private OrderRejectReason buyLimitVerification( + Trade trade, + RedisCommands syncCommands + ) { + String username = trade.getOrder().getUsername().toString(); + String symbol = trade.getSymbol().toString(); + Integer quantity = trade.getQuantity(); + + String stockKey = username + ":" + symbol; + + syncCommands.incrby(stockKey, quantity); + return null; + } + + private OrderRejectReason buyMarketVerification( + Trade trade, + RedisCommands syncCommands + ) { String username = trade.getOrder().getUsername().toString(); String symbol = trade.getSymbol().toString(); Integer quantity = trade.getQuantity(); Double price = trade.getPrice(); - Side side = trade.getOrder().getSide(); + Double amount = price * quantity; String stockKey = username + ":" + symbol; String balanceKey = username + ":balance"; - if (side == Side.SELL) { - syncCommands.incrbyfloat(balanceKey, amount); - return null; - } - syncCommands.watch(balanceKey); int countdown = 10; while (countdown-- > 0) { @@ -85,17 +111,44 @@ private OrderRejectReason marketOrderCheckIntegrity(Trade trade) { return OrderRejectReason.INCORRECT_QUANTITY; } + private OrderRejectReason marketOrderCheckIntegrity(Trade trade) { + RedisCommands syncCommands = redisConnection.sync(); + Side side = trade.getOrder().getSide(); + + if (side == Side.SELL) { + return sellVerification(trade, syncCommands); + } + + return buyMarketVerification(trade, syncCommands); + } + + private OrderRejectReason limitOrderCheckIntegrity(Trade trade) { + RedisCommands syncCommands = redisConnection.sync(); + Side side = trade.getOrder().getSide(); + + if (side == Side.SELL) { + return sellVerification(trade, syncCommands); + } + + return buyLimitVerification(trade, syncCommands); + } + public OrderRejectReason checkIntegrity(Trade trade) { LOG.debug("Checking integrity of trade {}", trade); + Type type = trade.getOrder().getType(); - OrderRejectReason marketOrderCheckIntegrityResult = - marketOrderCheckIntegrity(trade); + OrderRejectReason tradeCheckIntegrityResult = OrderRejectReason.OTHER; + if (type == Type.MARKET) { + tradeCheckIntegrityResult = marketOrderCheckIntegrity(trade); + } else if (type == Type.LIMIT) { + tradeCheckIntegrityResult = limitOrderCheckIntegrity(trade); + } - if (marketOrderCheckIntegrityResult == null) { + if (tradeCheckIntegrityResult == null) { LOG.debug("Trade {} accepted", trade); } - return marketOrderCheckIntegrityResult; + return tradeCheckIntegrityResult; } private boolean isRedisRunning() { diff --git a/components/trade-stream/src/test/java/pfe_broker/trade_stream/TradeStreamTest.java b/components/trade-stream/src/test/java/pfe_broker/trade_stream/TradeStreamTest.java index 12595d52..6cffa420 100644 --- a/components/trade-stream/src/test/java/pfe_broker/trade_stream/TradeStreamTest.java +++ b/components/trade-stream/src/test/java/pfe_broker/trade_stream/TradeStreamTest.java @@ -17,6 +17,7 @@ import pfe_broker.avro.Order; import pfe_broker.avro.Side; import pfe_broker.avro.Trade; +import pfe_broker.avro.Type; import pfe_broker.common.utils.KafkaTestContainer; import pfe_broker.common.utils.RedisTestContainer; import pfe_broker.trade_stream.mocks.MockListener; @@ -26,9 +27,6 @@ @Testcontainers(disabledWithoutDocker = true) @TestInstance(TestInstance.Lifecycle.PER_CLASS) public class TradeStreamTest implements TestPropertyProvider { - static { - Application.setProperties(); - } @Container static final KafkaTestContainer kafka = new KafkaTestContainer(); @@ -41,7 +39,6 @@ public class TradeStreamTest implements TestPropertyProvider { if (!kafka.isRunning()) { kafka.start(); } - kafka.registerTopics("trades", "accepted-trades", "rejected-orders"); if (!redis.isRunning()) { redis.start(); } @@ -72,7 +69,15 @@ void testTradeStreamBuyMarketOrder( StatefulRedisConnection redisConnection ) { // Given - Order order = new Order("user", "AAPL", 10, Side.BUY); + Order order = new Order( + "user", + "AAPL", + 10, + Side.BUY, + Type.MARKET, + null, + "1" + ); Trade trade = new Trade(order, "APPL", 100.0, 10); redisConnection.sync().set("user:balance", "10000"); @@ -98,7 +103,15 @@ void testTradeStreamSellMarketOrder( StatefulRedisConnection redisConnection ) { // Given - Order order = new Order("user", "AAPL", 10, Side.SELL); + Order order = new Order( + "user", + "AAPL", + 10, + Side.SELL, + Type.MARKET, + null, + "1" + ); Trade trade = new Trade(order, "APPL", 100.0, 10); redisConnection.sync().set("user:balance", "10000"); @@ -123,7 +136,15 @@ void testTradeStreamBuyMarketOrderInsufficientFunds( StatefulRedisConnection redisConnection ) { // Given - Order order = new Order("user", "AAPL", 10, Side.BUY); + Order order = new Order( + "user", + "AAPL", + 10, + Side.BUY, + Type.MARKET, + null, + "1" + ); Trade trade = new Trade(order, "APPL", 100.0, 10); redisConnection.sync().set("user:balance", "100"); diff --git a/config/avro/market-data.avsc b/config/avro/market-data.avsc index 3e1c69d6..d4191f63 100644 --- a/config/avro/market-data.avsc +++ b/config/avro/market-data.avsc @@ -5,19 +5,19 @@ "fields": [ { "name": "open", - "type": "float" + "type": "double" }, { "name": "high", - "type": "float" + "type": "double" }, { "name": "low", - "type": "float" + "type": "double" }, { "name": "close", - "type": "float" + "type": "double" }, { "name": "volume", diff --git a/config/avro/order-book-request-type.avsc b/config/avro/order-book-request-type.avsc new file mode 100644 index 00000000..ae67e033 --- /dev/null +++ b/config/avro/order-book-request-type.avsc @@ -0,0 +1,6 @@ +{ + "namespace": "pfe_broker.avro", + "type": "enum", + "name": "OrderBookRequestType", + "symbols": ["NEW", "REPLACE", "CANCEL"] +} diff --git a/config/avro/order-book-request.avsc b/config/avro/order-book-request.avsc new file mode 100644 index 00000000..97e94b63 --- /dev/null +++ b/config/avro/order-book-request.avsc @@ -0,0 +1,21 @@ +{ + "namespace": "pfe_broker.avro", + "type": "record", + "name": "OrderBookRequest", + "fields": [ + { + "name": "type", + "type": "OrderBookRequestType" + }, + { + "name": "order", + "type": "Order" + }, + { + "name": "origClOrderID", + "type": ["null", "string"], + "default": null, + "doc": "Only used for REPLACE and CANCEL orders" + } + ] +} diff --git a/config/avro/order.avsc b/config/avro/order.avsc index b62d11ff..b4b76128 100644 --- a/config/avro/order.avsc +++ b/config/avro/order.avsc @@ -18,6 +18,22 @@ { "name": "side", "type": "Side" // Reference to the Side schema + }, + { + "name": "type", + "type": "Type", // Reference to the Type schema + "default": "MARKET" + }, + { + "name": "price", + "type": ["null", "double"], + "default": null, + "doc": "Only used for LIMIT orders" + }, + { + "name": "clOrderID", + "type": "string", + "default": "" } ] } diff --git a/config/avro/type.avsc b/config/avro/type.avsc new file mode 100644 index 00000000..62845c36 --- /dev/null +++ b/config/avro/type.avsc @@ -0,0 +1,6 @@ +{ + "namespace": "pfe_broker.avro", + "type": "enum", + "name": "Type", + "symbols": ["MARKET", "LIMIT"] +} diff --git a/config/common/kafka.yml b/config/common/kafka.yml index e48d6e48..ae2c73ce 100644 --- a/config/common/kafka.yml +++ b/config/common/kafka.yml @@ -3,18 +3,22 @@ kafka: schema.registry.url: http://localhost:8081 common: symbol-topic-prefix: market-data. + market-data-thread-pool-size: 3 topics: trades: trades accepted-trades: accepted-trades orders: orders - accepted-orders: accepted-orders + accepted-orders: accepted-orders-market rejected-orders: rejected-orders + order-book-request: order-book-request + order-book-response: order-book-response + order-book-rejected: order-book-rejected producers: default: value.serializer: io.confluent.kafka.serializers.KafkaAvroSerializer key.serializer: org.apache.kafka.common.serialization.StringSerializer consumers: - default: + default: &default specific.avro.reader: true metadata.max.age.ms: 30000 # 30 seconds value.deserializer: io.confluent.kafka.serializers.KafkaAvroDeserializer @@ -22,6 +26,14 @@ kafka: fetch.min.bytes: 1 fetch.max.wait.ms: 10 allow.auto.create.topics: false + market-matcher-market-data: + <<: *default + fetch.max.wait.ms: 500 # 500ms + fetch.min.bytes: 20000 # 20kb ~ 150 entries + order-book-market-data: + <<: *default + fetch.max.wait.ms: 500 # 500ms + fetch.min.bytes: 20000 # 20kb ~ 150 entries streams: default: diff --git a/gradle.properties b/gradle.properties index ad35c751..e1c303c6 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,12 +1,12 @@ #Gradle properties javaVersion=17 -micronautVersion=4.0.2 +micronautVersion=4.2.1 shadowVersion=8.1.1 jnxplusGradlePluginVersion=0.2.2 lombokGradlePluginVersion=8.4 # Project properties -version=0.2.0 +version=0.3.0 org.gradle.parallel=true org.gradle.caching=true diff --git a/libs/avro/build.gradle b/libs/avro/build.gradle index 02364377..9d9587c8 100644 --- a/libs/avro/build.gradle +++ b/libs/avro/build.gradle @@ -11,10 +11,10 @@ repositories { } dependencies { - testImplementation 'org.junit.jupiter:junit-jupiter:5.9.1' + testImplementation 'org.junit.jupiter:junit-jupiter:5.10.1' // Avro - implementation group: 'org.apache.avro', name: 'avro', version: '1.11.1' - implementation group: 'org.apache.avro', name: 'avro-compiler', version: '1.11.1' + implementation group: 'org.apache.avro', name: 'avro', version: '1.11.3' + implementation group: 'org.apache.avro', name: 'avro-compiler', version: '1.11.3' // QuickfixJ implementation group: 'org.quickfixj', name: 'quickfixj-core', version: '2.3.1' implementation group: 'org.quickfixj', name: 'quickfixj-messages-all', version: '2.3.1' diff --git a/libs/avro/src/main/java/pfe_broker/avro/MarketData.java b/libs/avro/src/main/java/pfe_broker/avro/MarketData.java index 24802e08..3d1ebd9d 100644 --- a/libs/avro/src/main/java/pfe_broker/avro/MarketData.java +++ b/libs/avro/src/main/java/pfe_broker/avro/MarketData.java @@ -14,10 +14,10 @@ @org.apache.avro.specific.AvroGenerated public class MarketData extends org.apache.avro.specific.SpecificRecordBase implements org.apache.avro.specific.SpecificRecord { - private static final long serialVersionUID = -7647647996371877075L; + private static final long serialVersionUID = -276699413808386372L; - public static final org.apache.avro.Schema SCHEMA$ = new org.apache.avro.Schema.Parser().parse("{\"type\":\"record\",\"name\":\"MarketData\",\"namespace\":\"pfe_broker.avro\",\"fields\":[{\"name\":\"open\",\"type\":\"float\"},{\"name\":\"high\",\"type\":\"float\"},{\"name\":\"low\",\"type\":\"float\"},{\"name\":\"close\",\"type\":\"float\"},{\"name\":\"volume\",\"type\":\"int\"}]}"); + public static final org.apache.avro.Schema SCHEMA$ = new org.apache.avro.Schema.Parser().parse("{\"type\":\"record\",\"name\":\"MarketData\",\"namespace\":\"pfe_broker.avro\",\"fields\":[{\"name\":\"open\",\"type\":\"double\"},{\"name\":\"high\",\"type\":\"double\"},{\"name\":\"low\",\"type\":\"double\"},{\"name\":\"close\",\"type\":\"double\"},{\"name\":\"volume\",\"type\":\"int\"}]}"); public static org.apache.avro.Schema getClassSchema() { return SCHEMA$; } private static final SpecificData MODEL$ = new SpecificData(); @@ -73,10 +73,10 @@ public static MarketData fromByteBuffer( return DECODER.decode(b); } - private float open; - private float high; - private float low; - private float close; + private double open; + private double high; + private double low; + private double close; private int volume; /** @@ -94,7 +94,7 @@ public MarketData() {} * @param close The new value for close * @param volume The new value for volume */ - public MarketData(java.lang.Float open, java.lang.Float high, java.lang.Float low, java.lang.Float close, java.lang.Integer volume) { + public MarketData(java.lang.Double open, java.lang.Double high, java.lang.Double low, java.lang.Double close, java.lang.Integer volume) { this.open = open; this.high = high; this.low = low; @@ -126,10 +126,10 @@ public java.lang.Object get(int field$) { @SuppressWarnings(value="unchecked") public void put(int field$, java.lang.Object value$) { switch (field$) { - case 0: open = (java.lang.Float)value$; break; - case 1: high = (java.lang.Float)value$; break; - case 2: low = (java.lang.Float)value$; break; - case 3: close = (java.lang.Float)value$; break; + case 0: open = (java.lang.Double)value$; break; + case 1: high = (java.lang.Double)value$; break; + case 2: low = (java.lang.Double)value$; break; + case 3: close = (java.lang.Double)value$; break; case 4: volume = (java.lang.Integer)value$; break; default: throw new IndexOutOfBoundsException("Invalid index: " + field$); } @@ -139,7 +139,7 @@ public void put(int field$, java.lang.Object value$) { * Gets the value of the 'open' field. * @return The value of the 'open' field. */ - public float getOpen() { + public double getOpen() { return open; } @@ -148,7 +148,7 @@ public float getOpen() { * Sets the value of the 'open' field. * @param value the value to set. */ - public void setOpen(float value) { + public void setOpen(double value) { this.open = value; } @@ -156,7 +156,7 @@ public void setOpen(float value) { * Gets the value of the 'high' field. * @return The value of the 'high' field. */ - public float getHigh() { + public double getHigh() { return high; } @@ -165,7 +165,7 @@ public float getHigh() { * Sets the value of the 'high' field. * @param value the value to set. */ - public void setHigh(float value) { + public void setHigh(double value) { this.high = value; } @@ -173,7 +173,7 @@ public void setHigh(float value) { * Gets the value of the 'low' field. * @return The value of the 'low' field. */ - public float getLow() { + public double getLow() { return low; } @@ -182,7 +182,7 @@ public float getLow() { * Sets the value of the 'low' field. * @param value the value to set. */ - public void setLow(float value) { + public void setLow(double value) { this.low = value; } @@ -190,7 +190,7 @@ public void setLow(float value) { * Gets the value of the 'close' field. * @return The value of the 'close' field. */ - public float getClose() { + public double getClose() { return close; } @@ -199,7 +199,7 @@ public float getClose() { * Sets the value of the 'close' field. * @param value the value to set. */ - public void setClose(float value) { + public void setClose(double value) { this.close = value; } @@ -261,10 +261,10 @@ public static pfe_broker.avro.MarketData.Builder newBuilder(pfe_broker.avro.Mark public static class Builder extends org.apache.avro.specific.SpecificRecordBuilderBase implements org.apache.avro.data.RecordBuilder { - private float open; - private float high; - private float low; - private float close; + private double open; + private double high; + private double low; + private double close; private int volume; /** Creates a new Builder */ @@ -332,7 +332,7 @@ private Builder(pfe_broker.avro.MarketData other) { * Gets the value of the 'open' field. * @return The value. */ - public float getOpen() { + public double getOpen() { return open; } @@ -342,7 +342,7 @@ public float getOpen() { * @param value The value of 'open'. * @return This builder. */ - public pfe_broker.avro.MarketData.Builder setOpen(float value) { + public pfe_broker.avro.MarketData.Builder setOpen(double value) { validate(fields()[0], value); this.open = value; fieldSetFlags()[0] = true; @@ -371,7 +371,7 @@ public pfe_broker.avro.MarketData.Builder clearOpen() { * Gets the value of the 'high' field. * @return The value. */ - public float getHigh() { + public double getHigh() { return high; } @@ -381,7 +381,7 @@ public float getHigh() { * @param value The value of 'high'. * @return This builder. */ - public pfe_broker.avro.MarketData.Builder setHigh(float value) { + public pfe_broker.avro.MarketData.Builder setHigh(double value) { validate(fields()[1], value); this.high = value; fieldSetFlags()[1] = true; @@ -410,7 +410,7 @@ public pfe_broker.avro.MarketData.Builder clearHigh() { * Gets the value of the 'low' field. * @return The value. */ - public float getLow() { + public double getLow() { return low; } @@ -420,7 +420,7 @@ public float getLow() { * @param value The value of 'low'. * @return This builder. */ - public pfe_broker.avro.MarketData.Builder setLow(float value) { + public pfe_broker.avro.MarketData.Builder setLow(double value) { validate(fields()[2], value); this.low = value; fieldSetFlags()[2] = true; @@ -449,7 +449,7 @@ public pfe_broker.avro.MarketData.Builder clearLow() { * Gets the value of the 'close' field. * @return The value. */ - public float getClose() { + public double getClose() { return close; } @@ -459,7 +459,7 @@ public float getClose() { * @param value The value of 'close'. * @return This builder. */ - public pfe_broker.avro.MarketData.Builder setClose(float value) { + public pfe_broker.avro.MarketData.Builder setClose(double value) { validate(fields()[3], value); this.close = value; fieldSetFlags()[3] = true; @@ -528,10 +528,10 @@ public pfe_broker.avro.MarketData.Builder clearVolume() { public MarketData build() { try { MarketData record = new MarketData(); - record.open = fieldSetFlags()[0] ? this.open : (java.lang.Float) defaultValue(fields()[0]); - record.high = fieldSetFlags()[1] ? this.high : (java.lang.Float) defaultValue(fields()[1]); - record.low = fieldSetFlags()[2] ? this.low : (java.lang.Float) defaultValue(fields()[2]); - record.close = fieldSetFlags()[3] ? this.close : (java.lang.Float) defaultValue(fields()[3]); + record.open = fieldSetFlags()[0] ? this.open : (java.lang.Double) defaultValue(fields()[0]); + record.high = fieldSetFlags()[1] ? this.high : (java.lang.Double) defaultValue(fields()[1]); + record.low = fieldSetFlags()[2] ? this.low : (java.lang.Double) defaultValue(fields()[2]); + record.close = fieldSetFlags()[3] ? this.close : (java.lang.Double) defaultValue(fields()[3]); record.volume = fieldSetFlags()[4] ? this.volume : (java.lang.Integer) defaultValue(fields()[4]); return record; } catch (org.apache.avro.AvroMissingFieldException e) { @@ -565,13 +565,13 @@ public MarketData build() { @Override public void customEncode(org.apache.avro.io.Encoder out) throws java.io.IOException { - out.writeFloat(this.open); + out.writeDouble(this.open); - out.writeFloat(this.high); + out.writeDouble(this.high); - out.writeFloat(this.low); + out.writeDouble(this.low); - out.writeFloat(this.close); + out.writeDouble(this.close); out.writeInt(this.volume); @@ -582,13 +582,13 @@ public MarketData build() { { org.apache.avro.Schema.Field[] fieldOrder = in.readFieldOrderIfDiff(); if (fieldOrder == null) { - this.open = in.readFloat(); + this.open = in.readDouble(); - this.high = in.readFloat(); + this.high = in.readDouble(); - this.low = in.readFloat(); + this.low = in.readDouble(); - this.close = in.readFloat(); + this.close = in.readDouble(); this.volume = in.readInt(); @@ -596,19 +596,19 @@ public MarketData build() { for (int i = 0; i < 5; i++) { switch (fieldOrder[i].pos()) { case 0: - this.open = in.readFloat(); + this.open = in.readDouble(); break; case 1: - this.high = in.readFloat(); + this.high = in.readDouble(); break; case 2: - this.low = in.readFloat(); + this.low = in.readDouble(); break; case 3: - this.close = in.readFloat(); + this.close = in.readDouble(); break; case 4: diff --git a/libs/avro/src/main/java/pfe_broker/avro/Order.java b/libs/avro/src/main/java/pfe_broker/avro/Order.java index 1debaf1b..872972b1 100644 --- a/libs/avro/src/main/java/pfe_broker/avro/Order.java +++ b/libs/avro/src/main/java/pfe_broker/avro/Order.java @@ -14,10 +14,10 @@ @org.apache.avro.specific.AvroGenerated public class Order extends org.apache.avro.specific.SpecificRecordBase implements org.apache.avro.specific.SpecificRecord { - private static final long serialVersionUID = 5889493834219403263L; + private static final long serialVersionUID = -3668500449969664216L; - public static final org.apache.avro.Schema SCHEMA$ = new org.apache.avro.Schema.Parser().parse("{\"type\":\"record\",\"name\":\"Order\",\"namespace\":\"pfe_broker.avro\",\"fields\":[{\"name\":\"username\",\"type\":\"string\"},{\"name\":\"symbol\",\"type\":\"string\"},{\"name\":\"quantity\",\"type\":\"int\"},{\"name\":\"side\",\"type\":{\"type\":\"enum\",\"name\":\"Side\",\"symbols\":[\"BUY\",\"SELL\"]}}]}"); + public static final org.apache.avro.Schema SCHEMA$ = new org.apache.avro.Schema.Parser().parse("{\"type\":\"record\",\"name\":\"Order\",\"namespace\":\"pfe_broker.avro\",\"fields\":[{\"name\":\"username\",\"type\":\"string\"},{\"name\":\"symbol\",\"type\":\"string\"},{\"name\":\"quantity\",\"type\":\"int\"},{\"name\":\"side\",\"type\":{\"type\":\"enum\",\"name\":\"Side\",\"symbols\":[\"BUY\",\"SELL\"]}},{\"name\":\"type\",\"type\":{\"type\":\"enum\",\"name\":\"Type\",\"symbols\":[\"MARKET\",\"LIMIT\"]},\"default\":\"MARKET\"},{\"name\":\"price\",\"type\":[\"null\",\"double\"],\"doc\":\"Only used for LIMIT orders\",\"default\":null},{\"name\":\"clOrderID\",\"type\":\"string\",\"default\":\"\"}]}"); public static org.apache.avro.Schema getClassSchema() { return SCHEMA$; } private static final SpecificData MODEL$ = new SpecificData(); @@ -77,6 +77,10 @@ public static Order fromByteBuffer( private java.lang.CharSequence symbol; private int quantity; private pfe_broker.avro.Side side; + private pfe_broker.avro.Type type; + /** Only used for LIMIT orders */ + private java.lang.Double price; + private java.lang.CharSequence clOrderID; /** * Default constructor. Note that this does not initialize fields @@ -91,12 +95,18 @@ public Order() {} * @param symbol The new value for symbol * @param quantity The new value for quantity * @param side The new value for side + * @param type The new value for type + * @param price Only used for LIMIT orders + * @param clOrderID The new value for clOrderID */ - public Order(java.lang.CharSequence username, java.lang.CharSequence symbol, java.lang.Integer quantity, pfe_broker.avro.Side side) { + public Order(java.lang.CharSequence username, java.lang.CharSequence symbol, java.lang.Integer quantity, pfe_broker.avro.Side side, pfe_broker.avro.Type type, java.lang.Double price, java.lang.CharSequence clOrderID) { this.username = username; this.symbol = symbol; this.quantity = quantity; this.side = side; + this.type = type; + this.price = price; + this.clOrderID = clOrderID; } @Override @@ -113,6 +123,9 @@ public java.lang.Object get(int field$) { case 1: return symbol; case 2: return quantity; case 3: return side; + case 4: return type; + case 5: return price; + case 6: return clOrderID; default: throw new IndexOutOfBoundsException("Invalid index: " + field$); } } @@ -126,6 +139,9 @@ public void put(int field$, java.lang.Object value$) { case 1: symbol = (java.lang.CharSequence)value$; break; case 2: quantity = (java.lang.Integer)value$; break; case 3: side = (pfe_broker.avro.Side)value$; break; + case 4: type = (pfe_broker.avro.Type)value$; break; + case 5: price = (java.lang.Double)value$; break; + case 6: clOrderID = (java.lang.CharSequence)value$; break; default: throw new IndexOutOfBoundsException("Invalid index: " + field$); } } @@ -198,6 +214,58 @@ public void setSide(pfe_broker.avro.Side value) { this.side = value; } + /** + * Gets the value of the 'type' field. + * @return The value of the 'type' field. + */ + public pfe_broker.avro.Type getType() { + return type; + } + + + /** + * Sets the value of the 'type' field. + * @param value the value to set. + */ + public void setType(pfe_broker.avro.Type value) { + this.type = value; + } + + /** + * Gets the value of the 'price' field. + * @return Only used for LIMIT orders + */ + public java.lang.Double getPrice() { + return price; + } + + + /** + * Sets the value of the 'price' field. + * Only used for LIMIT orders + * @param value the value to set. + */ + public void setPrice(java.lang.Double value) { + this.price = value; + } + + /** + * Gets the value of the 'clOrderID' field. + * @return The value of the 'clOrderID' field. + */ + public java.lang.CharSequence getClOrderID() { + return clOrderID; + } + + + /** + * Sets the value of the 'clOrderID' field. + * @param value the value to set. + */ + public void setClOrderID(java.lang.CharSequence value) { + this.clOrderID = value; + } + /** * Creates a new Order RecordBuilder. * @return A new Order RecordBuilder @@ -243,6 +311,10 @@ public static class Builder extends org.apache.avro.specific.SpecificRecordBuild private java.lang.CharSequence symbol; private int quantity; private pfe_broker.avro.Side side; + private pfe_broker.avro.Type type; + /** Only used for LIMIT orders */ + private java.lang.Double price; + private java.lang.CharSequence clOrderID; /** Creates a new Builder */ private Builder() { @@ -271,6 +343,18 @@ private Builder(pfe_broker.avro.Order.Builder other) { this.side = data().deepCopy(fields()[3].schema(), other.side); fieldSetFlags()[3] = other.fieldSetFlags()[3]; } + if (isValidValue(fields()[4], other.type)) { + this.type = data().deepCopy(fields()[4].schema(), other.type); + fieldSetFlags()[4] = other.fieldSetFlags()[4]; + } + if (isValidValue(fields()[5], other.price)) { + this.price = data().deepCopy(fields()[5].schema(), other.price); + fieldSetFlags()[5] = other.fieldSetFlags()[5]; + } + if (isValidValue(fields()[6], other.clOrderID)) { + this.clOrderID = data().deepCopy(fields()[6].schema(), other.clOrderID); + fieldSetFlags()[6] = other.fieldSetFlags()[6]; + } } /** @@ -295,6 +379,18 @@ private Builder(pfe_broker.avro.Order other) { this.side = data().deepCopy(fields()[3].schema(), other.side); fieldSetFlags()[3] = true; } + if (isValidValue(fields()[4], other.type)) { + this.type = data().deepCopy(fields()[4].schema(), other.type); + fieldSetFlags()[4] = true; + } + if (isValidValue(fields()[5], other.price)) { + this.price = data().deepCopy(fields()[5].schema(), other.price); + fieldSetFlags()[5] = true; + } + if (isValidValue(fields()[6], other.clOrderID)) { + this.clOrderID = data().deepCopy(fields()[6].schema(), other.clOrderID); + fieldSetFlags()[6] = true; + } } /** @@ -456,6 +552,130 @@ public pfe_broker.avro.Order.Builder clearSide() { return this; } + /** + * Gets the value of the 'type' field. + * @return The value. + */ + public pfe_broker.avro.Type getType() { + return type; + } + + + /** + * Sets the value of the 'type' field. + * @param value The value of 'type'. + * @return This builder. + */ + public pfe_broker.avro.Order.Builder setType(pfe_broker.avro.Type value) { + validate(fields()[4], value); + this.type = value; + fieldSetFlags()[4] = true; + return this; + } + + /** + * Checks whether the 'type' field has been set. + * @return True if the 'type' field has been set, false otherwise. + */ + public boolean hasType() { + return fieldSetFlags()[4]; + } + + + /** + * Clears the value of the 'type' field. + * @return This builder. + */ + public pfe_broker.avro.Order.Builder clearType() { + type = null; + fieldSetFlags()[4] = false; + return this; + } + + /** + * Gets the value of the 'price' field. + * Only used for LIMIT orders + * @return The value. + */ + public java.lang.Double getPrice() { + return price; + } + + + /** + * Sets the value of the 'price' field. + * Only used for LIMIT orders + * @param value The value of 'price'. + * @return This builder. + */ + public pfe_broker.avro.Order.Builder setPrice(java.lang.Double value) { + validate(fields()[5], value); + this.price = value; + fieldSetFlags()[5] = true; + return this; + } + + /** + * Checks whether the 'price' field has been set. + * Only used for LIMIT orders + * @return True if the 'price' field has been set, false otherwise. + */ + public boolean hasPrice() { + return fieldSetFlags()[5]; + } + + + /** + * Clears the value of the 'price' field. + * Only used for LIMIT orders + * @return This builder. + */ + public pfe_broker.avro.Order.Builder clearPrice() { + price = null; + fieldSetFlags()[5] = false; + return this; + } + + /** + * Gets the value of the 'clOrderID' field. + * @return The value. + */ + public java.lang.CharSequence getClOrderID() { + return clOrderID; + } + + + /** + * Sets the value of the 'clOrderID' field. + * @param value The value of 'clOrderID'. + * @return This builder. + */ + public pfe_broker.avro.Order.Builder setClOrderID(java.lang.CharSequence value) { + validate(fields()[6], value); + this.clOrderID = value; + fieldSetFlags()[6] = true; + return this; + } + + /** + * Checks whether the 'clOrderID' field has been set. + * @return True if the 'clOrderID' field has been set, false otherwise. + */ + public boolean hasClOrderID() { + return fieldSetFlags()[6]; + } + + + /** + * Clears the value of the 'clOrderID' field. + * @return This builder. + */ + public pfe_broker.avro.Order.Builder clearClOrderID() { + clOrderID = null; + fieldSetFlags()[6] = false; + return this; + } + @Override @SuppressWarnings("unchecked") public Order build() { @@ -465,6 +685,9 @@ public Order build() { record.symbol = fieldSetFlags()[1] ? this.symbol : (java.lang.CharSequence) defaultValue(fields()[1]); record.quantity = fieldSetFlags()[2] ? this.quantity : (java.lang.Integer) defaultValue(fields()[2]); record.side = fieldSetFlags()[3] ? this.side : (pfe_broker.avro.Side) defaultValue(fields()[3]); + record.type = fieldSetFlags()[4] ? this.type : (pfe_broker.avro.Type) defaultValue(fields()[4]); + record.price = fieldSetFlags()[5] ? this.price : (java.lang.Double) defaultValue(fields()[5]); + record.clOrderID = fieldSetFlags()[6] ? this.clOrderID : (java.lang.CharSequence) defaultValue(fields()[6]); return record; } catch (org.apache.avro.AvroMissingFieldException e) { throw e; @@ -505,6 +728,18 @@ public Order build() { out.writeEnum(this.side.ordinal()); + out.writeEnum(this.type.ordinal()); + + if (this.price == null) { + out.writeIndex(0); + out.writeNull(); + } else { + out.writeIndex(1); + out.writeDouble(this.price); + } + + out.writeString(this.clOrderID); + } @Override public void customDecode(org.apache.avro.io.ResolvingDecoder in) @@ -520,8 +755,19 @@ public Order build() { this.side = pfe_broker.avro.Side.values()[in.readEnum()]; + this.type = pfe_broker.avro.Type.values()[in.readEnum()]; + + if (in.readIndex() != 1) { + in.readNull(); + this.price = null; + } else { + this.price = in.readDouble(); + } + + this.clOrderID = in.readString(this.clOrderID instanceof Utf8 ? (Utf8)this.clOrderID : null); + } else { - for (int i = 0; i < 4; i++) { + for (int i = 0; i < 7; i++) { switch (fieldOrder[i].pos()) { case 0: this.username = in.readString(this.username instanceof Utf8 ? (Utf8)this.username : null); @@ -539,6 +785,23 @@ public Order build() { this.side = pfe_broker.avro.Side.values()[in.readEnum()]; break; + case 4: + this.type = pfe_broker.avro.Type.values()[in.readEnum()]; + break; + + case 5: + if (in.readIndex() != 1) { + in.readNull(); + this.price = null; + } else { + this.price = in.readDouble(); + } + break; + + case 6: + this.clOrderID = in.readString(this.clOrderID instanceof Utf8 ? (Utf8)this.clOrderID : null); + break; + default: throw new java.io.IOException("Corrupt ResolvingDecoder."); } diff --git a/libs/avro/src/main/java/pfe_broker/avro/OrderBookRequest.java b/libs/avro/src/main/java/pfe_broker/avro/OrderBookRequest.java new file mode 100644 index 00000000..5bf65527 --- /dev/null +++ b/libs/avro/src/main/java/pfe_broker/avro/OrderBookRequest.java @@ -0,0 +1,558 @@ +/** + * Autogenerated by Avro + * + * DO NOT EDIT DIRECTLY + */ +package pfe_broker.avro; + +import org.apache.avro.generic.GenericArray; +import org.apache.avro.specific.SpecificData; +import org.apache.avro.util.Utf8; +import org.apache.avro.message.BinaryMessageEncoder; +import org.apache.avro.message.BinaryMessageDecoder; +import org.apache.avro.message.SchemaStore; + +@org.apache.avro.specific.AvroGenerated +public class OrderBookRequest extends org.apache.avro.specific.SpecificRecordBase implements org.apache.avro.specific.SpecificRecord { + private static final long serialVersionUID = 5753349618358525505L; + + + public static final org.apache.avro.Schema SCHEMA$ = new org.apache.avro.Schema.Parser().parse("{\"type\":\"record\",\"name\":\"OrderBookRequest\",\"namespace\":\"pfe_broker.avro\",\"fields\":[{\"name\":\"type\",\"type\":{\"type\":\"enum\",\"name\":\"OrderBookRequestType\",\"symbols\":[\"NEW\",\"REPLACE\",\"CANCEL\"]}},{\"name\":\"order\",\"type\":{\"type\":\"record\",\"name\":\"Order\",\"fields\":[{\"name\":\"username\",\"type\":\"string\"},{\"name\":\"symbol\",\"type\":\"string\"},{\"name\":\"quantity\",\"type\":\"int\"},{\"name\":\"side\",\"type\":{\"type\":\"enum\",\"name\":\"Side\",\"symbols\":[\"BUY\",\"SELL\"]}},{\"name\":\"type\",\"type\":{\"type\":\"enum\",\"name\":\"Type\",\"symbols\":[\"MARKET\",\"LIMIT\"]},\"default\":\"MARKET\"},{\"name\":\"price\",\"type\":[\"null\",\"double\"],\"doc\":\"Only used for LIMIT orders\",\"default\":null},{\"name\":\"clOrderID\",\"type\":\"string\",\"default\":\"\"}]}},{\"name\":\"origClOrderID\",\"type\":[\"null\",\"string\"],\"doc\":\"Only used for REPLACE and CANCEL orders\",\"default\":null}]}"); + public static org.apache.avro.Schema getClassSchema() { return SCHEMA$; } + + private static final SpecificData MODEL$ = new SpecificData(); + + private static final BinaryMessageEncoder ENCODER = + new BinaryMessageEncoder<>(MODEL$, SCHEMA$); + + private static final BinaryMessageDecoder DECODER = + new BinaryMessageDecoder<>(MODEL$, SCHEMA$); + + /** + * Return the BinaryMessageEncoder instance used by this class. + * @return the message encoder used by this class + */ + public static BinaryMessageEncoder getEncoder() { + return ENCODER; + } + + /** + * Return the BinaryMessageDecoder instance used by this class. + * @return the message decoder used by this class + */ + public static BinaryMessageDecoder getDecoder() { + return DECODER; + } + + /** + * Create a new BinaryMessageDecoder instance for this class that uses the specified {@link SchemaStore}. + * @param resolver a {@link SchemaStore} used to find schemas by fingerprint + * @return a BinaryMessageDecoder instance for this class backed by the given SchemaStore + */ + public static BinaryMessageDecoder createDecoder(SchemaStore resolver) { + return new BinaryMessageDecoder<>(MODEL$, SCHEMA$, resolver); + } + + /** + * Serializes this OrderBookRequest to a ByteBuffer. + * @return a buffer holding the serialized data for this instance + * @throws java.io.IOException if this instance could not be serialized + */ + public java.nio.ByteBuffer toByteBuffer() throws java.io.IOException { + return ENCODER.encode(this); + } + + /** + * Deserializes a OrderBookRequest from a ByteBuffer. + * @param b a byte buffer holding serialized data for an instance of this class + * @return a OrderBookRequest instance decoded from the given buffer + * @throws java.io.IOException if the given bytes could not be deserialized into an instance of this class + */ + public static OrderBookRequest fromByteBuffer( + java.nio.ByteBuffer b) throws java.io.IOException { + return DECODER.decode(b); + } + + private pfe_broker.avro.OrderBookRequestType type; + private pfe_broker.avro.Order order; + /** Only used for REPLACE and CANCEL orders */ + private java.lang.CharSequence origClOrderID; + + /** + * Default constructor. Note that this does not initialize fields + * to their default values from the schema. If that is desired then + * one should use newBuilder(). + */ + public OrderBookRequest() {} + + /** + * All-args constructor. + * @param type The new value for type + * @param order The new value for order + * @param origClOrderID Only used for REPLACE and CANCEL orders + */ + public OrderBookRequest(pfe_broker.avro.OrderBookRequestType type, pfe_broker.avro.Order order, java.lang.CharSequence origClOrderID) { + this.type = type; + this.order = order; + this.origClOrderID = origClOrderID; + } + + @Override + public org.apache.avro.specific.SpecificData getSpecificData() { return MODEL$; } + + @Override + public org.apache.avro.Schema getSchema() { return SCHEMA$; } + + // Used by DatumWriter. Applications should not call. + @Override + public java.lang.Object get(int field$) { + switch (field$) { + case 0: return type; + case 1: return order; + case 2: return origClOrderID; + default: throw new IndexOutOfBoundsException("Invalid index: " + field$); + } + } + + // Used by DatumReader. Applications should not call. + @Override + @SuppressWarnings(value="unchecked") + public void put(int field$, java.lang.Object value$) { + switch (field$) { + case 0: type = (pfe_broker.avro.OrderBookRequestType)value$; break; + case 1: order = (pfe_broker.avro.Order)value$; break; + case 2: origClOrderID = (java.lang.CharSequence)value$; break; + default: throw new IndexOutOfBoundsException("Invalid index: " + field$); + } + } + + /** + * Gets the value of the 'type' field. + * @return The value of the 'type' field. + */ + public pfe_broker.avro.OrderBookRequestType getType() { + return type; + } + + + /** + * Sets the value of the 'type' field. + * @param value the value to set. + */ + public void setType(pfe_broker.avro.OrderBookRequestType value) { + this.type = value; + } + + /** + * Gets the value of the 'order' field. + * @return The value of the 'order' field. + */ + public pfe_broker.avro.Order getOrder() { + return order; + } + + + /** + * Sets the value of the 'order' field. + * @param value the value to set. + */ + public void setOrder(pfe_broker.avro.Order value) { + this.order = value; + } + + /** + * Gets the value of the 'origClOrderID' field. + * @return Only used for REPLACE and CANCEL orders + */ + public java.lang.CharSequence getOrigClOrderID() { + return origClOrderID; + } + + + /** + * Sets the value of the 'origClOrderID' field. + * Only used for REPLACE and CANCEL orders + * @param value the value to set. + */ + public void setOrigClOrderID(java.lang.CharSequence value) { + this.origClOrderID = value; + } + + /** + * Creates a new OrderBookRequest RecordBuilder. + * @return A new OrderBookRequest RecordBuilder + */ + public static pfe_broker.avro.OrderBookRequest.Builder newBuilder() { + return new pfe_broker.avro.OrderBookRequest.Builder(); + } + + /** + * Creates a new OrderBookRequest RecordBuilder by copying an existing Builder. + * @param other The existing builder to copy. + * @return A new OrderBookRequest RecordBuilder + */ + public static pfe_broker.avro.OrderBookRequest.Builder newBuilder(pfe_broker.avro.OrderBookRequest.Builder other) { + if (other == null) { + return new pfe_broker.avro.OrderBookRequest.Builder(); + } else { + return new pfe_broker.avro.OrderBookRequest.Builder(other); + } + } + + /** + * Creates a new OrderBookRequest RecordBuilder by copying an existing OrderBookRequest instance. + * @param other The existing instance to copy. + * @return A new OrderBookRequest RecordBuilder + */ + public static pfe_broker.avro.OrderBookRequest.Builder newBuilder(pfe_broker.avro.OrderBookRequest other) { + if (other == null) { + return new pfe_broker.avro.OrderBookRequest.Builder(); + } else { + return new pfe_broker.avro.OrderBookRequest.Builder(other); + } + } + + /** + * RecordBuilder for OrderBookRequest instances. + */ + @org.apache.avro.specific.AvroGenerated + public static class Builder extends org.apache.avro.specific.SpecificRecordBuilderBase + implements org.apache.avro.data.RecordBuilder { + + private pfe_broker.avro.OrderBookRequestType type; + private pfe_broker.avro.Order order; + private pfe_broker.avro.Order.Builder orderBuilder; + /** Only used for REPLACE and CANCEL orders */ + private java.lang.CharSequence origClOrderID; + + /** Creates a new Builder */ + private Builder() { + super(SCHEMA$, MODEL$); + } + + /** + * Creates a Builder by copying an existing Builder. + * @param other The existing Builder to copy. + */ + private Builder(pfe_broker.avro.OrderBookRequest.Builder other) { + super(other); + if (isValidValue(fields()[0], other.type)) { + this.type = data().deepCopy(fields()[0].schema(), other.type); + fieldSetFlags()[0] = other.fieldSetFlags()[0]; + } + if (isValidValue(fields()[1], other.order)) { + this.order = data().deepCopy(fields()[1].schema(), other.order); + fieldSetFlags()[1] = other.fieldSetFlags()[1]; + } + if (other.hasOrderBuilder()) { + this.orderBuilder = pfe_broker.avro.Order.newBuilder(other.getOrderBuilder()); + } + if (isValidValue(fields()[2], other.origClOrderID)) { + this.origClOrderID = data().deepCopy(fields()[2].schema(), other.origClOrderID); + fieldSetFlags()[2] = other.fieldSetFlags()[2]; + } + } + + /** + * Creates a Builder by copying an existing OrderBookRequest instance + * @param other The existing instance to copy. + */ + private Builder(pfe_broker.avro.OrderBookRequest other) { + super(SCHEMA$, MODEL$); + if (isValidValue(fields()[0], other.type)) { + this.type = data().deepCopy(fields()[0].schema(), other.type); + fieldSetFlags()[0] = true; + } + if (isValidValue(fields()[1], other.order)) { + this.order = data().deepCopy(fields()[1].schema(), other.order); + fieldSetFlags()[1] = true; + } + this.orderBuilder = null; + if (isValidValue(fields()[2], other.origClOrderID)) { + this.origClOrderID = data().deepCopy(fields()[2].schema(), other.origClOrderID); + fieldSetFlags()[2] = true; + } + } + + /** + * Gets the value of the 'type' field. + * @return The value. + */ + public pfe_broker.avro.OrderBookRequestType getType() { + return type; + } + + + /** + * Sets the value of the 'type' field. + * @param value The value of 'type'. + * @return This builder. + */ + public pfe_broker.avro.OrderBookRequest.Builder setType(pfe_broker.avro.OrderBookRequestType value) { + validate(fields()[0], value); + this.type = value; + fieldSetFlags()[0] = true; + return this; + } + + /** + * Checks whether the 'type' field has been set. + * @return True if the 'type' field has been set, false otherwise. + */ + public boolean hasType() { + return fieldSetFlags()[0]; + } + + + /** + * Clears the value of the 'type' field. + * @return This builder. + */ + public pfe_broker.avro.OrderBookRequest.Builder clearType() { + type = null; + fieldSetFlags()[0] = false; + return this; + } + + /** + * Gets the value of the 'order' field. + * @return The value. + */ + public pfe_broker.avro.Order getOrder() { + return order; + } + + + /** + * Sets the value of the 'order' field. + * @param value The value of 'order'. + * @return This builder. + */ + public pfe_broker.avro.OrderBookRequest.Builder setOrder(pfe_broker.avro.Order value) { + validate(fields()[1], value); + this.orderBuilder = null; + this.order = value; + fieldSetFlags()[1] = true; + return this; + } + + /** + * Checks whether the 'order' field has been set. + * @return True if the 'order' field has been set, false otherwise. + */ + public boolean hasOrder() { + return fieldSetFlags()[1]; + } + + /** + * Gets the Builder instance for the 'order' field and creates one if it doesn't exist yet. + * @return This builder. + */ + public pfe_broker.avro.Order.Builder getOrderBuilder() { + if (orderBuilder == null) { + if (hasOrder()) { + setOrderBuilder(pfe_broker.avro.Order.newBuilder(order)); + } else { + setOrderBuilder(pfe_broker.avro.Order.newBuilder()); + } + } + return orderBuilder; + } + + /** + * Sets the Builder instance for the 'order' field + * @param value The builder instance that must be set. + * @return This builder. + */ + + public pfe_broker.avro.OrderBookRequest.Builder setOrderBuilder(pfe_broker.avro.Order.Builder value) { + clearOrder(); + orderBuilder = value; + return this; + } + + /** + * Checks whether the 'order' field has an active Builder instance + * @return True if the 'order' field has an active Builder instance + */ + public boolean hasOrderBuilder() { + return orderBuilder != null; + } + + /** + * Clears the value of the 'order' field. + * @return This builder. + */ + public pfe_broker.avro.OrderBookRequest.Builder clearOrder() { + order = null; + orderBuilder = null; + fieldSetFlags()[1] = false; + return this; + } + + /** + * Gets the value of the 'origClOrderID' field. + * Only used for REPLACE and CANCEL orders + * @return The value. + */ + public java.lang.CharSequence getOrigClOrderID() { + return origClOrderID; + } + + + /** + * Sets the value of the 'origClOrderID' field. + * Only used for REPLACE and CANCEL orders + * @param value The value of 'origClOrderID'. + * @return This builder. + */ + public pfe_broker.avro.OrderBookRequest.Builder setOrigClOrderID(java.lang.CharSequence value) { + validate(fields()[2], value); + this.origClOrderID = value; + fieldSetFlags()[2] = true; + return this; + } + + /** + * Checks whether the 'origClOrderID' field has been set. + * Only used for REPLACE and CANCEL orders + * @return True if the 'origClOrderID' field has been set, false otherwise. + */ + public boolean hasOrigClOrderID() { + return fieldSetFlags()[2]; + } + + + /** + * Clears the value of the 'origClOrderID' field. + * Only used for REPLACE and CANCEL orders + * @return This builder. + */ + public pfe_broker.avro.OrderBookRequest.Builder clearOrigClOrderID() { + origClOrderID = null; + fieldSetFlags()[2] = false; + return this; + } + + @Override + @SuppressWarnings("unchecked") + public OrderBookRequest build() { + try { + OrderBookRequest record = new OrderBookRequest(); + record.type = fieldSetFlags()[0] ? this.type : (pfe_broker.avro.OrderBookRequestType) defaultValue(fields()[0]); + if (orderBuilder != null) { + try { + record.order = this.orderBuilder.build(); + } catch (org.apache.avro.AvroMissingFieldException e) { + e.addParentField(record.getSchema().getField("order")); + throw e; + } + } else { + record.order = fieldSetFlags()[1] ? this.order : (pfe_broker.avro.Order) defaultValue(fields()[1]); + } + record.origClOrderID = fieldSetFlags()[2] ? this.origClOrderID : (java.lang.CharSequence) defaultValue(fields()[2]); + return record; + } catch (org.apache.avro.AvroMissingFieldException e) { + throw e; + } catch (java.lang.Exception e) { + throw new org.apache.avro.AvroRuntimeException(e); + } + } + } + + @SuppressWarnings("unchecked") + private static final org.apache.avro.io.DatumWriter + WRITER$ = (org.apache.avro.io.DatumWriter)MODEL$.createDatumWriter(SCHEMA$); + + @Override public void writeExternal(java.io.ObjectOutput out) + throws java.io.IOException { + WRITER$.write(this, SpecificData.getEncoder(out)); + } + + @SuppressWarnings("unchecked") + private static final org.apache.avro.io.DatumReader + READER$ = (org.apache.avro.io.DatumReader)MODEL$.createDatumReader(SCHEMA$); + + @Override public void readExternal(java.io.ObjectInput in) + throws java.io.IOException { + READER$.read(this, SpecificData.getDecoder(in)); + } + + @Override protected boolean hasCustomCoders() { return true; } + + @Override public void customEncode(org.apache.avro.io.Encoder out) + throws java.io.IOException + { + out.writeEnum(this.type.ordinal()); + + this.order.customEncode(out); + + if (this.origClOrderID == null) { + out.writeIndex(0); + out.writeNull(); + } else { + out.writeIndex(1); + out.writeString(this.origClOrderID); + } + + } + + @Override public void customDecode(org.apache.avro.io.ResolvingDecoder in) + throws java.io.IOException + { + org.apache.avro.Schema.Field[] fieldOrder = in.readFieldOrderIfDiff(); + if (fieldOrder == null) { + this.type = pfe_broker.avro.OrderBookRequestType.values()[in.readEnum()]; + + if (this.order == null) { + this.order = new pfe_broker.avro.Order(); + } + this.order.customDecode(in); + + if (in.readIndex() != 1) { + in.readNull(); + this.origClOrderID = null; + } else { + this.origClOrderID = in.readString(this.origClOrderID instanceof Utf8 ? (Utf8)this.origClOrderID : null); + } + + } else { + for (int i = 0; i < 3; i++) { + switch (fieldOrder[i].pos()) { + case 0: + this.type = pfe_broker.avro.OrderBookRequestType.values()[in.readEnum()]; + break; + + case 1: + if (this.order == null) { + this.order = new pfe_broker.avro.Order(); + } + this.order.customDecode(in); + break; + + case 2: + if (in.readIndex() != 1) { + in.readNull(); + this.origClOrderID = null; + } else { + this.origClOrderID = in.readString(this.origClOrderID instanceof Utf8 ? (Utf8)this.origClOrderID : null); + } + break; + + default: + throw new java.io.IOException("Corrupt ResolvingDecoder."); + } + } + } + } +} + + + + + + + + + + diff --git a/libs/avro/src/main/java/pfe_broker/avro/OrderBookRequestType.java b/libs/avro/src/main/java/pfe_broker/avro/OrderBookRequestType.java new file mode 100644 index 00000000..fe924db8 --- /dev/null +++ b/libs/avro/src/main/java/pfe_broker/avro/OrderBookRequestType.java @@ -0,0 +1,15 @@ +/** + * Autogenerated by Avro + * + * DO NOT EDIT DIRECTLY + */ +package pfe_broker.avro; +@org.apache.avro.specific.AvroGenerated +public enum OrderBookRequestType implements org.apache.avro.generic.GenericEnumSymbol { + NEW, REPLACE, CANCEL ; + public static final org.apache.avro.Schema SCHEMA$ = new org.apache.avro.Schema.Parser().parse("{\"type\":\"enum\",\"name\":\"OrderBookRequestType\",\"namespace\":\"pfe_broker.avro\",\"symbols\":[\"NEW\",\"REPLACE\",\"CANCEL\"]}"); + public static org.apache.avro.Schema getClassSchema() { return SCHEMA$; } + + @Override + public org.apache.avro.Schema getSchema() { return SCHEMA$; } +} diff --git a/libs/avro/src/main/java/pfe_broker/avro/RejectedOrder.java b/libs/avro/src/main/java/pfe_broker/avro/RejectedOrder.java index b29f1817..7cf76c8f 100644 --- a/libs/avro/src/main/java/pfe_broker/avro/RejectedOrder.java +++ b/libs/avro/src/main/java/pfe_broker/avro/RejectedOrder.java @@ -14,10 +14,10 @@ @org.apache.avro.specific.AvroGenerated public class RejectedOrder extends org.apache.avro.specific.SpecificRecordBase implements org.apache.avro.specific.SpecificRecord { - private static final long serialVersionUID = -1963098368973760489L; + private static final long serialVersionUID = 484513667864573901L; - public static final org.apache.avro.Schema SCHEMA$ = new org.apache.avro.Schema.Parser().parse("{\"type\":\"record\",\"name\":\"RejectedOrder\",\"namespace\":\"pfe_broker.avro\",\"fields\":[{\"name\":\"order\",\"type\":{\"type\":\"record\",\"name\":\"Order\",\"fields\":[{\"name\":\"username\",\"type\":\"string\"},{\"name\":\"symbol\",\"type\":\"string\"},{\"name\":\"quantity\",\"type\":\"int\"},{\"name\":\"side\",\"type\":{\"type\":\"enum\",\"name\":\"Side\",\"symbols\":[\"BUY\",\"SELL\"]}}]}},{\"name\":\"reason\",\"type\":{\"type\":\"enum\",\"name\":\"OrderRejectReason\",\"symbols\":[\"BROKER_EXCHANGE_OPTION\",\"UNKNOWN_SYMBOL\",\"EXCHANGE_CLOSED\",\"ORDER_EXCEEDS_LIMIT\",\"TOO_LATE_TO_ENTER\",\"UNKNOWN_ORDER\",\"DUPLICATE_ORDER\",\"STALE_ORDER\",\"INCORRECT_QUANTITY\",\"UNKNOWN_ACCOUNT\",\"PRICE_EXCEEDS_CURRENT_PRICE_BAND\",\"OTHER\"]}}]}"); + public static final org.apache.avro.Schema SCHEMA$ = new org.apache.avro.Schema.Parser().parse("{\"type\":\"record\",\"name\":\"RejectedOrder\",\"namespace\":\"pfe_broker.avro\",\"fields\":[{\"name\":\"order\",\"type\":{\"type\":\"record\",\"name\":\"Order\",\"fields\":[{\"name\":\"username\",\"type\":\"string\"},{\"name\":\"symbol\",\"type\":\"string\"},{\"name\":\"quantity\",\"type\":\"int\"},{\"name\":\"side\",\"type\":{\"type\":\"enum\",\"name\":\"Side\",\"symbols\":[\"BUY\",\"SELL\"]}},{\"name\":\"type\",\"type\":{\"type\":\"enum\",\"name\":\"Type\",\"symbols\":[\"MARKET\",\"LIMIT\"]},\"default\":\"MARKET\"},{\"name\":\"price\",\"type\":[\"null\",\"double\"],\"doc\":\"Only used for LIMIT orders\",\"default\":null},{\"name\":\"clOrderID\",\"type\":\"string\",\"default\":\"\"}]}},{\"name\":\"reason\",\"type\":{\"type\":\"enum\",\"name\":\"OrderRejectReason\",\"symbols\":[\"BROKER_EXCHANGE_OPTION\",\"UNKNOWN_SYMBOL\",\"EXCHANGE_CLOSED\",\"ORDER_EXCEEDS_LIMIT\",\"TOO_LATE_TO_ENTER\",\"UNKNOWN_ORDER\",\"DUPLICATE_ORDER\",\"STALE_ORDER\",\"INCORRECT_QUANTITY\",\"UNKNOWN_ACCOUNT\",\"PRICE_EXCEEDS_CURRENT_PRICE_BAND\",\"OTHER\"]}}]}"); public static org.apache.avro.Schema getClassSchema() { return SCHEMA$; } private static final SpecificData MODEL$ = new SpecificData(); diff --git a/libs/avro/src/main/java/pfe_broker/avro/Trade.java b/libs/avro/src/main/java/pfe_broker/avro/Trade.java index 5030cf85..3b63ec0f 100644 --- a/libs/avro/src/main/java/pfe_broker/avro/Trade.java +++ b/libs/avro/src/main/java/pfe_broker/avro/Trade.java @@ -14,10 +14,10 @@ @org.apache.avro.specific.AvroGenerated public class Trade extends org.apache.avro.specific.SpecificRecordBase implements org.apache.avro.specific.SpecificRecord { - private static final long serialVersionUID = -5736402585144105847L; + private static final long serialVersionUID = 7665842856207240417L; - public static final org.apache.avro.Schema SCHEMA$ = new org.apache.avro.Schema.Parser().parse("{\"type\":\"record\",\"name\":\"Trade\",\"namespace\":\"pfe_broker.avro\",\"fields\":[{\"name\":\"order\",\"type\":{\"type\":\"record\",\"name\":\"Order\",\"fields\":[{\"name\":\"username\",\"type\":\"string\"},{\"name\":\"symbol\",\"type\":\"string\"},{\"name\":\"quantity\",\"type\":\"int\"},{\"name\":\"side\",\"type\":{\"type\":\"enum\",\"name\":\"Side\",\"symbols\":[\"BUY\",\"SELL\"]}}]}},{\"name\":\"symbol\",\"type\":\"string\"},{\"name\":\"price\",\"type\":\"double\"},{\"name\":\"quantity\",\"type\":\"int\"}]}"); + public static final org.apache.avro.Schema SCHEMA$ = new org.apache.avro.Schema.Parser().parse("{\"type\":\"record\",\"name\":\"Trade\",\"namespace\":\"pfe_broker.avro\",\"fields\":[{\"name\":\"order\",\"type\":{\"type\":\"record\",\"name\":\"Order\",\"fields\":[{\"name\":\"username\",\"type\":\"string\"},{\"name\":\"symbol\",\"type\":\"string\"},{\"name\":\"quantity\",\"type\":\"int\"},{\"name\":\"side\",\"type\":{\"type\":\"enum\",\"name\":\"Side\",\"symbols\":[\"BUY\",\"SELL\"]}},{\"name\":\"type\",\"type\":{\"type\":\"enum\",\"name\":\"Type\",\"symbols\":[\"MARKET\",\"LIMIT\"]},\"default\":\"MARKET\"},{\"name\":\"price\",\"type\":[\"null\",\"double\"],\"doc\":\"Only used for LIMIT orders\",\"default\":null},{\"name\":\"clOrderID\",\"type\":\"string\",\"default\":\"\"}]}},{\"name\":\"symbol\",\"type\":\"string\"},{\"name\":\"price\",\"type\":\"double\"},{\"name\":\"quantity\",\"type\":\"int\"}]}"); public static org.apache.avro.Schema getClassSchema() { return SCHEMA$; } private static final SpecificData MODEL$ = new SpecificData(); diff --git a/libs/avro/src/main/java/pfe_broker/avro/Type.java b/libs/avro/src/main/java/pfe_broker/avro/Type.java new file mode 100644 index 00000000..829e7214 --- /dev/null +++ b/libs/avro/src/main/java/pfe_broker/avro/Type.java @@ -0,0 +1,15 @@ +/** + * Autogenerated by Avro + * + * DO NOT EDIT DIRECTLY + */ +package pfe_broker.avro; +@org.apache.avro.specific.AvroGenerated +public enum Type implements org.apache.avro.generic.GenericEnumSymbol { + MARKET, LIMIT ; + public static final org.apache.avro.Schema SCHEMA$ = new org.apache.avro.Schema.Parser().parse("{\"type\":\"enum\",\"name\":\"Type\",\"namespace\":\"pfe_broker.avro\",\"symbols\":[\"MARKET\",\"LIMIT\"]}"); + public static org.apache.avro.Schema getClassSchema() { return SCHEMA$; } + + @Override + public org.apache.avro.Schema getSchema() { return SCHEMA$; } +} diff --git a/libs/avro/src/main/java/pfe_broker/avro/utils/Converters.java b/libs/avro/src/main/java/pfe_broker/avro/utils/Converters.java index ef6e6040..8fecf512 100644 --- a/libs/avro/src/main/java/pfe_broker/avro/utils/Converters.java +++ b/libs/avro/src/main/java/pfe_broker/avro/utils/Converters.java @@ -44,27 +44,32 @@ public static class OrderRejectReason { private static final Map quickfixReasonMap = new HashMap<>(); static { - avroReasonMap.put(quickfix.field.OrdRejReason.BROKER_EXCHANGE_OPTION, pfe_broker.avro.OrderRejectReason.BROKER_EXCHANGE_OPTION); + avroReasonMap.put(quickfix.field.OrdRejReason.BROKER_EXCHANGE_OPTION, + pfe_broker.avro.OrderRejectReason.BROKER_EXCHANGE_OPTION); avroReasonMap.put(quickfix.field.OrdRejReason.UNKNOWN_SYMBOL, pfe_broker.avro.OrderRejectReason.UNKNOWN_SYMBOL); avroReasonMap.put(quickfix.field.OrdRejReason.EXCHANGE_CLOSED, pfe_broker.avro.OrderRejectReason.EXCHANGE_CLOSED); - avroReasonMap.put(quickfix.field.OrdRejReason.ORDER_EXCEEDS_LIMIT, pfe_broker.avro.OrderRejectReason.ORDER_EXCEEDS_LIMIT); - avroReasonMap.put(quickfix.field.OrdRejReason.TOO_LATE_TO_ENTER, pfe_broker.avro.OrderRejectReason.TOO_LATE_TO_ENTER); + avroReasonMap.put(quickfix.field.OrdRejReason.ORDER_EXCEEDS_LIMIT, + pfe_broker.avro.OrderRejectReason.ORDER_EXCEEDS_LIMIT); + avroReasonMap.put(quickfix.field.OrdRejReason.TOO_LATE_TO_ENTER, + pfe_broker.avro.OrderRejectReason.TOO_LATE_TO_ENTER); avroReasonMap.put(quickfix.field.OrdRejReason.UNKNOWN_ORDER, pfe_broker.avro.OrderRejectReason.UNKNOWN_ORDER); avroReasonMap.put(quickfix.field.OrdRejReason.DUPLICATE_ORDER, pfe_broker.avro.OrderRejectReason.DUPLICATE_ORDER); avroReasonMap.put(quickfix.field.OrdRejReason.STALE_ORDER, pfe_broker.avro.OrderRejectReason.STALE_ORDER); - avroReasonMap.put(quickfix.field.OrdRejReason.INCORRECT_QUANTITY, pfe_broker.avro.OrderRejectReason.INCORRECT_QUANTITY); + avroReasonMap.put(quickfix.field.OrdRejReason.INCORRECT_QUANTITY, + pfe_broker.avro.OrderRejectReason.INCORRECT_QUANTITY); avroReasonMap.put(quickfix.field.OrdRejReason.UNKNOWN_ACCOUNT, pfe_broker.avro.OrderRejectReason.UNKNOWN_ACCOUNT); - avroReasonMap.put(quickfix.field.OrdRejReason.PRICE_EXCEEDS_CURRENT_PRICE_BAND, pfe_broker.avro.OrderRejectReason.PRICE_EXCEEDS_CURRENT_PRICE_BAND); + avroReasonMap.put(quickfix.field.OrdRejReason.PRICE_EXCEEDS_CURRENT_PRICE_BAND, + pfe_broker.avro.OrderRejectReason.PRICE_EXCEEDS_CURRENT_PRICE_BAND); avroReasonMap.entrySet().forEach(entry -> quickfixReasonMap.put(entry.getValue(), entry.getKey())); } - public static int charFromAvro(pfe_broker.avro.OrderRejectReason reason) { + public static int intFromAvro(pfe_broker.avro.OrderRejectReason reason) { return quickfixReasonMap.getOrDefault(reason, quickfix.field.OrdRejReason.OTHER); } public static quickfix.field.OrdRejReason fromAvro(pfe_broker.avro.OrderRejectReason reason) { - return new quickfix.field.OrdRejReason(charFromAvro(reason)); + return new quickfix.field.OrdRejReason(intFromAvro(reason)); } public static pfe_broker.avro.OrderRejectReason toAvro(int reason) { @@ -75,4 +80,39 @@ public static pfe_broker.avro.OrderRejectReason toAvro(quickfix.field.OrdRejReas return toAvro(reason.getValue()); } } + + public static class Type { + + private static final Map avroTypeMap = new HashMap<>(); + private static final Map quickfixTypeMap = new HashMap<>(); + + static { + avroTypeMap.put(quickfix.field.OrdType.MARKET, pfe_broker.avro.Type.MARKET); + avroTypeMap.put(quickfix.field.OrdType.LIMIT, pfe_broker.avro.Type.LIMIT); + + avroTypeMap.entrySet().forEach(entry -> quickfixTypeMap.put(entry.getValue(), entry.getKey())); + } + + public static char charFromAvro(pfe_broker.avro.Type type) { + if (!quickfixTypeMap.containsKey(type)) { + throw new IllegalArgumentException("Unknown type: " + type); + } + return quickfixTypeMap.get(type); + } + + public static quickfix.field.OrdType fromAvro(pfe_broker.avro.Type type) { + return new quickfix.field.OrdType(charFromAvro(type)); + } + + public static pfe_broker.avro.Type toAvro(char type) { + if (!avroTypeMap.containsKey(type)) { + throw new IllegalArgumentException("Unknown type: " + type); + } + return avroTypeMap.get(type); + } + + public static pfe_broker.avro.Type toAvro(quickfix.field.OrdType type) { + return toAvro(type.getValue()); + } + } } diff --git a/libs/avro/src/main/java/pfe_broker/avro/utils/GenerateAvro.java b/libs/avro/src/main/java/pfe_broker/avro/utils/GenerateAvro.java index d5d3c3f5..447e9b59 100644 --- a/libs/avro/src/main/java/pfe_broker/avro/utils/GenerateAvro.java +++ b/libs/avro/src/main/java/pfe_broker/avro/utils/GenerateAvro.java @@ -10,11 +10,14 @@ public class GenerateAvro { // The order of the files is important public static File[] files = new File[] { getFileFromRessource("order-rejected-reason.avsc"), + getFileFromRessource("type.avsc"), getFileFromRessource("side.avsc"), + getFileFromRessource("order-book-request-type.avsc"), getFileFromRessource("order.avsc"), getFileFromRessource("trade.avsc"), getFileFromRessource("rejected-order.avsc"), getFileFromRessource("market-data.avsc"), + getFileFromRessource("order-book-request.avsc"), }; public static void main(String[] args) { diff --git a/libs/avro/src/main/java/pfe_broker/avro/utils/SchemaRecord.java b/libs/avro/src/main/java/pfe_broker/avro/utils/SchemaRecord.java new file mode 100644 index 00000000..4be756a2 --- /dev/null +++ b/libs/avro/src/main/java/pfe_broker/avro/utils/SchemaRecord.java @@ -0,0 +1,7 @@ +package pfe_broker.avro.utils; + +import org.apache.avro.Schema; + +public record SchemaRecord(Schema schema, String topicName) { + +} diff --git a/libs/common/build.gradle b/libs/common/build.gradle index eb6c3a05..5580ae41 100644 --- a/libs/common/build.gradle +++ b/libs/common/build.gradle @@ -11,9 +11,13 @@ repositories { dependencies { implementation project(":libs:log") + implementation project(":libs:avro") - runtimeOnly "org.yaml:snakeyaml" - implementation("io.micronaut.kafka:micronaut-kafka") + runtimeOnly group: 'org.yaml', name: 'snakeyaml', version: '2.2' + implementation group: 'io.micronaut.kafka', name: 'micronaut-kafka', version: '5.2.0' + + implementation group: 'io.micronaut', name: 'micronaut-http-client-jdk', version: '4.2.3' + implementation group: 'io.micronaut.serde', name: 'micronaut-serde-jackson', version: '2.7.0' implementation group: 'org.testcontainers', name: 'kafka', version: '1.19.3' implementation group: 'org.testcontainers', name: 'testcontainers', version: '1.19.3' @@ -24,7 +28,10 @@ dependencies { testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine" // Log4J - implementation group: 'org.apache.logging.log4j', name: 'log4j-api', version: '2.21.1' + implementation group: 'org.apache.logging.log4j', name: 'log4j-api', version: '2.22.1' + + // Avro + implementation group: 'org.apache.avro', name: 'avro', version: '1.11.3' } java { diff --git a/libs/common/src/main/java/pfe_broker/common/SchemaFactory.java b/libs/common/src/main/java/pfe_broker/common/SchemaFactory.java new file mode 100644 index 00000000..820bd820 --- /dev/null +++ b/libs/common/src/main/java/pfe_broker/common/SchemaFactory.java @@ -0,0 +1,76 @@ +package pfe_broker.common; + +import io.micronaut.context.ApplicationContext; +import io.micronaut.context.annotation.Context; +import io.micronaut.context.annotation.Property; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.client.HttpClient; +import java.util.List; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import pfe_broker.avro.utils.SchemaRecord; + +@Context +public class SchemaFactory { + + private static final Logger LOG = LoggerFactory.getLogger( + SchemaFactory.class + ); + + public SchemaFactory( + @NonNull List schemas, + ApplicationContext applicationContext, + @Property(name = "kafka.schema.registry.url") String schemaRegistryUrl + ) { + if (schemas == null || schemas.isEmpty()) { + return; + } + if (!UtilsRunning.isSchemaRegistryRunning(schemaRegistryUrl)) { + LOG.error("Schema registry is not running"); + return; + } + HttpClient httpClient = applicationContext.createBean( + HttpClient.class, + schemaRegistryUrl + ); + LOG.debug("Registering schemas"); + schemas.forEach(schemaRecord -> { + String schemaString = schemaRecord + .schema() + .toString() + .replaceAll("\"", "\\\\\""); + String subjectName = schemaRecord.topicName() + "-value"; + + String contentType = "application/vnd.schemaregistry.v1+json"; + + // Verify if the schema already exists and is the same + HttpRequest requestGet = HttpRequest.GET( + "/subjects/" + subjectName + "/versions/latest" + ); + + try { + String response = httpClient.toBlocking().retrieve(requestGet); + if (response.contains(schemaString)) { + LOG.trace("Schema already registered with latest version"); + return; + } + } catch (Exception e) { + // Do nothing + } + + HttpRequest request = HttpRequest + .POST( + "/subjects/" + subjectName + "/versions", + "{\"schema\": \"" + schemaString + "\"}" + ) + .header("Content-Type", contentType); + + try { + httpClient.toBlocking().retrieve(request); + } catch (Exception e) { + LOG.error("Error while registering schema: " + e.getMessage()); + } + }); + } +} diff --git a/libs/common/src/main/java/pfe_broker/common/UtilsRunning.java b/libs/common/src/main/java/pfe_broker/common/UtilsRunning.java index 7e8b9c25..d7034251 100644 --- a/libs/common/src/main/java/pfe_broker/common/UtilsRunning.java +++ b/libs/common/src/main/java/pfe_broker/common/UtilsRunning.java @@ -34,4 +34,38 @@ public static boolean isRedisRunning(String redisURI) { return false; } } + + public static boolean isPostgresRunning(String postgresURI) { + if (!postgresURI.startsWith("jdbc:postgresql://")) { + return false; + } + postgresURI = postgresURI.substring("jdbc:postgresql://".length()); + String[] hostAndPort = postgresURI.split(":"); + try (Socket socket = new Socket()) { + socket.connect( + new InetSocketAddress(hostAndPort[0], Integer.parseInt(hostAndPort[1])), + 500 + ); + return socket.isConnected(); + } catch (Exception e) { + return false; + } + } + + public static boolean isSchemaRegistryRunning(String schemaRegistryURI) { + if (!schemaRegistryURI.startsWith("http://")) { + return false; + } + schemaRegistryURI = schemaRegistryURI.substring("http://".length()); + String[] hostAndPort = schemaRegistryURI.split(":"); + try (Socket socket = new Socket()) { + socket.connect( + new InetSocketAddress(hostAndPort[0], Integer.parseInt(hostAndPort[1])), + 500 + ); + return socket.isConnected(); + } catch (Exception e) { + return false; + } + } } diff --git a/libs/log/build.gradle b/libs/log/build.gradle index 5b72ab4a..089f630c 100644 --- a/libs/log/build.gradle +++ b/libs/log/build.gradle @@ -11,13 +11,13 @@ repositories { } dependencies { - testImplementation 'org.junit.jupiter:junit-jupiter:5.9.1' + testImplementation 'org.junit.jupiter:junit-jupiter:5.10.1' // Log4J - implementation group: 'org.apache.logging.log4j', name: 'log4j-core', version: '2.21.1' - implementation group: 'org.apache.logging.log4j', name: 'log4j-api', version: '2.21.1' + implementation group: 'org.apache.logging.log4j', name: 'log4j-core', version: '2.22.1' + implementation group: 'org.apache.logging.log4j', name: 'log4j-api', version: '2.22.1' // Jackson yaml - implementation group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-yaml', version: '2.16.0' - implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.16.0' + implementation group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-yaml', version: '2.16.1' + implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.16.1' } sourceSets { diff --git a/libs/models/build.gradle b/libs/models/build.gradle index 770a0d42..d8fda605 100644 --- a/libs/models/build.gradle +++ b/libs/models/build.gradle @@ -14,8 +14,8 @@ dependencies { runtimeOnly group: 'org.yaml', name: 'snakeyaml', version: '2.2' // Database dependencies - annotationProcessor group: 'io.micronaut.data', name: 'micronaut-data-processor', version: '4.4.0' - implementation group: 'io.micronaut.data', name: 'micronaut-data-hibernate-jpa', version: '4.4.0' + annotationProcessor group: 'io.micronaut.data', name: 'micronaut-data-processor', version: '4.4.1' + implementation group: 'io.micronaut.data', name: 'micronaut-data-hibernate-jpa', version: '4.4.1' implementation group: 'io.micronaut.sql', name: 'micronaut-jdbc-hikari', version: '5.4.0' runtimeOnly group: 'org.postgresql', name: 'postgresql', version: '42.7.1' @@ -25,7 +25,7 @@ dependencies { // Test dependencies testImplementation "io.micronaut.test:micronaut-test-junit5" - testImplementation group: 'org.assertj', name: 'assertj-core', version: '3.24.2' + testImplementation group: 'org.assertj', name: 'assertj-core', version: '3.25.1' testImplementation group: 'org.testcontainers', name: 'postgresql', version: '1.19.3' testImplementation group: 'org.testcontainers', name: 'junit-jupiter', version: '1.19.3' } diff --git a/package-lock.json b/package-lock.json index 707adfee..4d3faa5b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33,9 +33,9 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.4.tgz", - "integrity": "sha512-r1IONyb6Ia+jYR2vvIDhdWdlTGhqbBoFqLTQidzZ4kepUFH15ejXvFHxCVbtl7BOXIudsIubf4E81xeA3h3IXA==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", + "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", "dev": true, "dependencies": { "@babel/highlight": "^7.23.4", @@ -108,30 +108,30 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.3.tgz", - "integrity": "sha512-BmR4bWbDIoFJmJ9z2cZ8Gmm2MXgEDgjdWgpKmKWUt54UGFJdlj31ECtbaDvCG/qVdG3AQ1SfpZEs01lUFbzLOQ==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.5.tgz", + "integrity": "sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.3.tgz", - "integrity": "sha512-Jg+msLuNuCJDyBvFv5+OKOUjWMZgd85bKjbICd3zWrKAo+bJ49HJufi7CQE0q0uR8NGyO6xkCACScNqyjHSZew==", + "version": "7.23.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.7.tgz", + "integrity": "sha512-+UpDgowcmqe36d4NwqvKsyPMlOLNGMsfMmQ5WGCu+siCe3t3dfe9njrzGfdN4qq+bcNUt0+Vw6haRxBOycs4dw==", "dev": true, "dependencies": { "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.22.13", - "@babel/generator": "^7.23.3", - "@babel/helper-compilation-targets": "^7.22.15", + "@babel/code-frame": "^7.23.5", + "@babel/generator": "^7.23.6", + "@babel/helper-compilation-targets": "^7.23.6", "@babel/helper-module-transforms": "^7.23.3", - "@babel/helpers": "^7.23.2", - "@babel/parser": "^7.23.3", + "@babel/helpers": "^7.23.7", + "@babel/parser": "^7.23.6", "@babel/template": "^7.22.15", - "@babel/traverse": "^7.23.3", - "@babel/types": "^7.23.3", + "@babel/traverse": "^7.23.7", + "@babel/types": "^7.23.6", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -156,12 +156,12 @@ } }, "node_modules/@babel/generator": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.4.tgz", - "integrity": "sha512-esuS49Cga3HcThFNebGhlgsrVLkvhqvYDTzgjfFFlHJcIfLe5jFmRRfCQ1KuBfc4Jrtn3ndLgKWAKjBE+IraYQ==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz", + "integrity": "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==", "dev": true, "dependencies": { - "@babel/types": "^7.23.4", + "@babel/types": "^7.23.6", "@jridgewell/gen-mapping": "^0.3.2", "@jridgewell/trace-mapping": "^0.3.17", "jsesc": "^2.5.1" @@ -195,14 +195,14 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.15.tgz", - "integrity": "sha512-y6EEzULok0Qvz8yyLkCvVX+02ic+By2UdOhylwUOvOn9dvYc9mKICJuuU1n1XBI02YWsNsnrY1kc6DVbjcXbtw==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz", + "integrity": "sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==", "dev": true, "dependencies": { - "@babel/compat-data": "^7.22.9", - "@babel/helper-validator-option": "^7.22.15", - "browserslist": "^4.21.9", + "@babel/compat-data": "^7.23.5", + "@babel/helper-validator-option": "^7.23.5", + "browserslist": "^4.22.2", "lru-cache": "^5.1.1", "semver": "^6.3.1" }, @@ -220,17 +220,17 @@ } }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.15.tgz", - "integrity": "sha512-jKkwA59IXcvSaiK2UN45kKwSC9o+KuoXsBDvHvU/7BecYIp8GQ2UwrVvFgJASUT+hBnwJx6MhvMCuMzwZZ7jlg==", + "version": "7.23.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.23.7.tgz", + "integrity": "sha512-xCoqR/8+BoNnXOY7RVSgv6X+o7pmT5q1d+gGcRlXYkI+9B31glE4jeejhKVpA04O1AtzOt7OSQ6VYKP5FcRl9g==", "dev": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-function-name": "^7.22.5", - "@babel/helper-member-expression-to-functions": "^7.22.15", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-member-expression-to-functions": "^7.23.0", "@babel/helper-optimise-call-expression": "^7.22.5", - "@babel/helper-replace-supers": "^7.22.9", + "@babel/helper-replace-supers": "^7.22.20", "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", "@babel/helper-split-export-declaration": "^7.22.6", "semver": "^6.3.1" @@ -278,9 +278,9 @@ } }, "node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.4.3.tgz", - "integrity": "sha512-WBrLmuPP47n7PNwsZ57pqam6G/RGo1vw/87b0Blc53tZNGZ4x7YvZ6HgQe2vo1W/FR20OgjeZuGXzudPiXHFug==", + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.4.4.tgz", + "integrity": "sha512-QcJMILQCu2jm5TFPGA3lCpJJTeEP+mqeXooG/NZbg/h5FTFi6V0+99ahlRsW8/kRLyb24LZVCCiclDedhLKcBA==", "dev": true, "dependencies": { "@babel/helper-compilation-targets": "^7.22.6", @@ -480,9 +480,9 @@ } }, "node_modules/@babel/helper-validator-option": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.15.tgz", - "integrity": "sha512-bMn7RmyFjY/mdECUbgn9eoSY4vqvacUnS9i9vGAGttgFWesO6B4CYWA7XlpbWgBt71iv/hfbPlynohStqnu5hA==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz", + "integrity": "sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==", "dev": true, "engines": { "node": ">=6.9.0" @@ -503,14 +503,14 @@ } }, "node_modules/@babel/helpers": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.4.tgz", - "integrity": "sha512-HfcMizYz10cr3h29VqyfGL6ZWIjTwWfvYBMsBVGwpcbhNGe3wQ1ZXZRPzZoAHhd9OqHadHqjQ89iVKINXnbzuw==", + "version": "7.23.8", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.8.tgz", + "integrity": "sha512-KDqYz4PiOWvDFrdHLPhKtCThtIcKVy6avWD2oG4GEvyQ+XDZwHD4YQd+H2vNMnq2rkdxsDkU82T+Vk8U/WXHRQ==", "dev": true, "dependencies": { "@babel/template": "^7.22.15", - "@babel/traverse": "^7.23.4", - "@babel/types": "^7.23.4" + "@babel/traverse": "^7.23.7", + "@babel/types": "^7.23.6" }, "engines": { "node": ">=6.9.0" @@ -593,9 +593,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.4.tgz", - "integrity": "sha512-vf3Xna6UEprW+7t6EtOmFpHNAuxw3xqPZghy+brsnusscJRW5BMUzzHZc5ICjULee81WeUV2jjakG09MDglJXQ==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.6.tgz", + "integrity": "sha512-Z2uID7YJ7oNvAI20O9X0bblw7Qqs8Q2hFy0R9tAfnfLkp5MW0UH9eUvnDSnFwKZ0AvgS1ucqR4KzvVHgnke1VQ==", "dev": true, "bin": { "parser": "bin/babel-parser.js" @@ -637,9 +637,9 @@ } }, "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.23.3.tgz", - "integrity": "sha512-XaJak1qcityzrX0/IU5nKHb34VaibwP3saKqG6a/tppelgllOH13LUann4ZCIBcVOeE6H18K4Vx9QKkVww3z/w==", + "version": "7.23.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.23.7.tgz", + "integrity": "sha512-LlRT7HgaifEpQA1ZgLVOIJZZFVPWN5iReq/7/JixwBtwcoeVGDBD53ZV28rrsLYOZs1Y/EHhA8N/Z6aazHR8cw==", "dev": true, "dependencies": { "@babel/helper-environment-visitor": "^7.22.20", @@ -653,15 +653,13 @@ } }, "node_modules/@babel/plugin-proposal-decorators": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.23.3.tgz", - "integrity": "sha512-u8SwzOcP0DYSsa++nHd/9exlHb0NAlHCb890qtZZbSwPX2bFv8LBEztxwN7Xg/dS8oAFFidhrI9PBcLBJSkGRQ==", + "version": "7.23.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.23.7.tgz", + "integrity": "sha512-b1s5JyeMvqj7d9m9KhJNHKc18gEJiSyVzVX3bwbiPalQBQpuvfPh6lA9F7Kk/dWH0TIiXRpB9yicwijY6buPng==", "dev": true, "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.22.15", + "@babel/helper-create-class-features-plugin": "^7.23.7", "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-replace-supers": "^7.22.20", - "@babel/helper-split-export-declaration": "^7.22.6", "@babel/plugin-syntax-decorators": "^7.23.3" }, "engines": { @@ -979,9 +977,9 @@ } }, "node_modules/@babel/plugin-transform-async-generator-functions": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.23.4.tgz", - "integrity": "sha512-efdkfPhHYTtn0G6n2ddrESE91fgXxjlqLsnUtPWnJs4a4mZIbUaK7ffqKIIUKXSHwcDvaCVX6GXkaJJFqtX7jw==", + "version": "7.23.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.23.7.tgz", + "integrity": "sha512-PdxEpL71bJp1byMG0va5gwQcXHxuEYC/BgI/e88mGTtohbZN28O5Yit0Plkkm/dBzCF/BxmbNcses1RH1T+urA==", "dev": true, "dependencies": { "@babel/helper-environment-visitor": "^7.22.20", @@ -1077,16 +1075,15 @@ } }, "node_modules/@babel/plugin-transform-classes": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.23.3.tgz", - "integrity": "sha512-FGEQmugvAEu2QtgtU0uTASXevfLMFfBeVCIIdcQhn/uBQsMTjBajdnAtanQlOcuihWh10PZ7+HWvc7NtBwP74w==", + "version": "7.23.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.23.8.tgz", + "integrity": "sha512-yAYslGsY1bX6Knmg46RjiCiNSwJKv2IUC8qOdYKqMMr0491SXFhcHqOdRDeCRohOOIzwN/90C6mQ9qAKgrP7dg==", "dev": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-compilation-targets": "^7.22.15", + "@babel/helper-compilation-targets": "^7.23.6", "@babel/helper-environment-visitor": "^7.22.20", "@babel/helper-function-name": "^7.23.0", - "@babel/helper-optimise-call-expression": "^7.22.5", "@babel/helper-plugin-utils": "^7.22.5", "@babel/helper-replace-supers": "^7.22.20", "@babel/helper-split-export-declaration": "^7.22.6", @@ -1210,12 +1207,13 @@ } }, "node_modules/@babel/plugin-transform-for-of": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.23.3.tgz", - "integrity": "sha512-X8jSm8X1CMwxmK878qsUGJRmbysKNbdpTv/O1/v0LuY/ZkZrng5WYiekYSdg9m09OTmDDUWeEDsTE+17WYbAZw==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.23.6.tgz", + "integrity": "sha512-aYH4ytZ0qSuBbpfhuofbg/e96oQ7U2w1Aw/UQmKT+1l39uEhUPoFS3fHevDc1G0OvewyDudfMKY1OulczHzWIw==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1597,16 +1595,16 @@ } }, "node_modules/@babel/plugin-transform-runtime": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.23.4.tgz", - "integrity": "sha512-ITwqpb6V4btwUG0YJR82o2QvmWrLgDnx/p2A3CTPYGaRgULkDiC0DRA2C4jlRB9uXGUEfaSS/IGHfVW+ohzYDw==", + "version": "7.23.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.23.7.tgz", + "integrity": "sha512-fa0hnfmiXc9fq/weK34MUV0drz2pOL/vfKWvN7Qw127hiUPabFCUMgAbYWcchRzMJit4o5ARsK/s+5h0249pLw==", "dev": true, "dependencies": { "@babel/helper-module-imports": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5", - "babel-plugin-polyfill-corejs2": "^0.4.6", - "babel-plugin-polyfill-corejs3": "^0.8.5", - "babel-plugin-polyfill-regenerator": "^0.5.3", + "babel-plugin-polyfill-corejs2": "^0.4.7", + "babel-plugin-polyfill-corejs3": "^0.8.7", + "babel-plugin-polyfill-regenerator": "^0.5.4", "semver": "^6.3.1" }, "engines": { @@ -1702,13 +1700,13 @@ } }, "node_modules/@babel/plugin-transform-typescript": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.23.4.tgz", - "integrity": "sha512-39hCCOl+YUAyMOu6B9SmUTiHUU0t/CxJNUmY3qRdJujbqi+lrQcL11ysYUsAvFWPBdhihrv1z0oRG84Yr3dODQ==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.23.6.tgz", + "integrity": "sha512-6cBG5mBvUu4VUD04OHKnYzbuHNP8huDsD3EDqqpIpsswTDoqHCjLoHb6+QgsV1WsT2nipRqCPgxD3LXnEO7XfA==", "dev": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-create-class-features-plugin": "^7.22.15", + "@babel/helper-create-class-features-plugin": "^7.23.6", "@babel/helper-plugin-utils": "^7.22.5", "@babel/plugin-syntax-typescript": "^7.23.3" }, @@ -1783,18 +1781,18 @@ } }, "node_modules/@babel/preset-env": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.23.3.tgz", - "integrity": "sha512-ovzGc2uuyNfNAs/jyjIGxS8arOHS5FENZaNn4rtE7UdKMMkqHCvboHfcuhWLZNX5cB44QfcGNWjaevxMzzMf+Q==", + "version": "7.23.8", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.23.8.tgz", + "integrity": "sha512-lFlpmkApLkEP6woIKprO6DO60RImpatTQKtz4sUcDjVcK8M8mQ4sZsuxaTMNOZf0sqAq/ReYW1ZBHnOQwKpLWA==", "dev": true, "dependencies": { - "@babel/compat-data": "^7.23.3", - "@babel/helper-compilation-targets": "^7.22.15", + "@babel/compat-data": "^7.23.5", + "@babel/helper-compilation-targets": "^7.23.6", "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-validator-option": "^7.22.15", + "@babel/helper-validator-option": "^7.23.5", "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.23.3", "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.23.3", - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.23.3", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.23.7", "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", "@babel/plugin-syntax-async-generators": "^7.8.4", "@babel/plugin-syntax-class-properties": "^7.12.13", @@ -1815,25 +1813,25 @@ "@babel/plugin-syntax-top-level-await": "^7.14.5", "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", "@babel/plugin-transform-arrow-functions": "^7.23.3", - "@babel/plugin-transform-async-generator-functions": "^7.23.3", + "@babel/plugin-transform-async-generator-functions": "^7.23.7", "@babel/plugin-transform-async-to-generator": "^7.23.3", "@babel/plugin-transform-block-scoped-functions": "^7.23.3", - "@babel/plugin-transform-block-scoping": "^7.23.3", + "@babel/plugin-transform-block-scoping": "^7.23.4", "@babel/plugin-transform-class-properties": "^7.23.3", - "@babel/plugin-transform-class-static-block": "^7.23.3", - "@babel/plugin-transform-classes": "^7.23.3", + "@babel/plugin-transform-class-static-block": "^7.23.4", + "@babel/plugin-transform-classes": "^7.23.8", "@babel/plugin-transform-computed-properties": "^7.23.3", "@babel/plugin-transform-destructuring": "^7.23.3", "@babel/plugin-transform-dotall-regex": "^7.23.3", "@babel/plugin-transform-duplicate-keys": "^7.23.3", - "@babel/plugin-transform-dynamic-import": "^7.23.3", + "@babel/plugin-transform-dynamic-import": "^7.23.4", "@babel/plugin-transform-exponentiation-operator": "^7.23.3", - "@babel/plugin-transform-export-namespace-from": "^7.23.3", - "@babel/plugin-transform-for-of": "^7.23.3", + "@babel/plugin-transform-export-namespace-from": "^7.23.4", + "@babel/plugin-transform-for-of": "^7.23.6", "@babel/plugin-transform-function-name": "^7.23.3", - "@babel/plugin-transform-json-strings": "^7.23.3", + "@babel/plugin-transform-json-strings": "^7.23.4", "@babel/plugin-transform-literals": "^7.23.3", - "@babel/plugin-transform-logical-assignment-operators": "^7.23.3", + "@babel/plugin-transform-logical-assignment-operators": "^7.23.4", "@babel/plugin-transform-member-expression-literals": "^7.23.3", "@babel/plugin-transform-modules-amd": "^7.23.3", "@babel/plugin-transform-modules-commonjs": "^7.23.3", @@ -1841,15 +1839,15 @@ "@babel/plugin-transform-modules-umd": "^7.23.3", "@babel/plugin-transform-named-capturing-groups-regex": "^7.22.5", "@babel/plugin-transform-new-target": "^7.23.3", - "@babel/plugin-transform-nullish-coalescing-operator": "^7.23.3", - "@babel/plugin-transform-numeric-separator": "^7.23.3", - "@babel/plugin-transform-object-rest-spread": "^7.23.3", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.23.4", + "@babel/plugin-transform-numeric-separator": "^7.23.4", + "@babel/plugin-transform-object-rest-spread": "^7.23.4", "@babel/plugin-transform-object-super": "^7.23.3", - "@babel/plugin-transform-optional-catch-binding": "^7.23.3", - "@babel/plugin-transform-optional-chaining": "^7.23.3", + "@babel/plugin-transform-optional-catch-binding": "^7.23.4", + "@babel/plugin-transform-optional-chaining": "^7.23.4", "@babel/plugin-transform-parameters": "^7.23.3", "@babel/plugin-transform-private-methods": "^7.23.3", - "@babel/plugin-transform-private-property-in-object": "^7.23.3", + "@babel/plugin-transform-private-property-in-object": "^7.23.4", "@babel/plugin-transform-property-literals": "^7.23.3", "@babel/plugin-transform-regenerator": "^7.23.3", "@babel/plugin-transform-reserved-words": "^7.23.3", @@ -1863,9 +1861,9 @@ "@babel/plugin-transform-unicode-regex": "^7.23.3", "@babel/plugin-transform-unicode-sets-regex": "^7.23.3", "@babel/preset-modules": "0.1.6-no-external-plugins", - "babel-plugin-polyfill-corejs2": "^0.4.6", - "babel-plugin-polyfill-corejs3": "^0.8.5", - "babel-plugin-polyfill-regenerator": "^0.5.3", + "babel-plugin-polyfill-corejs2": "^0.4.7", + "babel-plugin-polyfill-corejs3": "^0.8.7", + "babel-plugin-polyfill-regenerator": "^0.5.4", "core-js-compat": "^3.31.0", "semver": "^6.3.1" }, @@ -1925,9 +1923,9 @@ "dev": true }, "node_modules/@babel/runtime": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.4.tgz", - "integrity": "sha512-2Yv65nlWnWlSpe3fXEyX5i7fx5kIKo4Qbcj+hMO0odwaneFjfXw5fdum+4yL20O0QiaHpia0cYQ9xpNMqrBwHg==", + "version": "7.23.8", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.8.tgz", + "integrity": "sha512-Y7KbAP984rn1VGMbGqKmBLio9V7y5Je9GvU4rQPCPinCyNfUcToxIXl06d59URp/F3LwinvODxab5N/G6qggkw==", "dev": true, "dependencies": { "regenerator-runtime": "^0.14.0" @@ -1951,20 +1949,20 @@ } }, "node_modules/@babel/traverse": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.4.tgz", - "integrity": "sha512-IYM8wSUwunWTB6tFC2dkKZhxbIjHoWemdK+3f8/wq8aKhbUscxD5MX72ubd90fxvFknaLPeGw5ycU84V1obHJg==", + "version": "7.23.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.7.tgz", + "integrity": "sha512-tY3mM8rH9jM0YHFGyfC0/xf+SB5eKUu7HPj7/k3fpi9dAlsMc5YbQvDi0Sh2QTPXqMhyaAtzAr807TIyfQrmyg==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.23.4", - "@babel/generator": "^7.23.4", + "@babel/code-frame": "^7.23.5", + "@babel/generator": "^7.23.6", "@babel/helper-environment-visitor": "^7.22.20", "@babel/helper-function-name": "^7.23.0", "@babel/helper-hoist-variables": "^7.22.5", "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/parser": "^7.23.4", - "@babel/types": "^7.23.4", - "debug": "^4.1.0", + "@babel/parser": "^7.23.6", + "@babel/types": "^7.23.6", + "debug": "^4.3.1", "globals": "^11.1.0" }, "engines": { @@ -1972,9 +1970,9 @@ } }, "node_modules/@babel/types": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.4.tgz", - "integrity": "sha512-7uIFwVYpoplT5jp/kVv6EF93VaJ8H+Yn5IczYiaAi98ajzjfoZfslet/e0sLh+wVBjb2qqIut1b0S26VSafsSQ==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz", + "integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==", "dev": true, "dependencies": { "@babel/helper-string-parser": "^7.23.4", @@ -2026,23 +2024,22 @@ } }, "node_modules/@jnxplus/common": { - "version": "0.15.2", - "resolved": "https://registry.npmjs.org/@jnxplus/common/-/common-0.15.2.tgz", - "integrity": "sha512-PDf1r9f5JYAVrP6KGwgzCuMI0Ip9s4MdHmy1WVexG3UsE1NuKeo6GV9ixzKdgQ2VqteOeznpnBoa0L6vheogow==", + "version": "0.18.4", + "resolved": "https://registry.npmjs.org/@jnxplus/common/-/common-0.18.4.tgz", + "integrity": "sha512-4iUeLthq+XH1K41h6rUVRJ2Ckqp/L9jagZ7SrySpty16lKtLRo+TJ11a86ncy1lPD2V6fW8AJafTxE4kP8nsAQ==", "dev": true, "dependencies": { "@nx/devkit": ">=17.0.0", - "axios": "^1.6.0", "tslib": "^2.6.2" } }, "node_modules/@jnxplus/nx-gradle": { - "version": "0.17.6", - "resolved": "https://registry.npmjs.org/@jnxplus/nx-gradle/-/nx-gradle-0.17.6.tgz", - "integrity": "sha512-2RofEAio+E4iq7ob7Gwle8gnInqBMA3+r9pJ/zFy5pgIZbaPGnAp++b8V8vYdpR2fFb1+ymka21VIB7DreNbsA==", + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@jnxplus/nx-gradle/-/nx-gradle-0.17.19.tgz", + "integrity": "sha512-ajaWXHy2A8J+NQstYr1vQ4w/dfAiixbaYg1v4FBIMHHKv+cAcWg336JkR/F7szvuAGL3a5LHxiHNwTJ+1WyTRA==", "dev": true, "dependencies": { - "@jnxplus/common": "0.15.2", + "@jnxplus/common": "0.18.4", "@nx/devkit": ">=17.0.0", "nx": ">=17.0.0", "tslib": "^2.6.2" @@ -2087,9 +2084,9 @@ "dev": true }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.20", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz", - "integrity": "sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==", + "version": "0.3.21", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.21.tgz", + "integrity": "sha512-SRfKmRe1KvYnxjEMtxEr+J4HIeMX5YBg/qhRHpxEIGjhX1rshcHlnFUE9K0GazhVKWM7B+nARSkV8LuvJdJ5/g==", "dev": true, "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -2132,12 +2129,12 @@ } }, "node_modules/@nrwl/devkit": { - "version": "17.1.3", - "resolved": "https://registry.npmjs.org/@nrwl/devkit/-/devkit-17.1.3.tgz", - "integrity": "sha512-8HfIY7P3yIYfQ/XKuHoq0GGLA9GpwWtBlI9kPQ0ygjuJ9BkpiGMtQvO6003zs7c6vpc2vNeG+Jmi72+EKvoN5A==", + "version": "17.2.8", + "resolved": "https://registry.npmjs.org/@nrwl/devkit/-/devkit-17.2.8.tgz", + "integrity": "sha512-l2dFy5LkWqSA45s6pee6CoqJeluH+sjRdVnAAQfjLHRNSx6mFAKblyzq5h1f4P0EUCVVVqLs+kVqmNx5zxYqvw==", "dev": true, "dependencies": { - "@nx/devkit": "17.1.3" + "@nx/devkit": "17.2.8" } }, "node_modules/@nrwl/js": { @@ -2172,12 +2169,12 @@ } }, "node_modules/@nx/devkit": { - "version": "17.1.3", - "resolved": "https://registry.npmjs.org/@nx/devkit/-/devkit-17.1.3.tgz", - "integrity": "sha512-1Is7ooovg3kdGJ5VdkePulRUDaMYLLULr+LwXgx7oHSW7AY2iCmhkoOE/vSR7DJ6rkey2gYx7eT1IoRoORiIaQ==", + "version": "17.2.8", + "resolved": "https://registry.npmjs.org/@nx/devkit/-/devkit-17.2.8.tgz", + "integrity": "sha512-6LtiQihtZwqz4hSrtT5cCG5XMCWppG6/B8c1kNksg97JuomELlWyUyVF+sxmeERkcLYFaKPTZytP0L3dmCFXaw==", "dev": true, "dependencies": { - "@nrwl/devkit": "17.1.3", + "@nrwl/devkit": "17.2.8", "ejs": "^3.1.7", "enquirer": "~2.3.6", "ignore": "^5.0.4", @@ -2235,6 +2232,33 @@ } } }, + "node_modules/@nx/js/node_modules/@nrwl/devkit": { + "version": "17.1.3", + "resolved": "https://registry.npmjs.org/@nrwl/devkit/-/devkit-17.1.3.tgz", + "integrity": "sha512-8HfIY7P3yIYfQ/XKuHoq0GGLA9GpwWtBlI9kPQ0ygjuJ9BkpiGMtQvO6003zs7c6vpc2vNeG+Jmi72+EKvoN5A==", + "dev": true, + "dependencies": { + "@nx/devkit": "17.1.3" + } + }, + "node_modules/@nx/js/node_modules/@nx/devkit": { + "version": "17.1.3", + "resolved": "https://registry.npmjs.org/@nx/devkit/-/devkit-17.1.3.tgz", + "integrity": "sha512-1Is7ooovg3kdGJ5VdkePulRUDaMYLLULr+LwXgx7oHSW7AY2iCmhkoOE/vSR7DJ6rkey2gYx7eT1IoRoORiIaQ==", + "dev": true, + "dependencies": { + "@nrwl/devkit": "17.1.3", + "ejs": "^3.1.7", + "enquirer": "~2.3.6", + "ignore": "^5.0.4", + "semver": "7.5.3", + "tmp": "~0.2.1", + "tslib": "^2.3.0" + }, + "peerDependencies": { + "nx": ">= 16 <= 18" + } + }, "node_modules/@nx/nx-darwin-arm64": { "version": "17.1.3", "resolved": "https://registry.npmjs.org/@nx/nx-darwin-arm64/-/nx-darwin-arm64-17.1.3.tgz", @@ -2410,6 +2434,33 @@ "yargs-parser": "21.1.1" } }, + "node_modules/@nx/workspace/node_modules/@nrwl/devkit": { + "version": "17.1.3", + "resolved": "https://registry.npmjs.org/@nrwl/devkit/-/devkit-17.1.3.tgz", + "integrity": "sha512-8HfIY7P3yIYfQ/XKuHoq0GGLA9GpwWtBlI9kPQ0ygjuJ9BkpiGMtQvO6003zs7c6vpc2vNeG+Jmi72+EKvoN5A==", + "dev": true, + "dependencies": { + "@nx/devkit": "17.1.3" + } + }, + "node_modules/@nx/workspace/node_modules/@nx/devkit": { + "version": "17.1.3", + "resolved": "https://registry.npmjs.org/@nx/devkit/-/devkit-17.1.3.tgz", + "integrity": "sha512-1Is7ooovg3kdGJ5VdkePulRUDaMYLLULr+LwXgx7oHSW7AY2iCmhkoOE/vSR7DJ6rkey2gYx7eT1IoRoORiIaQ==", + "dev": true, + "dependencies": { + "@nrwl/devkit": "17.1.3", + "ejs": "^3.1.7", + "enquirer": "~2.3.6", + "ignore": "^5.0.4", + "semver": "7.5.3", + "tmp": "~0.2.1", + "tslib": "^2.3.0" + }, + "peerDependencies": { + "nx": ">= 16 <= 18" + } + }, "node_modules/@nxlv/python": { "version": "16.3.1", "resolved": "https://registry.npmjs.org/@nxlv/python/-/python-16.3.1.tgz", @@ -2532,9 +2583,9 @@ } }, "node_modules/acorn": { - "version": "8.11.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz", - "integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==", + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", "dev": true, "bin": { "acorn": "bin/acorn" @@ -2544,9 +2595,9 @@ } }, "node_modules/acorn-walk": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.0.tgz", - "integrity": "sha512-FS7hV565M5l1R08MXqo8odwMTB02C2UqzB17RVgu9EyuYFBqJZ3/ZY97sQD5FewVu1UyDFc1yztUDrAwT0EypA==", + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", + "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", "dev": true, "engines": { "node": ">=0.4.0" @@ -2619,12 +2670,12 @@ "dev": true }, "node_modules/axios": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.2.tgz", - "integrity": "sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==", + "version": "1.6.5", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.5.tgz", + "integrity": "sha512-Ii012v05KEVuUoFWmMW/UQv9aRIc3ZwkWDcM+h5Il8izZCtRVpDUfwpoFf7eOtajT3QiGR4yDUx7lPqHJULgbg==", "dev": true, "dependencies": { - "follow-redirects": "^1.15.0", + "follow-redirects": "^1.15.4", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } @@ -2655,13 +2706,13 @@ } }, "node_modules/babel-plugin-polyfill-corejs2": { - "version": "0.4.6", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.6.tgz", - "integrity": "sha512-jhHiWVZIlnPbEUKSSNb9YoWcQGdlTLq7z1GHL4AjFxaoOUMuuEVJ+Y4pAaQUGOGk93YsVCKPbqbfw3m0SM6H8Q==", + "version": "0.4.7", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.7.tgz", + "integrity": "sha512-LidDk/tEGDfuHW2DWh/Hgo4rmnw3cduK6ZkOI1NPFceSK3n/yAGeOsNT7FLnSGHkXj3RHGSEVkN3FsCTY6w2CQ==", "dev": true, "dependencies": { "@babel/compat-data": "^7.22.6", - "@babel/helper-define-polyfill-provider": "^0.4.3", + "@babel/helper-define-polyfill-provider": "^0.4.4", "semver": "^6.3.1" }, "peerDependencies": { @@ -2678,12 +2729,12 @@ } }, "node_modules/babel-plugin-polyfill-corejs3": { - "version": "0.8.6", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.8.6.tgz", - "integrity": "sha512-leDIc4l4tUgU7str5BWLS2h8q2N4Nf6lGZP6UrNDxdtfF2g69eJ5L0H7S8A5Ln/arfFAfHor5InAdZuIOwZdgQ==", + "version": "0.8.7", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.8.7.tgz", + "integrity": "sha512-KyDvZYxAzkC0Aj2dAPyDzi2Ym15e5JKZSK+maI7NAwSqofvuFglbSsxE7wUOvTg9oFVnHMzVzBKcqEb4PJgtOA==", "dev": true, "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.4.3", + "@babel/helper-define-polyfill-provider": "^0.4.4", "core-js-compat": "^3.33.1" }, "peerDependencies": { @@ -2691,12 +2742,12 @@ } }, "node_modules/babel-plugin-polyfill-regenerator": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.3.tgz", - "integrity": "sha512-8sHeDOmXC8csczMrYEOf0UTNa4yE2SxV5JGeT/LP1n0OYVDUUFPxG9vdk2AlDlIit4t+Kf0xCtpgXPBwnn/9pw==", + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.4.tgz", + "integrity": "sha512-S/x2iOCvDaCASLYsOOgWOq4bCfKYVqvO/uxjkaYyZ3rVsVE3CeAI/c84NpyuBBymEgNvHgjEot3a9/Z/kXvqsg==", "dev": true, "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.4.3" + "@babel/helper-define-polyfill-provider": "^0.4.4" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" @@ -2771,9 +2822,9 @@ } }, "node_modules/browserslist": { - "version": "4.22.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.1.tgz", - "integrity": "sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ==", + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.2.tgz", + "integrity": "sha512-0UgcrvQmBDvZHFGdYUehrCNIazki7/lUP3kkoi/r3YB2amZbFM9J43ZRkJTXBUZK4gmx56+Sqk9+Vs9mwZx9+A==", "dev": true, "funding": [ { @@ -2790,9 +2841,9 @@ } ], "dependencies": { - "caniuse-lite": "^1.0.30001541", - "electron-to-chromium": "^1.4.535", - "node-releases": "^2.0.13", + "caniuse-lite": "^1.0.30001565", + "electron-to-chromium": "^1.4.601", + "node-releases": "^2.0.14", "update-browserslist-db": "^1.0.13" }, "bin": { @@ -2851,9 +2902,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001564", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001564.tgz", - "integrity": "sha512-DqAOf+rhof+6GVx1y+xzbFPeOumfQnhYzVnZD6LAXijR77yPtm9mfOcqOnT3mpnJiZVT+kwLAFnRlZcIz+c6bg==", + "version": "1.0.30001576", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001576.tgz", + "integrity": "sha512-ff5BdakGe2P3SQsMsiqmt1Lc8221NR1VzHj5jXN5vBny9A6fpze94HiVV/n7XRosOlsShJcvMv5mdnpjOGCEgg==", "dev": true, "funding": [ { @@ -3004,12 +3055,12 @@ "dev": true }, "node_modules/core-js-compat": { - "version": "3.33.3", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.33.3.tgz", - "integrity": "sha512-cNzGqFsh3Ot+529GIXacjTJ7kegdt5fPXxCBVS1G0iaZpuo/tBz399ymceLJveQhFFZ8qThHiP3fzuoQjKN2ow==", + "version": "3.35.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.35.0.tgz", + "integrity": "sha512-5blwFAddknKeNgsjBzilkdQ0+YK8L1PfqPYq40NOYMYFSS38qj+hpTcLLWwpIwA2A5bje/x5jmVn2tzUMg9IVw==", "dev": true, "dependencies": { - "browserslist": "^4.22.1" + "browserslist": "^4.22.2" }, "funding": { "type": "opencollective", @@ -3174,9 +3225,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.4.594", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.594.tgz", - "integrity": "sha512-xT1HVAu5xFn7bDfkjGQi9dNpMqGchUkebwf1GL7cZN32NSwwlHRPMSDJ1KN6HkS0bWUtndbSQZqvpQftKG2uFQ==", + "version": "1.4.630", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.630.tgz", + "integrity": "sha512-osHqhtjojpCsACVnuD11xO5g9xaCyw7Qqn/C2KParkMv42i8jrJJgx3g7mkHfpxwhy9MnOJr8+pKOdZ7qzgizg==", "dev": true }, "node_modules/emoji-regex": { @@ -3293,9 +3344,9 @@ } }, "node_modules/fastq": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", - "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.16.0.tgz", + "integrity": "sha512-ifCoaXsDrsdkWTtiNJX5uzHDsrck5TzfKKDcuFFTIrrc/BS076qgEIfoIy1VeZqViznfKiysPYTh/QeHtnIsYA==", "dev": true, "dependencies": { "reusify": "^1.0.4" @@ -3377,9 +3428,9 @@ } }, "node_modules/follow-redirects": { - "version": "1.15.3", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz", - "integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==", + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", + "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==", "dev": true, "funding": [ { @@ -3417,9 +3468,9 @@ "dev": true }, "node_modules/fs-extra": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.1.1.tgz", - "integrity": "sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==", + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", + "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", "dev": true, "dependencies": { "graceful-fs": "^4.2.0", @@ -3752,9 +3803,9 @@ } }, "node_modules/java-parser": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/java-parser/-/java-parser-2.1.0.tgz", - "integrity": "sha512-oOnFTc7jOe1ts3J/8M1iAzoBOLmY3O/vMRVTJlskFINzg4ky8OIZjx8irZaoaEZ1HIEOAd/b51vM59AaxeyRPw==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/java-parser/-/java-parser-2.2.0.tgz", + "integrity": "sha512-20/Rpuv4FnzTNgWkJBqs2M4qwBuPLMqOqNiwU5j7vKvrw4Ej+re8Aoc92KbDFb61M1u/Sd4Ygst+CpEcKXvhBQ==", "dev": true, "dependencies": { "chevrotain": "6.5.0", @@ -3989,9 +4040,9 @@ "dev": true }, "node_modules/node-releases": { - "version": "2.0.13", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", - "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==", + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", "dev": true }, "node_modules/npm-package-arg": { @@ -4244,9 +4295,9 @@ } }, "node_modules/prettier": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.1.0.tgz", - "integrity": "sha512-TQLvXjq5IAibjh8EpBIkNKxO749UEWABoiIZehEPiY4GNpVdhaFKqSTu+QrlU6D2dPAfubRmtJTi4K4YkQ5eXw==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.1.tgz", + "integrity": "sha512-qSUWshj1IobVbKc226Gw2pync27t0Kf0EdufZa9j7uBSJay1CC+B3K5lAAZoqgX3ASiKuWsk6OmzKRetXNObWg==", "dev": true, "bin": { "prettier": "bin/prettier.cjs" @@ -4259,12 +4310,12 @@ } }, "node_modules/prettier-plugin-java": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/prettier-plugin-java/-/prettier-plugin-java-2.4.0.tgz", - "integrity": "sha512-Yfso3P0H/8U6NYtpYppW07dEx+WZl48lBRmJ3YmMXHE+YS72HDRcraxMWwvqQh9hn9wkBWnqTlph8BkDwlYd5w==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/prettier-plugin-java/-/prettier-plugin-java-2.5.0.tgz", + "integrity": "sha512-6Uc1yHVrLTwgl7D9hsBS3Xa2qXXlEd4v/6z1oio7u8O6mQdh/6cP0cbhS1daik9tNSMMES/VyzI+78918KRuGA==", "dev": true, "dependencies": { - "java-parser": "2.1.0", + "java-parser": "2.2.0", "lodash": "4.17.21", "prettier": "3.0.3" } @@ -4384,9 +4435,9 @@ } }, "node_modules/regenerator-runtime": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", - "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==", + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", "dev": true }, "node_modules/regenerator-transform": { diff --git a/settings.gradle b/settings.gradle index 384ff64e..232710ac 100644 --- a/settings.gradle +++ b/settings.gradle @@ -5,10 +5,12 @@ pluginManagement { id 'com.github.johnrengelman.shadow' version "${shadowVersion}" id 'io.github.khalilou88.jnxplus' version "${jnxplusGradlePluginVersion}" id 'io.freefair.lombok' version "${lombokGradlePluginVersion}" + id 'jacoco' } } rootProject.name = 'pfe-broker' +include('components:order-book') include('components:quickfix-server') include('components:trade-stream') include('components:order-stream')