From 78bbcd1d524728f12d7fa87ae5f68168e92d355b Mon Sep 17 00:00:00 2001 From: Peter Lawrey Date: Tue, 17 Dec 2024 10:14:17 +0000 Subject: [PATCH 01/31] Add prompts and requirements, improve code and tests --- account/README.adoc | 211 +++++++++------- .../chronicle/account/dto/AbstractEvent.java | 24 +- .../run/chronicle/account/dto/CheckPoint.java | 18 +- .../chronicle/account/dto/CreateAccount.java | 53 ++-- .../account/dto/CreateAccountFailed.java | 28 ++- .../account/dto/OnCreateAccount.java | 33 ++- .../run/chronicle/account/dto/OnTransfer.java | 35 +-- .../run/chronicle/account/dto/Transfer.java | 52 ++-- .../chronicle/account/dto/TransferFailed.java | 33 ++- .../account/impl/AccountManagerImpl.java | 30 ++- .../chronicle/account/util/ErrorListener.java | 36 ++- .../account/util/LogsAccountManagerOut.java | 60 ++++- .../run/chronicle/account/AccountsTest.java | 99 ++++++-- .../account/dto/CreateAccountFailedTest.java | 79 ++++-- .../account/dto/CreateAccountTest.java | 72 ++++-- .../account/dto/OnCreateAccountTest.java | 62 ++++- .../chronicle/account/dto/OnTransferTest.java | 75 +++++- .../account/dto/TransferFailedTest.java | 85 +++++-- .../chronicle/account/dto/TransferTest.java | 65 +++-- .../util/LogsAccountManagerOutTest.java | 46 +++- .../test/resources/account/copilot/in.yaml | 171 +++++++++++-- .../test/resources/account/copilot/out.yaml | 234 +++++++++++++----- .../test/resources/account/simple/_setup.yaml | 2 +- 23 files changed, 1205 insertions(+), 398 deletions(-) diff --git a/account/README.adoc b/account/README.adoc index 12dae6f..29dfb25 100644 --- a/account/README.adoc +++ b/account/README.adoc @@ -1,68 +1,76 @@ = Low-Latency Account Management System Over TCP and Shared Memory -Peter Lawrey +:author: Peter Lawrey +:revdate: 2024-12-16 +:revnumber: 1.3 -This example shows a simple high performance microservice accessible via TCP or shared memory storing each message in and out persisted in a Chronicle Queue. -There are a number of configurations with a comparison of the performance. -It is designed to produce minimal garbage to minimise jitter. +This project showcases a low-latency, high-performance account management microservice accessible via TCP or shared memory. Each inbound and outbound message is persisted in a Chronicle Queue, ensuring durability and traceability. The system is designed to produce minimal garbage and reduce latency jitter, making it suitable for high-frequency financial operations. -This Account Management System is designed to handle basic account operations such as account creation and fund transfers. -It's built on Chronicle Wire and uses Fluent Interface pattern, taking advantage of YAML files for test input data. +Built on Chronicle Queue and leveraging YAML-based input data, this service uses a fluent interface pattern to handle various account operations, such as account creation, fund transfers, and state checkpoints. It also includes benchmarking and testing utilities for performance evaluation. -This system leverages the `channels` package in Chronicle Wire, a high-speed messaging framework that excels in handling high volume and latency-critical data. +== Key Features -== Overview +* **Low Latency**: Designed for microsecond-level response times, especially when using shared memory transport. +* **High Throughput**: Capable of handling high transaction volumes with minimal garbage production. +* **Fault Tolerance & Durability**: All messages persist in a Chronicle Queue, aiding recovery and auditability. +* **Flexible Communication**: Supports TCP and shared memory transport configurations. +* **Modular Design**: Separates API, DTOs, and implementations for easier maintenance and extension. -The Account Management System serves as an efficient banking system that facilitates basic account operations, including account creation and fund transfers. -It is architected for low latency, efficient memory utilization, and performance under high transaction volume scenarios. +== System Overview -The core functionality is built around three main components: +=== Core Operations -. *Account Creation*: This feature allows for the establishment of new bank accounts with relevant details, such as account holder's name, account ID, currency type, and the initial balance. -. *Fund Transfer*: The system is capable of processing funds transfer between different accounts securely and swiftly. -Transfer details include sender account, target account, transfer amount, currency type, and a reference note. -. *Checkpointing*: The checkpoint functionality provides a snapshot of the current state of all accounts in the system at a specific point in time. +. **Account Creation**: +Create new accounts with specified details (name, account ID, currency, initial balance, and optional overdraft). -=== Network Communication +. **Fund Transfers**: +Transfer funds between accounts, validating currencies, balances, and overdrafts. Produces events indicating success or failure. -The system communicates over TCP to ensure reliable data transmission, or shared memory where possible to minimise latencies. -TCP is chosen for its built-in error checking and correction, ensuring that all account management data reaches the correct destination in the correct order and without errors. -All shared memory messaging is persisted to memory mapped files as it is written. +. **Checkpointing**: +Emit a snapshot of all accounts' current state at a given time for auditing or recovery. + +=== Architecture and Communication + +The service can operate in various configurations: + +* **TCP Client to TCP Service**: Offers the most flexibility for distributed deployments, with typical latencies under 20 µs. +* **TCP Client to Shared Memory Service**: Achieves better performance, typically ~10 µs latency. +* **Shared Memory Client and Service**: This method achieves the lowest latency, around 1.5 µs for typical operations. + +Chronicle Services' `channels` package and Chronicle Queue facilitate fast, low-overhead message passing. State changes are persistently recorded for replay or state restoration. === Testability and Benchmarking -The system also comes with a comprehensive set of tests and a benchmarking suite, allowing for easy performance measurement and regression testing. -The `AccountManagerBenchmarkMain` class acts as a benchmarking and testing entry point, demonstrating the functionality of the system and showing the execution time for the operations. +A suite of tests and benchmarks (e.g., `AccountManagerBenchmarkMain`) allows easy verification of correctness and performance. You can measure latency, throughput, and other key metrics to ensure the service meets performance goals. + +== Making It Production-Ready with Chronicle Services -=== Using a Production Server +To further enhance this solution for production environments, consider using https://chronicle.software/services/[Chronicle Services]. Chronicle Services provides advanced features to ensure high availability, resilience, and smooth operations, including: -In production, you want a server that has +* **Failover Support**: If the primary fails, the system automatically switches to a standby service instance, minimizing downtime. +* **Process Restarting**: Automatically restart services after unexpected terminations or during scheduled maintenance windows. +* **Live Upgrades**: Deploy updates without halting the entire system, allowing for rolling upgrades with minimal latency impact. +* **Idempotent Collections**: Simplify restartable event processing by ensuring operations can be safely replayed without side effects. +* **Acknowledged Replication**: Achieve high availability and real-time distribution across hosts, ensuring data consistency and durability. +* **Encryption**: Protect sensitive transaction data with on-disk encryption. +* **Monitoring & Management**: Gain insights into system performance and health, enabling proactive troubleshooting and optimization. -- A recent, fast processor with enough cores. -More cores isn't always better esp if it's more than you need. -- A Fast enterprise grade storage device e.g. NVMe. -This can reduce jitter due to writes by a factor fo 10 or better. -- Fast memory. -As event processing systems stream a large amount of data, yet retain relatively little, the memory size doesn't need to be large if your IO subsystem is fast enough. -- As we advocate producing little garbage and storing most data off heap, your heap size might be relatively small. e.g. 2 GB -- If you have a large eden space of 24 GB, you can produce 1 GB/hour of garbage and only minor collect once per day. -Possibly in a maintenance window if you have one. -However, for modest volumes an Eden size of 1 GB might be enough. +Integrating Chronicle Services allows your account management microservice to meet strict service-level agreements (SLAs), maintain continuous uptime, and adapt to evolving business and technical requirements. -=== Commercial Extensions to Productionise Microservices +image::img/Chronicle-Services-Diagram.png[] -There is a commercial extension https://chronicle.software/services/ that supports +== Recommended Production Environment -- Control over restarting of services -- Idempotent collections to simplify restarting event processing. -- Acknowledged Replication for High Availability and Realtime Distribution restartable across hosts. -- Encryption of messages stored on disk. -- Monitoring and Management support +Consider running the service on a server with: -image:img/Chronicle-Services-Diagram.png[] +* **Modern, Fast CPU**: Ensure sufficient but not excessive CPU cores. +* **High-Performance Storage (NVMe)**: Reduces jitter from IO operations. +* **Fast Memory**: Memory-intensive operations benefit from faster RAM. +* **Small Heap Size**: A modest heap (e.g., 2 GB) is sufficient since most data is off-heap. +* **Tuned Eden Space**: A large Eden space can reduce GC frequency. For modest volumes, ~1 GB Eden size may suffice. == Network Layouts -The client and the service can be laid out in a variety of ways without changing the code. +The client and the service can be laid out in various ways without changing the code. .Ballpark latencies running on a production quality server |=== @@ -72,7 +80,7 @@ The client and the service can be laid out in a variety of ways without changing | Shared Memory | Shared Memory | 1.5 µs | 3 µs |=== -=== TCP Client, TCP Service +=== Example: TCP Client, TCP Service This approach offers the most distributed option. The typical latencies are under 20 µs with the 99%ile latency not much higher. @@ -80,58 +88,83 @@ The typical latencies are under 20 µs with the 99%ile latency not much hig [source,mermaid] .... sequenceDiagram -autonumber -Client->>Gateway: transfer -Note over Client,Gateway: via TCP, persisted -Gateway->>+Service: transfer -Note right of Gateway: via TCP -Note right of Service: processes event -Service->>-Gateway: onTransfer -Note over Service,Gateway: via TCP, persisted -Gateway->>Client: onTransfer -Note left of Gateway: via TCP + autonumber + + participant Client as Client Application + participant Gateway as Gateway
(TCP <-> Shared Memory) + participant Service as Service
(Account Manager) + + Client->>Gateway: transfer (via TCP) + Note over Client,Gateway: The request is persisted
to a Chronicle Queue + + Gateway->>+Service: transfer (via TCP) + Note right of Gateway: Gateway acts as a bridge
and persists the message + Note right of Service: Service processes the event
(update balances, etc.) + + Service->>-Gateway: onTransfer (via TCP) + Note over Service,Gateway: Response is persisted again
for audit and recovery + + Gateway->>Client: onTransfer (via TCP) + Note left of Gateway: Client receives
the result of the operation .... This can be benchmarked all-in-one with the command line properties `-Durl=tcp://localhost:1248 -DserviceUrl=tcp://:1248` running `AccountManagerBenchmarkMain` -=== TCP Client, Shared Memory Service +=== Example: TCP Client, Shared Memory Service This approach offers the most distributed option. The typical latencies are around 10 µs with the 99%ile latency not much higher. [source,mermaid] .... sequenceDiagram -autonumber -Client->>Gateway: transfer -Note over Client,Gateway: via TCP, persisted -Gateway->>+Service: transfer -Note right of Gateway: via Shared Memory -Note right of Service: processes event -Service->>-Gateway: onTransfer -Note over Service,Gateway: via Shared Memory, persisted -Gateway->>Client: onTransfer -Note left of Gateway: via TCP + autonumber + + participant Client as Client Application + participant Gateway as Gateway
(TCP <-> Shared Memory) + participant Service as Service
(Account Manager) + + Client->>Gateway: transfer (via TCP) + Note over Client,Gateway: The request is persisted
to a Chronicle Queue on the Gateway. + + Gateway->>+Service: transfer (via Shared Memory) + Note right of Gateway: Gateway provides low-latency
shared memory messaging + Note right of Service: Service processes the event
(e.g., debit & credit accounts) + + Service->>-Gateway: onTransfer (via Shared Memory) + Note over Service,Gateway: Response is persisted again
for audit and recovery + + Gateway->>Client: onTransfer (via TCP) + Note left of Gateway: Client receives the result
of the transfer operation .... This can be benchmarked all-in-one with the command line properties `-Durl=tcp://:1248` running `AccountManagerBenchmarkMain` -=== Shared Memory Client and Service +=== Example: Shared Memory Client and Service This approach offers the most distributed option. The typical latencies are under 2 µs with the 99%ile latency about double this. [source,mermaid] .... sequenceDiagram -autonumber -Client->>queue: transfer -Note over Client,queue: via Shared Memory, persisted -queue->>+Service: transfer -Note right of queue: via Shared Memory -Note right of Service: processes event -Service->>-queue: onTransfer -Note over Service,queue: via Shared Memory, persisted -queue->>Client: onTransfer -Note left of queue: via Shared Memory + autonumber + + participant Client as Client Application + participant Queue as Queue
(Shared Memory Channel) + participant Service as Service
(Account Manager) + + Client->>Queue: transfer (via Shared Memory) + Note over Client,Queue: The transfer request is immediately
persisted in a Chronicle Queue
for audit and recovery. + + Queue->>+Service: transfer (via Shared Memory) + Note right of Queue: The Service reads the request
directly from the Queue. + Note right of Service: The Service processes the event
(e.g., adjust balances). + + Service->>-Queue: onTransfer (via Shared Memory) + Note over Service,Queue: The response event is
also persisted in the Queue. + + Queue->>Client: onTransfer (via Shared Memory) + Note left of Queue: The Client reads the response,
completing the round-trip
with minimal latency. + .... This can be benchmarked all-in-one with the default command line properties running `AccountManagerBenchmarkMain` @@ -140,19 +173,18 @@ This can be benchmarked all-in-one with the default command line properties runn We lay out our packages in the following manner -- `api` package for the input and output interfaces. -The input of one microservice might be the output of another microservice. -- `dto` package for POJOs (Plain Old Java Objects) that hold the data associated with each event. -- `impl` package for the service implementation and the high level classes it uses -- `util` package for separating low level helper methods and classes. +* `api` – Input and output interfaces defining the service contract. +* `dto` – Data Transfer Objects (POJOs) representing commands and events. +* `impl` – The core implementation of the account management logic. +* `util` – Utility classes for low-level operations and helpers. -For demos, we might include `main` classes, however for a production system, we use a framework Chronicle Services https://chronicle.software/services/ to handle manageability, monitoring, restart and fail over. +For demos, we might include `main` classes; however, for a production system, we use a framework https://chronicle.software/services/[Chronicle Services] to handle manageability, monitoring, restart and failover. == How to Run . Compile the source files using your preferred Java compiler. -. `AccountManagerServiceMain` runs the end service responsible for holding state and generating results of transactions - +. `AccountManagerServiceMain` runs the end service responsible for holding the state and generating results of transactions +. . `AccountManagerGatewayMain` acts as a gateway listening for TCP connections and writing to/reading from the shared memory queue the microservices uses . `AccountManagerClientMain` injects a few simple messages and waits for resulting events from the AccountManagerImpl @@ -199,13 +231,10 @@ transfer: { } ---- -== Note - -This is a basic implementation and does not handle many edge cases. -It also lacks a user-friendly interface, and the input is provided directly through YAML files. -It's intended as a demonstration of a system built on Chronicle Wire, and may not be suitable for production use without further modifications and improvements. +== Limitations and Future Work -== Contribute +This demo does not handle all edge cases and lacks a user-friendly interface. Future enhancements may include: -We would love your contributions! -Please submit a pull request with any improvements or bug fixes you have made. +* Improved error handling +* Better user interfaces or REST/HTTP endpoints +* More robust fault tolerance and recovery strategies, possibly leveraging Chronicle Services further \ No newline at end of file diff --git a/account/src/main/java/run/chronicle/account/dto/AbstractEvent.java b/account/src/main/java/run/chronicle/account/dto/AbstractEvent.java index f9ed76f..8a819ba 100644 --- a/account/src/main/java/run/chronicle/account/dto/AbstractEvent.java +++ b/account/src/main/java/run/chronicle/account/dto/AbstractEvent.java @@ -30,20 +30,25 @@ * It provides common properties like sender, target, and sending time, * along with fluent setter methods for easy chaining. * + *

Concrete subclasses must ensure that a valid sender, target, and sendingTime + * are provided before use. Validation throws an {@link InvalidMarshallableException} + * if any required field is not set.

+ * * @param the type of the event extending {@code AbstractEvent} */ @SuppressWarnings("unchecked") public abstract class AbstractEvent> extends SelfDescribingMarshallable implements Validatable { + @ShortText - private long sender; // sender represented in ShortText + private long sender; @ShortText - private long target; // target represented in ShortText + private long target; @NanoTime - private long sendingTime; // sendingTime represented as a unique timestamp in nanoseconds + private long sendingTime; /** * Retrieves the sender identifier. @@ -86,7 +91,7 @@ public E target(long target) { } /** - * Retrieves the sending time. + * Retrieves the sending wall clock time since epoch. * * @return the sending time in nanoseconds */ @@ -95,7 +100,7 @@ public long sendingTime() { } /** - * Sets the sending time and returns the updated object. + * Sets the sending wall clock time since epoch and returns the updated object. * * @param sendingTime the sending time to set * @return the updated object @@ -106,17 +111,18 @@ public E sendingTime(long sendingTime) { } /** - * The validate method is used to verify that all necessary properties have been set. + * Validates that all required fields (sender, target, and sendingTime) have been set. + * If any field is unset (0 indicates an unset value), it throws an {@link InvalidMarshallableException}. * * @throws InvalidMarshallableException If any of these properties is not set */ @Override public void validate() throws InvalidMarshallableException { if (sender == 0) - throw new InvalidMarshallableException("sender must be set"); // ensure sender is set + throw new InvalidMarshallableException("sender must be set"); if (target == 0) - throw new InvalidMarshallableException("target must be set"); // ensure target is set + throw new InvalidMarshallableException("target must be set"); if (sendingTime == 0) - throw new InvalidMarshallableException("sendingTime must be set"); // ensure sendingTime is set + throw new InvalidMarshallableException("sendingTime must be set"); } } diff --git a/account/src/main/java/run/chronicle/account/dto/CheckPoint.java b/account/src/main/java/run/chronicle/account/dto/CheckPoint.java index 0c547ba..274f55e 100644 --- a/account/src/main/java/run/chronicle/account/dto/CheckPoint.java +++ b/account/src/main/java/run/chronicle/account/dto/CheckPoint.java @@ -19,8 +19,22 @@ /** - * The {@code CheckPoint} class represents a request to dump all the current state of the system. - * It extends {@link AbstractEvent} to include common event properties. + * The {@code CheckPoint} class represents a request to produce a snapshot + * (or "dump") of the entire current system state at a given moment in time. + * This includes all accounts and their balances, ensuring that the state + * can be recorded for audit, recovery, or analysis. + * + * A valid {@code CheckPoint} event must have all these fields set; validation + * is performed automatically when the event is processed. + * + *

Usage example: + *

{@code
+ * CheckPoint cp = new CheckPoint()
+ *     .sender(gw2Id)
+ *     .target(vaultId)
+ *     .sendingTime(SystemTimeProvider.CLOCK.currentTimeNanos());
+ * }
*/ public class CheckPoint extends AbstractEvent { + // The CheckPoint event leverages the common fields and validation logic provided by AbstractEvent. } diff --git a/account/src/main/java/run/chronicle/account/dto/CreateAccount.java b/account/src/main/java/run/chronicle/account/dto/CreateAccount.java index d21b604..3ba80f2 100644 --- a/account/src/main/java/run/chronicle/account/dto/CreateAccount.java +++ b/account/src/main/java/run/chronicle/account/dto/CreateAccount.java @@ -22,18 +22,36 @@ import net.openhft.chronicle.wire.converter.ShortText; /** - * Represents the event of creating a new account. - * This class extends {@link AbstractEvent} and adds properties specific to account creation, - * such as the account holder's name, account number, currency, balance, and overdraft limit. - * Setters are designed using the fluent interface pattern for method chaining. + * Represents an event for creating a new account. This event includes all necessary details + * about the account to be created, such as: + *
    + *
  • name: The account holder's name.
  • + *
  • account: A unique identifier for this account.
  • + *
  • currency: The currency code for the account (stored as an integer code).
  • + *
  • balance: The initial balance of the account. Must be ≥ 0.
  • + *
  • overdraft: The overdraft limit for the account. Must be ≥ 0.
  • + *
+ * + *

This class uses a fluent interface style for setter methods, allowing for chaining: + *

{@code
+ * CreateAccount event = new CreateAccount()
+ *     .sender(gatewayId)
+ *     .target(serviceId)
+ *     .sendingTime(SystemTimeProvider.CLOCK.currentTimeNanos())
+ *     .name("Alice")
+ *     .account(101013L)
+ *     .currency(EUR_CODE)
+ *     .balance(1000.0)
+ *     .overdraft(100.0);
+ * }
*/ public class CreateAccount extends AbstractEvent { - private String name; // Name associated with the account - private long account; // Account identifier + private String name; + private long account; @ShortText - private int currency; // Currency for the account - private double balance; // Initial Balance of the account - private double overdraft; // Overdraft limit of the account + private int currency; + private double balance; + private double overdraft; /** * Retrieves the account identifier. @@ -76,7 +94,8 @@ public CreateAccount name(String name) { } /** - * Retrieves the currency code. + * Returns the currency code of the account. + * This is typically an integer code mapping to a currency (e.g., EUR, USD). * * @return the currency code */ @@ -85,10 +104,11 @@ public int currency() { } /** - * Sets the currency code. + * Sets the currency code and returns this instance. + * It is expected that the caller uses predefined integer codes for currencies. * * @param currency the currency code to set (e.g., "EUR", "USD") - * @return this object for method chaining + * @return this instance for method chaining */ public CreateAccount currency(int currency) { this.currency = currency; @@ -105,7 +125,8 @@ public double balance() { } /** - * Sets the initial balance of the account. + * Sets the initial balance of the account and returns this instance. + * The balance must be ≥ 0. * * @param balance the balance to set * @return this object for method chaining @@ -125,7 +146,8 @@ public double overdraft() { } /** - * Sets the overdraft limit of the account. + * Sets the overdraft limit of the account and returns this instance. + * The overdraft limit must be ≥ 0. * * @param overdraft the overdraft limit to set * @return this object for method chaining @@ -136,7 +158,8 @@ public CreateAccount overdraft(double overdraft) { } /** - * Validates that all necessary properties have been set and are valid. + * Validates that all required properties (sender, target, sendingTime, name, account, currency, balance, overdraft) + * have been set correctly. * * @throws InvalidMarshallableException if validation fails */ diff --git a/account/src/main/java/run/chronicle/account/dto/CreateAccountFailed.java b/account/src/main/java/run/chronicle/account/dto/CreateAccountFailed.java index 155454f..6955036 100644 --- a/account/src/main/java/run/chronicle/account/dto/CreateAccountFailed.java +++ b/account/src/main/java/run/chronicle/account/dto/CreateAccountFailed.java @@ -21,15 +21,27 @@ import net.openhft.chronicle.core.io.InvalidMarshallableException; /** - * This class, CreateAccountFailed, is an extension of AbstractEvent used to represent a situation - * where an attempt to create an account has failed. This class adds two properties to the event: - * a reference to the original CreateAccount object that failed, and a reason string describing why - * the account creation failed. As with other classes in this system, it uses a fluent style of - * setters, and includes a validate method to ensure all necessary properties have been set. + * Represents an event indicating that an attempt to create an account has failed. + * This event includes: + *
    + *
  • createAccount: The original {@link CreateAccount} request that failed.
  • + *
  • reason: A descriptive message explaining why the creation failed.
  • + *
+ * + *

Example usage:

+ *
{@code
+ * CreateAccountFailed caf = new CreateAccountFailed()
+ *     .sender(vaultId)
+ *     .target(gwId)
+ *     .sendingTime(SystemTimeProvider.CLOCK.currentTimeNanos())
+ *     .createAccount(originalRequest)
+ *     .reason("account already exists");
+ * }
*/ public class CreateAccountFailed extends AbstractEvent { - private CreateAccount createAccount; // Reference to the CreateAccount instance that failed - private String reason; // The reason for the failure + + private CreateAccount createAccount; + private String reason; /** * @return the CreateAccount instance that failed @@ -71,6 +83,8 @@ public CreateAccountFailed reason(String reason) { /** * Validates that all necessary properties have been set and are valid. + * Ensures that the original {@link CreateAccount} request and the reason are present + * and valid. If validation fails, an {@link InvalidMarshallableException} is thrown. * * @throws InvalidMarshallableException if validation fails */ diff --git a/account/src/main/java/run/chronicle/account/dto/OnCreateAccount.java b/account/src/main/java/run/chronicle/account/dto/OnCreateAccount.java index 867d63c..2cb683a 100644 --- a/account/src/main/java/run/chronicle/account/dto/OnCreateAccount.java +++ b/account/src/main/java/run/chronicle/account/dto/OnCreateAccount.java @@ -21,14 +21,21 @@ import net.openhft.chronicle.core.io.InvalidMarshallableException; /** - * The class OnCreateAccount is an extension of the AbstractEvent class, - * and it represents an event that occurs when a CreateAccount action has successfully occurred. - * The class contains a reference to the CreateAccount instance that initiated the event. - * This class follows the convention of using a fluent style for its setters, - * and it also includes a validate method to make sure that the createAccount field has been properly set. + * Represents an event indicating that an account was successfully created. + * This event references the original {@link CreateAccount} request that + * led to the successful creation. + * + *

Example usage: + *

{@code
+ * OnCreateAccount event = new OnCreateAccount()
+ *     .sender(vaultId)
+ *     .target(gatewayId)
+ *     .sendingTime(SystemTimeProvider.CLOCK.currentTimeNanos())
+ *     .createAccount(originalRequest);
+ * }
*/ public class OnCreateAccount extends AbstractEvent { - private CreateAccount createAccount; // The CreateAccount instance that triggered this event + private CreateAccount createAccount; /** * Retrieves the {@link CreateAccount} instance that triggered this event. @@ -51,18 +58,22 @@ public OnCreateAccount createAccount(CreateAccount createAccount) { } /** - * Validates that all necessary properties have been set and are valid. + * Validates that all required properties are set and valid. This includes: + *
    + *
  • All fields from the superclass (sender, target, sendingTime)
  • + *
  • A non-null {@link CreateAccount} instance
  • + *
  • Validation of the {@code CreateAccount} instance itself
  • + *
* - * @throws InvalidMarshallableException if validation fails + * @throws InvalidMarshallableException if validation fails for this event */ @Override public void validate() throws InvalidMarshallableException { super.validate(); // Validate fields in the superclass if (createAccount == null) { - throw new InvalidMarshallableException("CreateAccount must be set"); - } else { - createAccount.validate(); // Validate the CreateAccount instance + throw new InvalidMarshallableException("Invalid OnCreateAccount: 'createAccount' must not be null."); } + createAccount.validate(); // Validate the associated CreateAccount object } } diff --git a/account/src/main/java/run/chronicle/account/dto/OnTransfer.java b/account/src/main/java/run/chronicle/account/dto/OnTransfer.java index 5536111..d23a9c2 100644 --- a/account/src/main/java/run/chronicle/account/dto/OnTransfer.java +++ b/account/src/main/java/run/chronicle/account/dto/OnTransfer.java @@ -21,15 +21,21 @@ import net.openhft.chronicle.core.io.InvalidMarshallableException; /** - * Represents an event that occurs when a {@code Transfer} action takes place. - * This class extends {@link AbstractEvent} and encapsulates a reference to - * the {@link Transfer} instance that initiated this event. - *

- * The class follows the Fluent Interface pattern for setter methods, - * allowing for method chaining. + * Represents an event indicating that a funds transfer has taken place. + * This class extends {@link AbstractEvent}, adding a reference to the + * {@link Transfer} instance that initiated the event. + * + *

The class follows a fluent interface pattern for setter methods: + *

{@code
+ * OnTransfer event = new OnTransfer()
+ *     .sender(vaultId)
+ *     .target(gatewayId)
+ *     .sendingTime(SystemTimeProvider.CLOCK.currentTimeNanos())
+ *     .transfer(transferRequest);
+ * }
*/ public class OnTransfer extends AbstractEvent { - private Transfer transfer; // The Transfer instance that triggered this event + private Transfer transfer; /** * Retrieves the {@link Transfer} instance that triggered this event. @@ -52,7 +58,9 @@ public OnTransfer transfer(Transfer transfer) { } /** - * The validate method is used to verify that all necessary properties have been set. + * Validates that all required properties have been set and are valid. This includes + * the fields inherited from {@link AbstractEvent} (sender, target, sendingTime) and + * the {@code Transfer} instance itself. * * @throws InvalidMarshallableException If any of these properties is not set */ @@ -61,17 +69,16 @@ public void validate() throws InvalidMarshallableException { super.validate(); // Validate fields in the superclass if (transfer == null) { - throw new InvalidMarshallableException("Transfer must be set"); - } else { - transfer.validate(); // Validate the Transfer instance + throw new InvalidMarshallableException("Invalid OnTransfer event: 'transfer' must not be null."); } + transfer.validate(); } /** - * Overridden to specify the message format. In this case, it uses a lower level binary format, - * not a self-describing message. + * Indicates that this event does not use a self-describing message format and instead + * relies on a more compact binary representation. * - * @return {@code false} as it does not use a self-describing message + * @return {@code false} as this event does not use a self-describing message format */ @Override public boolean usesSelfDescribingMessage() { diff --git a/account/src/main/java/run/chronicle/account/dto/Transfer.java b/account/src/main/java/run/chronicle/account/dto/Transfer.java index abfd525..5a0cab2 100644 --- a/account/src/main/java/run/chronicle/account/dto/Transfer.java +++ b/account/src/main/java/run/chronicle/account/dto/Transfer.java @@ -21,17 +21,29 @@ import net.openhft.chronicle.bytes.Bytes; import net.openhft.chronicle.core.io.InvalidMarshallableException; import net.openhft.chronicle.wire.converter.ShortText; + /** - * The Transfer class extends AbstractEvent and represents a transfer event in the banking system. - * It encapsulates all necessary details of a transfer, including the account numbers of both sender and receiver, the currency, the amount, and a reference to the transaction details. Like the previous classes, - * it also provides a fluent interface for setters and includes a validate method to ensure all required fields are set. + * Represents a funds transfer operation between two accounts. + * + *

Example usage:

+ *
{@code
+ * Transfer tx = new Transfer()
+ *     .sender(vaultId)
+ *     .target(gatewayId)
+ *     .sendingTime(SystemTimeProvider.CLOCK.currentTimeNanos())
+ *     .from(101013L)
+ *     .to(101025L)
+ *     .currency(EUR_CODE)
+ *     .amount(10.0)
+ *     .reference(Bytes.from("Payment for services"));
+ * }
*/ public class Transfer extends AbstractEvent { - private long from, to; // The account numbers for the transfer + private long from, to; @ShortText - private int currency; // The currency of the transfer, represented in ShortText format - private double amount; // The amount to be transferred - private Bytes reference = Bytes.allocateElasticOnHeap(); // Reference to the transaction details + private int currency; + private double amount; + private Bytes reference = Bytes.allocateElasticOnHeap(); /** * Retrieves the sender's account number. @@ -83,7 +95,8 @@ public int currency() { } /** - * Sets the currency code of the transfer. + * Sets the currency code of the transfer and returns this instance. + * The currency code should typically map to a known currency. * * @param currency the currency code to set (e.g., "EUR", "USD") * @return this object for method chaining @@ -114,7 +127,8 @@ public Transfer amount(double amount) { } /** - * Retrieves the reference to the transaction details. + * Returns the reference data associated with this transfer. + * This could be a note, a reference number, or any additional context. * * @return the transaction reference */ @@ -123,7 +137,8 @@ public Bytes reference() { } /** - * Sets the reference to the transaction details. + * Sets the reference details for this transfer and returns this instance. + * The provided {@code Bytes} data is appended after clearing the existing reference. * * @param reference the reference to the transaction details * @return the updated object @@ -134,9 +149,17 @@ public Transfer reference(Bytes reference) { } /** - * The validate method is used to verify that all necessary properties have been set. + * Validates that all required fields have been set and are valid. + * This includes: + *
    + *
  • from: must be nonzero
  • + *
  • to: must be nonzero
  • + *
  • currency: must be nonzero
  • + *
  • amount: must be positive
  • + *
  • reference: must be non-null and non-empty
  • + *
* - * @throws InvalidMarshallableException If any of these properties is not set + * @throws InvalidMarshallableException if any validation check fails */ @Override public void validate() throws InvalidMarshallableException { @@ -154,10 +177,9 @@ public void validate() throws InvalidMarshallableException { } /** - * Overridden to specify the message format. In this case, it uses a lower level binary format, - * not a self-describing message. + * Specifies that this event uses a lower-level binary format rather than a self-describing message. * - * @return false as it does not use a self-describing message. + * @return {@code false}, indicating a non-self-describing message format */ @Override public boolean usesSelfDescribingMessage() { diff --git a/account/src/main/java/run/chronicle/account/dto/TransferFailed.java b/account/src/main/java/run/chronicle/account/dto/TransferFailed.java index 57c46d2..39fec4b 100644 --- a/account/src/main/java/run/chronicle/account/dto/TransferFailed.java +++ b/account/src/main/java/run/chronicle/account/dto/TransferFailed.java @@ -23,11 +23,19 @@ /** * The TransferFailed class is a type of AbstractEvent that represents a failed transfer operation in the system. * It contains the Transfer object that failed and the reason for the failure. - * Like other classes, it also follows the Fluent Interface pattern for setters, allowing chaining of method calls. + *

Example usage:

+ *
{@code
+ * TransferFailed event = new TransferFailed()
+ *     .sender(vaultId)
+ *     .target(gatewayId)
+ *     .sendingTime(SystemTimeProvider.CLOCK.currentTimeNanos())
+ *     .transfer(originalTransfer)
+ *     .reason("Insufficient funds");
+ * }
*/ public class TransferFailed extends AbstractEvent { - private Transfer transfer; // The original transfer that failed - private String reason; // The reason for the failure + private Transfer transfer; + private String reason; /** * Retrieves the original {@link Transfer} that failed. @@ -70,21 +78,26 @@ public TransferFailed reason(String reason) { } /** - * Validates that all necessary properties have been set and are valid. + * Validates that all required fields have been set and are valid. This includes: + *
    + *
  • All fields from {@link AbstractEvent} (sender, target, sendingTime)
  • + *
  • A non-null {@link Transfer} instance, which is itself validated
  • + *
  • A non-null reason string
  • + *
* - * @throws InvalidMarshallableException if validation fails + * @throws InvalidMarshallableException if any required field is missing or invalid */ @Override public void validate() throws InvalidMarshallableException { super.validate(); // Validate fields in the superclass if (transfer == null) { - throw new InvalidMarshallableException("Transfer must be set"); - } else { - transfer.validate(); // Validate the Transfer instance + throw new InvalidMarshallableException("Invalid TransferFailed: 'transfer' must not be null."); } + transfer.validate(); // Validate the Transfer object - if (reason == null) - throw new InvalidMarshallableException("reason must be set"); // Ensure 'reason' is set + if (reason == null) { + throw new InvalidMarshallableException("Invalid TransferFailed: 'reason' must not be null."); + } } } diff --git a/account/src/main/java/run/chronicle/account/impl/AccountManagerImpl.java b/account/src/main/java/run/chronicle/account/impl/AccountManagerImpl.java index 4bead7a..b64fa3b 100644 --- a/account/src/main/java/run/chronicle/account/impl/AccountManagerImpl.java +++ b/account/src/main/java/run/chronicle/account/impl/AccountManagerImpl.java @@ -30,20 +30,31 @@ import static net.openhft.chronicle.core.time.SystemTimeProvider.CLOCK; /** - * AccountManagerImpl is the implementation of the AccountManagerIn interface. - * It handles account creation, money transfers and checkpoints in the account management system. + * Implementation of the AccountManagerIn interface, handling: + *
    + *
  • Account creation
  • + *
  • Fund transfers
  • + *
  • Checkpoints (state snapshots)
  • + *
+ * + * This service receives commands (events) and produces corresponding result events. + * It stores account states and ensures validation according to the defined requirements. */ public class AccountManagerImpl extends SelfDescribingMarshallable implements AccountManagerIn { private transient final AccountManagerOut out; - // use a primitive long map + + // Store accounts keyed by account number + // TODO Use a primitive lop to account map private final Map accountsMap = new LinkedHashMap<>(); - // DTOs for events out + + // Event instances reused to reduce object creation private final OnCreateAccount onCreateAccount = new OnCreateAccount(); private final CreateAccountFailed createAccountFailed = new CreateAccountFailed(); private final OnTransfer onTransfer = new OnTransfer(); private final TransferFailed transferFailed = new TransferFailed(); + private long id; /** @@ -56,7 +67,7 @@ public AccountManagerImpl(AccountManagerOut out) { } /** - * Sets the id of the AccountManagerImpl instance. + * Sets the unique ID for this AccountManager instance, used to match incoming commands. * * @param id A long representing the id to be set. * @return This AccountManagerImpl instance. @@ -74,6 +85,8 @@ public AccountManagerImpl id(long id) { */ @Override public void createAccount(CreateAccount createAccount) throws InvalidMarshallableException { + // Assume createAccount.validate() has been called + // Verify if the account creation request is intended for this instance by checking the target of the request against the id of this instance // If they don't match, a failure message is sent with the reason "target mismatch" and the method returns if (createAccount.target() != id) { @@ -83,6 +96,7 @@ public void createAccount(CreateAccount createAccount) throws InvalidMarshallabl // Verify if the initial balance for the account is greater than or equal to 0 // If it isn't, a failure message is sent with the reason "invalid balance" and the method returns + // NOTE this is !(x >= 0) required to reject NaN if (!(createAccount.balance() >= 0)) { sendCreateAccountFailed(createAccount, "invalid balance"); return; @@ -106,14 +120,16 @@ public void createAccount(CreateAccount createAccount) throws InvalidMarshallabl sendOnCreateAccount(createAccount); } - /** - * Handles transfers between accounts. + * Processes a transfer command. Validates input, checks conditions (existence of accounts, + * matching currencies, available funds), updates state if valid, and emits success or failure events. * * @param transfer An instance of Transfer containing details of the transfer to be performed. */ @Override public void transfer(Transfer transfer) { + // Assume transfer.validate() has been called + // Verify if the transfer is intended for this instance by checking the target of the transfer against the id of this instance // If it doesn't match, a failure message is sent with the reason "target mismatch" and the method returns if (transfer.target() != id) { diff --git a/account/src/main/java/run/chronicle/account/util/ErrorListener.java b/account/src/main/java/run/chronicle/account/util/ErrorListener.java index 9672cd8..8305eba 100644 --- a/account/src/main/java/run/chronicle/account/util/ErrorListener.java +++ b/account/src/main/java/run/chronicle/account/util/ErrorListener.java @@ -18,16 +18,42 @@ package run.chronicle.account.util; /** - * This interface provides a method to handle JVM errors. - * Implementations of this interface will define how these errors are handled. + * A functional interface for handling critical JVM-level errors in the system. + * Implementations can define custom error-handling strategies such as logging, + * triggering alerts, or performing cleanup and shutdown procedures. + *

+ * Using a replicated queue, this can be done on another machine to avoid + * the impact of GC or IO the monitoring system might have. + * + *

Example usage: + *

{@code
+ * public class LoggingErrorListener implements ErrorListener {
+ *     @Override
+ *     public void jvmError(String msg) {
+ *         // Log the error to monitoring system, a file, or console
+ *         logger.error(msg);
+ *     }
+ * }
+ * }
+ * + *

By providing different {@code ErrorListener} implementations, the system + * can adapt to various environments (production vs. development) or integrate + * with different error-handling frameworks. */ +@FunctionalInterface public interface ErrorListener { /** - * This method handles JVM errors. - * It takes a String message which provides details about the error. + * Handles a critical JVM-level error. + *

+ * Implementations may choose to: + *

    + *
  • Log the error message to files or monitoring systems.
  • + *
  • Send alerts to administrators.
  • + *
  • Trigger a controlled shutdown or cleanup process.
  • + *
* - * @param msg a String providing details about the JVM error. + * @param msg a human-readable message providing details about the encountered JVM error. */ void jvmError(String msg); } diff --git a/account/src/main/java/run/chronicle/account/util/LogsAccountManagerOut.java b/account/src/main/java/run/chronicle/account/util/LogsAccountManagerOut.java index dda222d..726b204 100644 --- a/account/src/main/java/run/chronicle/account/util/LogsAccountManagerOut.java +++ b/account/src/main/java/run/chronicle/account/util/LogsAccountManagerOut.java @@ -1,3 +1,20 @@ +/* + * Copyright 2016-2022 chronicle.software + * + * https://chronicle.software + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package run.chronicle.account.util; import net.openhft.chronicle.core.Jvm; @@ -5,39 +22,80 @@ import run.chronicle.account.dto.*; /** - * Mock interface implementation of AccountManagerOut that logs everything + * A mock implementation of {@link AccountManagerOut} that logs all received events. + *

+ * This class is useful for testing, demonstration, or diagnostic purposes. It prints + * events to the console (via {@link Jvm} logging) without performing any real actions. + * This allows developers to trace the flow of events through the system and verify + * that commands and results are being produced as expected. */ public class LogsAccountManagerOut implements AccountManagerOut { + + /** + * Logs the start of a checkpoint operation. + * + * @param checkPoint The checkpoint command indicating that a state snapshot should begin. + */ @Override public void startCheckpoint(CheckPoint checkPoint) { Jvm.debug().on(getClass(), "startCheckpoint " + checkPoint); } + /** + * Logs the end of a checkpoint operation. + * + * @param checkPoint The checkpoint command indicating that the state snapshot should conclude. + */ @Override public void endCheckpoint(CheckPoint checkPoint) { Jvm.debug().on(getClass(), "endCheckpoint " + checkPoint); } + /** + * Logs a successful account creation event. + * + * @param onCreateAccount The event confirming that an account has been created. + */ @Override public void onCreateAccount(OnCreateAccount onCreateAccount) { Jvm.debug().on(getClass(), "onCreateAccount " + onCreateAccount); } + /** + * Logs a failed account creation attempt. + * + * @param createAccountFailed The event detailing why an account creation failed. + */ @Override public void createAccountFailed(CreateAccountFailed createAccountFailed) { Jvm.warn().on(getClass(), "createAccountFailed " + createAccountFailed); } + /** + * Logs a successful funds transfer event. + * + * @param onTransfer The event confirming that a transfer has completed successfully. + */ @Override public void onTransfer(OnTransfer onTransfer) { Jvm.debug().on(getClass(), "onTransfer " + onTransfer); } + /** + * Logs a failed transfer attempt. + * + * @param transferFailed The event detailing why a funds transfer failed. + */ @Override public void transferFailed(TransferFailed transferFailed) { Jvm.warn().on(getClass(), "transferFailed " + transferFailed); } + /** + * Logs a critical JVM-level error event. + * + * @param msg A descriptive message about the JVM error encountered. + */ @Override public void jvmError(String msg) { Jvm.error().on(getClass(), "jvmError " + msg); diff --git a/account/src/test/java/run/chronicle/account/AccountsTest.java b/account/src/test/java/run/chronicle/account/AccountsTest.java index d2d69ec..2c08ce3 100644 --- a/account/src/test/java/run/chronicle/account/AccountsTest.java +++ b/account/src/test/java/run/chronicle/account/AccountsTest.java @@ -36,19 +36,32 @@ import static org.junit.Assert.assertEquals; /** - * This class AccountsTest is a test class that uses - * JUnit's Parameterized runner to run multiple tests with different parameters. - * The test parameters are set up in the parameters method - * and are used to create an instance of YamlTester for each test. - * Each test runs through the runTester method which sets the system clock to a specific time, - * runs the test, and checks the output. - * After each test, the system clock is reset to its default state in the tearDown method. + * The {@code AccountsTest} class uses JUnit's Parameterized runner to execute a suite + * of YAML-driven tests for the Account Management Service (AMS). Each test scenario is + * defined by YAML files representing inputs and expected outputs. The tests verify that + * the {@link AccountManagerImpl} responds correctly to various commands, including edge cases + * introduced via "agitators" that manipulate or remove fields to test the system's resilience. + * + *

Key Features: + *

    + *
  • Parameterized Testing: Multiple scenarios defined in YAML are run using + * the same code, providing comprehensive coverage of both normal and abnormal conditions.
  • + *
  • Agitators for Robustness: Deliberate modifications (e.g., missing fields, + * invalid amounts) are introduced to ensure that the system handles validation failures gracefully.
  • + *
  • Time Management: The system clock is controlled via a {@link SetTimeProvider} + * to produce deterministic timestamps, making tests reproducible and consistent.
  • + *
  • Integration with Jinjava: Templates containing placeholders (e.g., {{...}}) + * can be rendered at runtime, enabling dynamic test inputs or scenario generation.
  • + *
*/ -// This class is used to run tests for the Account system. @SuppressWarnings("deprecation") @RunWith(Parameterized.class) public class AccountsTest { - // Defines the paths to the tests to run. + + /** + * Paths to the YAML test directories. Each directory contains sets of input/output YAML files + * and may represent different categories of tests (e.g., simple scenarios, mixed scenarios, generated tests). + */ static final String paths = "" + "account/simple," + "account/simple-gen," + @@ -58,51 +71,95 @@ public class AccountsTest { "account/gpt-gen," + "account/gpt-jinja," + "account/bard-gen," + - "account/bard-jinja"; + "account/bard-jinja," + + "account/o1-pro"; + + /** + * The identifier ("vault") used as the system ID (target) in tests. This matches the requirements + * that commands and responses must reference a known target identifier. + */ static final long VAULT = ShortText.INSTANCE.parse("vault"); - // The name of the test, and the tester that will run the test. + /** + * The test name and YamlTester instance for each parameterized test run. + *
    + *
  • {@code name} is the scenario name (often derived from the directory name).
  • + *
  • {@code tester} is the utility that loads YAML input, runs the {@link AccountManagerImpl}, + * and compares the actual output against the expected output specified in the YAML files.
  • + *
+ */ final String name; final net.openhft.chronicle.wire.utils.YamlTester tester; - // Constructor that sets the name and tester. + /** + * Constructs a single test parameter instance with a given scenario name and YamlTester. + * + * @param name A descriptive name for the test scenario. + * @param tester The YamlTester that will execute and verify the test scenario. + */ public AccountsTest(String name, net.openhft.chronicle.wire.utils.YamlTester tester) { this.name = name; this.tester = tester; } - // Defines the parameters for the parameterized test runner. + /** + * Provides a list of test parameters for the Parameterized runner. + *

+ * Uses {@link net.openhft.chronicle.wire.utils.YamlTesterParametersBuilder} to: + *

    + *
  • Create an {@link AccountManagerImpl} instance for each scenario.
  • + *
  • Introduce "agitators" that simulate invalid or missing data, ensuring robustness and proper validation handling.
  • + *
  • Optionally render templates if the YAML files contain Jinjava placeholders.
  • + *
+ * + * @return A list of arrays, each containing a scenario name and a YamlTester instance. + */ @Parameterized.Parameters(name = "{0}") public static List parameters() { // Returns a list of test parameters to run the tests with. // Each test will be run with an instance of AccountManagerImpl, // and will be subjected to various agitations to ensure robustness. return new net.openhft.chronicle.wire.utils.YamlTesterParametersBuilder<>(out -> new AccountManagerImpl(out).id(VAULT), AccountManagerOut.class, paths) + // Add agitators to test the system's ability to handle missing or invalid fields. .agitators( net.openhft.chronicle.wire.utils.YamlAgitator.messageMissing(), net.openhft.chronicle.wire.utils.YamlAgitator.duplicateMessage(), net.openhft.chronicle.wire.utils.YamlAgitator.overrideFields("currency: , amount: NaN, amount: -1, balance: NaN, balance: -1, target: no-vault".split(", *")), net.openhft.chronicle.wire.utils.YamlAgitator.missingFields("name, account, balance, sender, target, sendingTime, from, to, currency, amount, reference".split(", *"))) + // Use a custom exception handler to ensure JVM errors are logged as events. .exceptionHandlerFunction(out -> (log, msg, thrown) -> out.jvmError(thrown == null ? msg : (msg + " " + thrown))) .exceptionHandlerFunctionAndLog(true) - .inputFunction(s -> s.contains("{{")||s.contains("{#") ? new Jinjava().render(s, Collections.emptyMap()) : s) + // Render any templates found in the test YAML using Jinjava. + .inputFunction(s -> s.contains("{{") || s.contains("{#") ? new Jinjava().render(s, Collections.emptyMap()) : s) .get(); } - // After each test, this method resets the system time provider. + /** + * Reset the system time provider after each test to avoid affecting subsequent tests. + * Ensures that tests are isolated and no global state "leaks" between them. + */ @After public void tearDown() { SystemTimeProvider.CLOCK = SystemTimeProvider.INSTANCE; } - // This is the actual test method, which uses the provided tester - // to run the test and then compares the expected output to the actual output. + /** + * Executes the test scenario using the provided YamlTester. Before running the test, sets a known, + * deterministic time using {@link SetTimeProvider} to ensure reproducible timestamps. + * + *

The {@code tester.expected()} and {@code tester.actual()} methods are used to compare the + * expected output with the actual output produced by the AccountManagerImpl. + * + *

Assertions ensure that the scenario behaves as defined in the YAML specification. + */ @Test public void runTester() { - // Sets the system clock to a specific time for the purpose of testing. - SystemTimeProvider.CLOCK = new SetTimeProvider("2023-01-21T11:00:00") - .autoIncrement(1, TimeUnit.SECONDS); - // Asserts that the expected output matches the actual output. + // Set the system clock to a fixed starting time (2023-01-21T11:00:00) and increment by 1 second each event. + // This gives consistent timestamps for the events, aligning with the requirements. + SystemTimeProvider.CLOCK = new SetTimeProvider("2023-01-21T11:00:00").autoIncrement(1, TimeUnit.SECONDS); + + // Validate that the actual output matches the expected output defined in the scenario's YAML files. + // This ensures the AccountManagerImpl logic aligns with the system requirements and handles all specified conditions. assertEquals(tester.expected(), tester.actual()); } } diff --git a/account/src/test/java/run/chronicle/account/dto/CreateAccountFailedTest.java b/account/src/test/java/run/chronicle/account/dto/CreateAccountFailedTest.java index 312644d..a50cba2 100755 --- a/account/src/test/java/run/chronicle/account/dto/CreateAccountFailedTest.java +++ b/account/src/test/java/run/chronicle/account/dto/CreateAccountFailedTest.java @@ -9,10 +9,27 @@ import static org.junit.Assert.fail; import static run.chronicle.account.dto.CreateAccountTest.getCreateAccount; +/** + * Unit tests for {@link CreateAccountFailed} events. + *

+ * These tests verify that a {@code CreateAccountFailed} object can be deserialized + * correctly from a YAML representation and that appropriate validation errors + * are thrown when required fields are missing. + *

+ * Validations: + * - Ensure all mandatory fields (sender, target, sendingTime, createAccount, reason) are present. + * - Confirm that absence of createAccount or reason fields results in an InvalidMarshallableException. + */ public class CreateAccountFailedTest { + + /** + * Tests that a fully populated {@link CreateAccountFailed} event can be deserialized + * from a YAML string. Verifies that all fields match expected values. + */ @Test public void testFromString() { - CreateAccountFailed asf = Marshallable.fromString("" + + // YAML representation of a valid CreateAccountFailed event. + String yaml = "" + "!run.chronicle.account.dto.CreateAccountFailed {\n" + " sender: sender,\n" + " target: target,\n" + @@ -24,31 +41,55 @@ public void testFromString() { " name: name,\n" + " account: 2,\n" + " currency: CURR,\n" + - " balance: 1.0," + - " overdraft: 3.0" + + " balance: 1.0,\n" + + " overdraft: 3.0\n" + " },\n" + " reason: reasons\n" + - "}"); - assertEquals("sender", ShortText.INSTANCE.asString(asf.sender())); - assertEquals("target", ShortText.INSTANCE.asString(asf.target())); - assertEquals("reasons", asf.reason()); - assertEquals(getCreateAccount(), asf.createAccount()); + "}"; + + CreateAccountFailed event = Marshallable.fromString(yaml); + + // Verify fields are set as expected + assertEquals("Sender should match the provided ShortText 'sender'.", + "sender", ShortText.INSTANCE.asString(event.sender())); + assertEquals("Target should match the provided ShortText 'target'.", + "target", ShortText.INSTANCE.asString(event.target())); + assertEquals("Reason should match the provided reason string.", + "reasons", event.reason()); + assertEquals("The embedded createAccount object should match the expected CreateAccount.", + getCreateAccount(), event.createAccount()); } + /** + * Tests that attempting to deserialize a CreateAccountFailed event with no createAccount field + * results in an InvalidMarshallableException. + * + * The createAccount field is mandatory, so if it's missing, the validation should fail. + */ @Test(expected = InvalidMarshallableException.class) public void missingCreateAccount() { - CreateAccountFailed asf = Marshallable.fromString("" + + String yaml = "" + "!run.chronicle.account.dto.CreateAccountFailed {\n" + " sender: sender,\n" + " target: target,\n" + " sendingTime: 2001/02/03T04:05:06.007008009,\n" + - "}"); - fail(asf.toString()); + "}"; + CreateAccountFailed event = Marshallable.fromString(yaml); + + // If we reach this line, the test failed to throw the expected exception. + fail("Expected InvalidMarshallableException due to missing createAccount field. Event: " + event); } + /** + * Tests that attempting to deserialize a CreateAccountFailed event with no reason field + * results in an InvalidMarshallableException. + * + * The reason field is mandatory, so if it's missing, the validation should fail. + */ @Test(expected = InvalidMarshallableException.class) public void missingReason() { - CreateAccountFailed asf = Marshallable.fromString("" + + // YAML missing 'reason' field + String yaml = "" + "!run.chronicle.account.dto.CreateAccountFailed {\n" + " sender: sender,\n" + " target: target,\n" + @@ -60,11 +101,13 @@ public void missingReason() { " name: name,\n" + " account: 2,\n" + " currency: CURR,\n" + - " balance: 1.0" + - " },\n" + - "}"); - fail(asf.toString()); - } + " balance: 1.0\n" + + " }\n" + + "}"; + CreateAccountFailed event = Marshallable.fromString(yaml); -} \ No newline at end of file + // If we reach this line, the test failed to throw the expected exception. + fail("Expected InvalidMarshallableException due to missing reason field. Event: " + event); + } +} diff --git a/account/src/test/java/run/chronicle/account/dto/CreateAccountTest.java b/account/src/test/java/run/chronicle/account/dto/CreateAccountTest.java index d370ee3..680dace 100755 --- a/account/src/test/java/run/chronicle/account/dto/CreateAccountTest.java +++ b/account/src/test/java/run/chronicle/account/dto/CreateAccountTest.java @@ -6,7 +6,22 @@ import static org.junit.Assert.assertEquals; +/** + * Unit tests for the {@link CreateAccount} class. + *

+ * These tests verify that a {@code CreateAccount} object is constructed correctly, + * serialized as expected via {@code toString()}, and that its fields are properly set. + * The test uses known, fixed values for fields such as sender, target, and sendingTime + * to produce deterministic outputs. + */ public class CreateAccountTest { + + /** + * Helper method to create a fully-initialized {@link CreateAccount} object + * for testing. Uses consistent, known values for all fields. + * + * @return a {@code CreateAccount} instance populated with test data + */ static CreateAccount getCreateAccount() { return new CreateAccount() .sender(ShortText.INSTANCE.parse("sender")) @@ -19,23 +34,50 @@ static CreateAccount getCreateAccount() { .name("name"); } + /** + * Tests the {@code toString()} representation of a {@link CreateAccount} object. + * Also verifies selected fields (e.g., sendingTime, name) to ensure they are set correctly. + */ @Test public void testToString() { CreateAccount createAccount = getCreateAccount(); - assertEquals("" + - "!run.chronicle.account.dto.CreateAccount {\n" + - " sender: sender,\n" + - " target: target,\n" + - " sendingTime: 2001-02-03T04:05:06.007008009,\n" + - " name: name,\n" + - " account: 2,\n" + - " currency: CURR,\n" + - " balance: 1.0,\n" + - " overdraft: 3.0\n" + - "}\n", - createAccount.toString()); - assertEquals(NanoTime.INSTANCE.parse("2001-02-03T04:05:06.007008009"), - createAccount.sendingTime()); - assertEquals("name", createAccount.name()); + + // Check the string representation line-by-line to ensure it matches the expected format. + String expected = "" + + "!run.chronicle.account.dto.CreateAccount {\n" + + " sender: sender,\n" + + " target: target,\n" + + " sendingTime: 2001-02-03T04:05:06.007008009,\n" + + " name: name,\n" + + " account: 2,\n" + + " currency: CURR,\n" + + " balance: 1.0,\n" + + " overdraft: 3.0\n" + + "}\n"; + assertEquals("The toString() output of CreateAccount should match the expected YAML-like format.", + expected, createAccount.toString()); + + // Verify the parsed sendingTime matches what was set. + long expectedTime = NanoTime.INSTANCE.parse("2001-02-03T04:05:06.007008009"); + assertEquals("The sendingTime field should match the expected nanosecond timestamp.", + expectedTime, createAccount.sendingTime()); + + // Verify the name field is correctly set. + assertEquals("The name field should match the initialized value.", + "name", createAccount.name()); + + // Additional checks to ensure all fields are as expected. + assertEquals("The sender field should match the initialized value.", + "sender", ShortText.INSTANCE.asString(createAccount.sender())); + assertEquals("The target field should match the initialized value.", + "target", ShortText.INSTANCE.asString(createAccount.target())); + assertEquals("The account number should match the initialized value.", + 2L, createAccount.account()); + assertEquals("The currency field should match the initialized value.", + "CURR", ShortText.INSTANCE.asString(createAccount.currency())); + assertEquals("The balance should match the initialized value.", + 1.0, createAccount.balance(), 0.0); + assertEquals("The overdraft should match the initialized value.", + 3.0, createAccount.overdraft(), 0.0); } } diff --git a/account/src/test/java/run/chronicle/account/dto/OnCreateAccountTest.java b/account/src/test/java/run/chronicle/account/dto/OnCreateAccountTest.java index a005e8e..774686f 100755 --- a/account/src/test/java/run/chronicle/account/dto/OnCreateAccountTest.java +++ b/account/src/test/java/run/chronicle/account/dto/OnCreateAccountTest.java @@ -9,10 +9,24 @@ import static org.junit.Assert.assertEquals; import static run.chronicle.account.dto.CreateAccountTest.getCreateAccount; +/** + * Unit tests for {@link OnCreateAccount} event deserialization and validation. + *

+ * These tests verify that a {@code OnCreateAccount} object can be correctly + * deserialized from its YAML representation. Additionally, they confirm that + * mandatory fields are present and that an {@link InvalidMarshallableException} + * is thrown if any required fields (e.g., createAccount) are missing. + */ public class OnCreateAccountTest { + + /** + * Tests that a fully formed {@link OnCreateAccount} event is correctly + * deserialized from a YAML string. Verifies that mandatory fields are set + * and the embedded {@link CreateAccount} object matches expectations. + */ @Test public void testFromString() { - OnCreateAccount asf = Marshallable.fromString("" + + String yaml = "" + "!run.chronicle.account.dto.OnCreateAccount {\n" + " sender: sender,\n" + " target: target,\n" + @@ -24,24 +38,48 @@ public void testFromString() { " name: name,\n" + " account: 2,\n" + " currency: CURR,\n" + - " balance: 1.0," + - " overdraft: 3.0" + + " balance: 1.0,\n" + + " overdraft: 3.0\n" + " }\n" + - "}"); - assertEquals("sender", ShortText.INSTANCE.asString(asf.sender())); - assertEquals("target", ShortText.INSTANCE.asString(asf.target())); - assertEquals(getCreateAccount(), asf.createAccount()); + "}"; + + OnCreateAccount event = Marshallable.fromString(yaml); + + // Check sender field + assertEquals("The sender field should match the 'sender' ShortText value.", + "sender", ShortText.INSTANCE.asString(event.sender())); + + // Check target field + assertEquals("The target field should match the 'target' ShortText value.", + "target", ShortText.INSTANCE.asString(event.target())); + + // Check sendingTime field + long expectedTime = net.openhft.chronicle.wire.converter.NanoTime.INSTANCE.parse("2001/02/03T04:05:06.007008009"); + assertEquals("The sendingTime field should match the provided timestamp.", + expectedTime, event.sendingTime()); + + // Check the embedded CreateAccount object + assertEquals("The embedded createAccount object should match the expected reference instance.", + getCreateAccount(), event.createAccount()); } + /** + * Tests that deserializing a {@link OnCreateAccount} event without the mandatory + * createAccount field results in an {@link InvalidMarshallableException}. This + * confirms that validation logic is working as intended. + */ @Test(expected = InvalidMarshallableException.class) public void missingCreateAccount() { - OnCreateAccount asf = Marshallable.fromString("" + + String yaml = "" + "!run.chronicle.account.dto.OnCreateAccount {\n" + " sender: sender,\n" + " target: target,\n" + " sendingTime: 2001/02/03T04:05:06.007008009\n" + - "}"); - Assert.fail(asf.toString()); - } + "}"; -} \ No newline at end of file + OnCreateAccount event = Marshallable.fromString(yaml); + + // If we reach here, no exception was thrown, which means the test failed. + Assert.fail("Expected InvalidMarshallableException due to missing createAccount field, but got: " + event); + } +} diff --git a/account/src/test/java/run/chronicle/account/dto/OnTransferTest.java b/account/src/test/java/run/chronicle/account/dto/OnTransferTest.java index 3a0160b..cf4a3ee 100755 --- a/account/src/test/java/run/chronicle/account/dto/OnTransferTest.java +++ b/account/src/test/java/run/chronicle/account/dto/OnTransferTest.java @@ -9,8 +9,22 @@ import static org.junit.Assert.*; import static run.chronicle.account.dto.TransferTest.getTransfer; +/** + * Unit tests for the {@link OnTransfer} DTO. + *

+ * These tests verify that: + *

    + *
  • {@code OnTransfer} objects can be correctly serialized to and deserialized from their YAML representation.
  • + *
  • Mandatory fields (like the embedded {@link Transfer}) are present and validated.
  • + *
  • The {@link OnTransfer#usesSelfDescribingMessage()} method behaves as expected.
  • + *
+ */ public class OnTransferTest { + /** + * A YAML representation of a fully populated {@link OnTransfer} instance, + * including a nested {@link Transfer} object and all required fields. + */ public static final String EXPECTED = "" + "!run.chronicle.account.dto.OnTransfer {\n" + " sender: target,\n" + @@ -28,32 +42,67 @@ public class OnTransferTest { " }\n" + "}\n"; + /** + * Tests that the {@link OnTransfer#toString()} method produces the expected YAML representation. + * Also serves as a regression check to ensure the serialized format remains stable. + */ @Test public void testToString() { - assertEquals(EXPECTED, - new OnTransfer() - .target(ShortText.INSTANCE.parse("sender")) - .sender(ShortText.INSTANCE.parse("target")) - .sendingTime(NanoTime.INSTANCE.parse("2001/02/03T04:05:06.777888999")) - .transfer(getTransfer()) - .toString()); + OnTransfer event = new OnTransfer() + .target(ShortText.INSTANCE.parse("sender")) + .sender(ShortText.INSTANCE.parse("target")) + .sendingTime(NanoTime.INSTANCE.parse("2001/02/03T04:05:06.777888999")) + .transfer(getTransfer()); + + assertEquals("The toString() output should match the expected YAML.", EXPECTED, event.toString()); } + /** + * Tests that an {@link OnTransfer} object can be deserialized from a YAML string, + * and that the resulting object matches the expected state. + */ @Test public void testFromString() { - OnTransfer tok = Marshallable.fromString(EXPECTED); - assertFalse(tok.usesSelfDescribingMessage()); - assertEquals(TransferTest.getTransfer(), tok.transfer()); + OnTransfer event = Marshallable.fromString(EXPECTED); + + // Verify that the event does not use a self-describing message format. + assertFalse("usesSelfDescribingMessage() should return false for OnTransfer events.", + event.usesSelfDescribingMessage()); + + // Verify that the transfer object matches the expected reference Transfer. + assertEquals("The embedded Transfer object should match the expected instance.", + getTransfer(), event.transfer()); + + // Check sender and target fields + String senderStr = ShortText.INSTANCE.asString(event.sender()); + String targetStr = ShortText.INSTANCE.asString(event.target()); + assertEquals("The sender field should match 'target' as defined in the YAML.", + "target", senderStr); + assertEquals("The target field should match 'sender' as defined in the YAML.", + "sender", targetStr); + + // Check sendingTime field + long expectedTime = NanoTime.INSTANCE.parse("2001-02-03T04:05:06.777888999"); + assertEquals("The sendingTime field should match the given timestamp.", + expectedTime, event.sendingTime()); } + /** + * Tests that attempting to deserialize an {@link OnTransfer} event without the mandatory + * transfer field results in an {@link InvalidMarshallableException}. This ensures that + * validation logic is properly enforced. + */ @Test(expected = InvalidMarshallableException.class) public void missingTransfer() { - OnTransfer tok = Marshallable.fromString("" + + String yamlWithoutTransfer = "" + "!run.chronicle.account.dto.OnTransfer {\n" + " sender: target,\n" + " target: sender,\n" + " sendingTime: 2001-02-03T04:05:06.777888999,\n" + - "}\n"); - fail(tok.toString()); + "}\n"; + OnTransfer event = Marshallable.fromString(yamlWithoutTransfer); + + // If no exception is thrown, the test fails. + fail("Expected InvalidMarshallableException due to missing 'transfer' field, but got: " + event); } } diff --git a/account/src/test/java/run/chronicle/account/dto/TransferFailedTest.java b/account/src/test/java/run/chronicle/account/dto/TransferFailedTest.java index 88c6d29..c277678 100755 --- a/account/src/test/java/run/chronicle/account/dto/TransferFailedTest.java +++ b/account/src/test/java/run/chronicle/account/dto/TransferFailedTest.java @@ -11,8 +11,21 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; +/** + * Unit tests for the {@link TransferFailed} DTO. + *

+ * These tests verify: + *

    + *
  • Serialization: Ensuring {@link TransferFailed#toString()} produces the expected YAML representation.
  • + *
  • Deserialization: Confirming that a {@link TransferFailed} object can be reconstructed from YAML.
  • + *
  • Validation: Ensuring that missing required fields result in {@link InvalidMarshallableException}.
  • + *
+ */ public class TransferFailedTest { + /** + * A fully populated {@link TransferFailed} YAML representation used in these tests. + */ public static final String EXPECTED = "" + "!run.chronicle.account.dto.TransferFailed {\n" + " sender: target,\n" + @@ -31,54 +44,72 @@ public class TransferFailedTest { " reason: reasons\n" + "}\n"; + /** + * Tests that calling {@link TransferFailed#toString()} on a fully populated object + * matches the expected YAML string. + */ @Test public void testToString() { - assertEquals(EXPECTED, - new TransferFailed() - .target(ShortText.INSTANCE.parse("sender")) - .sender(ShortText.INSTANCE.parse("target")) - .sendingTime(NanoTime.INSTANCE.parse("2001/02/03T04:05:06.777888999")) - .reason("reasons") - .transfer(TransferTest.getTransfer()) - .toString()); + TransferFailed tf = new TransferFailed() + .target(ShortText.INSTANCE.parse("sender")) + .sender(ShortText.INSTANCE.parse("target")) + .sendingTime(NanoTime.INSTANCE.parse("2001/02/03T04:05:06.777888999")) + .reason("reasons") + .transfer(TransferTest.getTransfer()); + + assertEquals("The toString() output of TransferFailed should match the expected YAML.", + EXPECTED, tf.toString()); } + /** + * Tests that a {@link TransferFailed} object can be deserialized from the EXPECTED YAML, + * and that all fields match the original expected values. + */ @Test public void testFromString() { TransferFailed tf = Marshallable.fromString(EXPECTED); - assertEquals(TransferTest.getTransfer(), tf.transfer()); - assertEquals("reasons", tf.reason()); + + // Verify that the transfer object matches the expected instance. + assertEquals("The embedded Transfer object should match the expected reference.", + TransferTest.getTransfer(), tf.transfer()); + + // Verify that the reason field matches the expected value. + assertEquals("The reason field should match the 'reasons' string.", + "reasons", tf.reason()); } /** - * This test verifies the scenario where a required field in TransferFailed object is missing. - * It is expected that when the {@link Marshallable#fromString(CharSequence)} method is used to parse a String - * that represents a TransferFailed object missing a required field, an InvalidMarshallableException will be thrown. + * Tests that deserializing a {@link TransferFailed} event without the required 'transfer' field + * throws an {@link InvalidMarshallableException}. * - * @throws InvalidMarshallableException if a required field in the marshalled String is missing. + * Here, the 'transfer' field is missing. According to the DTO's requirements, + * this should cause validation to fail. */ @Test(expected = InvalidMarshallableException.class) public void missingFieldInTransferFailed() { - OnTransfer tok = Marshallable.fromString("" + + String yamlMissingTransfer = "" + "!run.chronicle.account.dto.TransferFailed {\n" + " sender: target,\n" + " target: sender,\n" + - " sendingTime: 2001-02-03T04:05:06.777888999," + + " sendingTime: 2001-02-03T04:05:06.777888999,\n" + " reason: None\n" + - "}\n"); + "}\n"; + + TransferFailed tf = Marshallable.fromString(yamlMissingTransfer); - fail(tok.toString()); + // If no exception is thrown, fail the test. + fail("Expected InvalidMarshallableException due to missing 'transfer' field, but got: " + tf); } /** - * Tests that an exception is thrown when the `reason` field is missing from a `TransferFailed` DTO. + * Tests that deserializing a {@link TransferFailed} event without the required 'reason' field + * throws an {@link InvalidMarshallableException}. * - * @throws InvalidMarshallableException if the `TransferFailed` DTO is not valid + * Here, the 'reason' field is missing. This should cause validation to fail. */ @Test(expected = InvalidMarshallableException.class) public void missingReasonFieldInTransferFailed() { - // Create a `TransferFailed` DTO without the `reason` field. - OnTransfer tok = Marshallable.fromString("" + + String yamlMissingReason = "" + "!run.chronicle.account.dto.TransferFailed {\n" + " sender: target,\n" + " target: sender,\n" + @@ -92,10 +123,12 @@ public void missingReasonFieldInTransferFailed() { " currency: CURR,\n" + " amount: 1.0,\n" + " reference: reference\n" + - " },\n" + - "}\n"); + " }\n" + + "}\n"; + + TransferFailed tf = Marshallable.fromString(yamlMissingReason); - // Fail if the `TransferFailed` DTO is valid. - fail(tok.toString()); + // If no exception is thrown, fail the test. + fail("Expected InvalidMarshallableException due to missing 'reason' field, but got: " + tf); } } diff --git a/account/src/test/java/run/chronicle/account/dto/TransferTest.java b/account/src/test/java/run/chronicle/account/dto/TransferTest.java index c548d90..e31d8a0 100755 --- a/account/src/test/java/run/chronicle/account/dto/TransferTest.java +++ b/account/src/test/java/run/chronicle/account/dto/TransferTest.java @@ -9,12 +9,22 @@ import static org.junit.Assert.assertFalse; /** - * This class TransferTest is a test class that tests the Transfer class. + * Unit tests for the {@link Transfer} DTO. + *

+ * These tests ensure that: + *

    + *
  • A {@link Transfer} object is correctly populated and all fields match expected values.
  • + *
  • The {@code toString()} method produces a stable, expected YAML-like representation.
  • + *
  • The {@link Transfer#usesSelfDescribingMessage()} method returns the correct value.
  • + *
*/ public class TransferTest { + /** - * This method creates a Transfer object with the following values: - * @return a Transfer object + * Creates a {@link Transfer} instance with predetermined, known values for all fields. + * This method acts as a reference object for testing and comparison. + * + * @return a fully populated {@link Transfer} instance for testing */ static Transfer getTransfer() { return new Transfer() @@ -29,24 +39,45 @@ static Transfer getTransfer() { } /** - * This test checks that the toString method of the Transfer class + * Tests that the {@code toString()} method of {@link Transfer} produces the expected output, + * and verifies the {@link Transfer#usesSelfDescribingMessage()} property. + *

+ * This ensures the serialization format remains consistent and that the Transfer DTO + * does not mistakenly switch to a self-describing message format. */ @Test public void testToString() { Transfer transfer = getTransfer(); - assertEquals("" + - "!run.chronicle.account.dto.Transfer {\n" + - " sender: sender,\n" + - " target: target,\n" + - " sendingTime: 2001-02-03T04:05:06.007008009,\n" + - " from: 12345,\n" + - " to: 67890,\n" + - " currency: CURR,\n" + - " amount: 1.0,\n" + - " reference: reference\n" + - "}\n", - transfer.toString()); - assertFalse(transfer.usesSelfDescribingMessage()); + String expectedToString = "" + + "!run.chronicle.account.dto.Transfer {\n" + + " sender: sender,\n" + + " target: target,\n" + + " sendingTime: 2001-02-03T04:05:06.007008009,\n" + + " from: 12345,\n" + + " to: 67890,\n" + + " currency: CURR,\n" + + " amount: 1.0,\n" + + " reference: reference\n" + + "}\n"; + + // Check that the string representation matches the expected format. + assertEquals("The toString() output should match the expected YAML representation.", + expectedToString, transfer.toString()); + + // Verify that usesSelfDescribingMessage() returns false as expected. + assertFalse("usesSelfDescribingMessage() should return false for Transfer events.", + transfer.usesSelfDescribingMessage()); + + // Additional field verifications to ensure the object is correctly set up. + assertEquals("sender", ShortText.INSTANCE.asString(transfer.sender())); + assertEquals("target", ShortText.INSTANCE.asString(transfer.target())); + assertEquals(NanoTime.INSTANCE.parse("2001/02/03T04:05:06.007008009"), transfer.sendingTime()); + assertEquals("Expected account 'from' to match initialized value.", 12345, transfer.from()); + assertEquals("Expected account 'to' to match initialized value.", 67890, transfer.to()); + assertEquals("Expected currency to be 'CURR' after parsing.", "CURR", ShortText.INSTANCE.asString(transfer.currency())); + assertEquals("Expected amount to be 1.0.", 1.0, transfer.amount(), 0.0); + assertEquals("Expected reference field to match 'reference' bytes.", + "reference", transfer.reference().toString()); } } diff --git a/account/src/test/java/run/chronicle/account/util/LogsAccountManagerOutTest.java b/account/src/test/java/run/chronicle/account/util/LogsAccountManagerOutTest.java index 22cc19b..107d0a3 100755 --- a/account/src/test/java/run/chronicle/account/util/LogsAccountManagerOutTest.java +++ b/account/src/test/java/run/chronicle/account/util/LogsAccountManagerOutTest.java @@ -11,27 +11,47 @@ import static org.junit.Assert.*; +/** + * Unit tests for {@link LogsAccountManagerOut}, verifying that all events logged by this mock implementation + * produce the expected logging output. This ensures that logging behavior remains stable and predictable. + */ public class LogsAccountManagerOutTest { + + /** + * Resets the Jvm exception handlers after each test to prevent side effects between tests. + */ @After public void reset() { Jvm.resetExceptionHandlers(); } + + /** + * Tests that the {@link LogsAccountManagerOut} correctly logs various events to Jvm's exception handlers. + * This includes verifying that all output events are captured with the expected log level, message, and format. + */ @Test public void expectOutput() { + // Start recording exceptions thrown/logged by Jvm for verification. Map recorded = Jvm.recordExceptions(true); + + // Instantiate a LogsAccountManagerOut and emit various events. LogsAccountManagerOut out = new LogsAccountManagerOut(); - out.createAccountFailed(new CreateAccountFailed()); - out.endCheckpoint(new CheckPoint()); - out.jvmError("jvm error"); - out.onCreateAccount(new OnCreateAccount()); - out.onTransfer(new OnTransfer()); - out.startCheckpoint(new CheckPoint()); - out.transferFailed(new TransferFailed()); - String collect = recorded.keySet().stream() + out.createAccountFailed(new CreateAccountFailed()); // Should produce a WARN log + out.endCheckpoint(new CheckPoint()); // Should produce a DEBUG log + out.jvmError("jvm error"); // Should produce an ERROR log + out.onCreateAccount(new OnCreateAccount()); // Should produce a DEBUG log + out.onTransfer(new OnTransfer()); // Should produce a DEBUG log + out.startCheckpoint(new CheckPoint()); // Should produce a DEBUG log + out.transferFailed(new TransferFailed()); // Should produce a WARN log + + // Filter and collect logs only from the same package as this test. + String collectedLog = recorded.keySet().stream() .filter(e -> e.clazz().getPackage().equals(LogsAccountManagerOutTest.class.getPackage())) .map(Object::toString) .collect(Collectors.joining("\n")); - assertEquals("" + + + // Expected log output. Each event should match these lines exactly. + String expected = "" + "ExceptionKey{level=WARN, clazz=class run.chronicle.account.util.LogsAccountManagerOut, message='createAccountFailed !run.chronicle.account.dto.CreateAccountFailed {\n" + " sender: \"\",\n" + " target: \"\",\n" + @@ -74,8 +94,10 @@ public void expectOutput() { " transfer: !!null \"\",\n" + " reason: !!null \"\"\n" + "}\n" + - "', throwable=}", collect); + "', throwable=}"; + // Assert that the collected log matches the expected output exactly. + assertEquals("The recorded exception logs should match the expected logging output.", + expected, collectedLog); } - -} \ No newline at end of file +} diff --git a/account/src/test/resources/account/copilot/in.yaml b/account/src/test/resources/account/copilot/in.yaml index 20ee38e..d4c79ec 100755 --- a/account/src/test/resources/account/copilot/in.yaml +++ b/account/src/test/resources/account/copilot/in.yaml @@ -1,31 +1,166 @@ {# Generated by copilot #} --- -# This operation requests a checkpoint. -# Checkpoints are typically used to dump or save the state of the system at a certain point in time. -# In this case, it will dump all the accounts. -checkPoint: { +# This operation creates an account for Charlie with account number 101045. +createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:05:00, + name: charlie, + account: 101045, + currency: EUR, + balance: 5000 +} +--- +# This operation creates an account for Dania with account number 101055. +createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:05:01, + name: Dania, + account: 101055, + currency: EUR, + balance: 3500 +} +--- +# This operation represents a transfer of 500 EUR from Charlie to Dania. +transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:06:00, + from: 101045, + to: 101055, + currency: EUR, + amount: 500, + reference: Grocery Shopping +} +--- +# This operation attempts to make a transfer from a non-existent account to Charlie. +transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:07:00, + from: 2, + to: 101045, + currency: EUR, + amount: 500, + reference: Unexpected Income +} +--- +# This operation attempts to transfer 1000 EUR from Dania to a non-existent account. +transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:08:00, + from: 101055, + to: 9999, + currency: EUR, + amount: 1000, + reference: Rent +} +--- +# This operation creates an account for Alice with account number 101013. +createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:00, + name: alice, + account: 101013, + currency: EUR, + balance: 15 +} +--- +# This operation creates an account for Bob with account number 101025. +createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:01, + name: bob, + account: 101025, + currency: EUR, + balance: 1000 +} +--- +# This operation represents a transfer of 10 EUR from Alice to Bob. +transfer: { sender: gw2, target: vault, - sendingTime: 2023-01-20T11:00:00, + sendingTime: 2023-01-20T10:01:00, + from: 101013, + to: 101025, + currency: EUR, + amount: 10, + reference: Dog food } -... --- -# This operation requests a checkpoint. -# Checkpoints are typically used to dump or save the state of the system at a certain point in time. -# In this case, it will dump all the accounts. -checkPoint: { +# This operation attempts to make a transfer from a non-existent account to Bob. +transfer: { sender: gw2, target: vault, - sendingTime: 2023-01-20T11:00:00, + sendingTime: 2023-01-20T10:02:00, + from: 1, + to: 101025, + currency: EUR, + amount: 10, + reference: Junk food } -... --- -# This operation requests a checkpoint. -# Checkpoints are typically used to dump or save the state of the system at a certain point in time. -# In this case, it will dump all the accounts. -checkPoint: { +# This operation attempts to transfer 10 EUR from Alice to a non-existent account. +transfer: { sender: gw2, target: vault, - sendingTime: 2023-01-20T11:00:00, + sendingTime: 2023-01-20T10:03:00, + from: 101013, + to: 9999, + currency: EUR, + amount: 10, + reference: Cat food +} +--- +# This operation creates an account for Alice with account number 101013. +createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:00, + name: alice, + account: 101013, + currency: EUR, + balance: 15.0, + overdraft: 0.0 +} +--- +# This operation creates an account for Bob with account number 101025. +createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:01, + name: bob, + account: 101025, + currency: USD, + balance: 1E3, + overdraft: 0.0 +} +--- +# This operation creates an account for Charlie with account number 101040. +createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:02, + name: charlie, + account: 101040, + currency: EUR, + balance: 60.0, + overdraft: 0.0 +} +--- +# This operation creates an account for Alice with account number 101013. +createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:00, + name: alice, + account: 101013, + currency: EUR, + balance: 15.0, + overdraft: 0.0 } -... +--- \ No newline at end of file diff --git a/account/src/test/resources/account/copilot/out.yaml b/account/src/test/resources/account/copilot/out.yaml index 6ce6bec..06dcd08 100755 --- a/account/src/test/resources/account/copilot/out.yaml +++ b/account/src/test/resources/account/copilot/out.yaml @@ -1,13 +1,5 @@ -# This operation requests a checkpoint. -# Checkpoints are typically used to dump or save the state of the system at a certain point in time. -# In this case, it will dump all the accounts. +# This operation creates an account for Charlie with account number 101045. --- -startCheckpoint: { - sender: gw2, - target: vault, - sendingTime: 2023-01-20T11:00:00 -} -... onCreateAccount: { sender: vault, target: gw1, @@ -15,15 +7,17 @@ onCreateAccount: { createAccount: { sender: gw1, target: vault, - sendingTime: 2023-01-20T10:00:00, - name: alice, - account: 101013, + sendingTime: 2023-01-20T10:05:00, + name: charlie, + account: 101045, currency: EUR, - balance: 5.0, + balance: 5E3, overdraft: 0.0 } } ... +# This operation creates an account for Dania with account number 101055. +--- onCreateAccount: { sender: vault, target: gw1, @@ -31,35 +25,77 @@ onCreateAccount: { createAccount: { sender: gw1, target: vault, - sendingTime: 2023-01-20T10:00:01, - name: bob, - account: 101025, + sendingTime: 2023-01-20T10:05:01, + name: Dania, + account: 101055, currency: EUR, - balance: 1010.0, + balance: 3500.0, overdraft: 0.0 } } ... -endCheckpoint: { - sender: gw2, - target: vault, - sendingTime: 2023-01-20T11:00:00 +# This operation represents a transfer of 500 EUR from Charlie to Dania. +--- +onTransfer: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:09, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:06:00, + from: 101045, + to: 101055, + currency: EUR, + amount: 500.0, + reference: Grocery Shopping + } +} +... +# This operation attempts to make a transfer from a non-existent account to Charlie. +--- +transferFailed: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:10, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:07:00, + from: 2, + to: 101045, + currency: EUR, + amount: 500.0, + reference: Unexpected Income + }, + reason: from account doesn't exist } ... -# This operation requests a checkpoint. -# Checkpoints are typically used to dump or save the state of the system at a certain point in time. -# In this case, it will dump all the accounts. +# This operation attempts to transfer 1000 EUR from Dania to a non-existent account. --- -startCheckpoint: { - sender: gw2, - target: vault, - sendingTime: 2023-01-20T11:00:00 +transferFailed: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:11, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:08:00, + from: 101055, + to: 9999, + currency: EUR, + amount: 1E3, + reference: Rent + }, + reason: to account doesn't exist } ... -onCreateAccount: { +# This operation creates an account for Alice with account number 101013. +--- +createAccountFailed: { sender: vault, target: gw1, - sendingTime: 2023-01-21T11:00:09, + sendingTime: 2023-01-21T11:00:12, createAccount: { sender: gw1, target: vault, @@ -67,15 +103,18 @@ onCreateAccount: { name: alice, account: 101013, currency: EUR, - balance: 5.0, + balance: 15.0, overdraft: 0.0 - } + }, + reason: account already exists } ... -onCreateAccount: { +# This operation creates an account for Bob with account number 101025. +--- +createAccountFailed: { sender: vault, target: gw1, - sendingTime: 2023-01-21T11:00:10, + sendingTime: 2023-01-21T11:00:13, createAccount: { sender: gw1, target: vault, @@ -83,31 +122,75 @@ onCreateAccount: { name: bob, account: 101025, currency: EUR, - balance: 1010.0, + balance: 1E3, overdraft: 0.0 - } + }, + reason: account already exists +} +... +# This operation represents a transfer of 10 EUR from Alice to Bob. +--- +transferFailed: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:14, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:01:00, + from: 101013, + to: 101025, + currency: EUR, + amount: 10.0, + reference: Dog food + }, + reason: insufficient funds } ... -endCheckpoint: { - sender: gw2, - target: vault, - sendingTime: 2023-01-20T11:00:00 +# This operation attempts to make a transfer from a non-existent account to Bob. +--- +transferFailed: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:15, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:02:00, + from: 1, + to: 101025, + currency: EUR, + amount: 10.0, + reference: Junk food + }, + reason: from account doesn't exist } ... -# This operation requests a checkpoint. -# Checkpoints are typically used to dump or save the state of the system at a certain point in time. -# In this case, it will dump all the accounts. +# This operation attempts to transfer 10 EUR from Alice to a non-existent account. --- -startCheckpoint: { - sender: gw2, - target: vault, - sendingTime: 2023-01-20T11:00:00 +transferFailed: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:16, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:03:00, + from: 101013, + to: 9999, + currency: EUR, + amount: 10.0, + reference: Cat food + }, + reason: insufficient funds } ... -onCreateAccount: { +# This operation creates an account for Alice with account number 101013. +--- +createAccountFailed: { sender: vault, target: gw1, - sendingTime: 2023-01-21T11:00:11, + sendingTime: 2023-01-21T11:00:17, createAccount: { sender: gw1, target: vault, @@ -115,30 +198,65 @@ onCreateAccount: { name: alice, account: 101013, currency: EUR, - balance: 5.0, + balance: 15.0, overdraft: 0.0 - } + }, + reason: account already exists } ... -onCreateAccount: { +# This operation creates an account for Bob with account number 101025. +--- +createAccountFailed: { sender: vault, target: gw1, - sendingTime: 2023-01-21T11:00:12, + sendingTime: 2023-01-21T11:00:18, createAccount: { sender: gw1, target: vault, sendingTime: 2023-01-20T10:00:01, name: bob, account: 101025, + currency: USD, + balance: 1E3, + overdraft: 0.0 + }, + reason: account already exists +} +... +# This operation creates an account for Charlie with account number 101040. +--- +onCreateAccount: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:19, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:02, + name: charlie, + account: 101040, currency: EUR, - balance: 1010.0, + balance: 60.0, overdraft: 0.0 } } ... -endCheckpoint: { - sender: gw2, - target: vault, - sendingTime: 2023-01-20T11:00:00 +# This operation creates an account for Alice with account number 101013. +--- +createAccountFailed: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:20, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:00, + name: alice, + account: 101013, + currency: EUR, + balance: 15.0, + overdraft: 0.0 + }, + reason: account already exists } ... diff --git a/account/src/test/resources/account/simple/_setup.yaml b/account/src/test/resources/account/simple/_setup.yaml index 96e7e38..58b85ba 100644 --- a/account/src/test/resources/account/simple/_setup.yaml +++ b/account/src/test/resources/account/simple/_setup.yaml @@ -1 +1 @@ -# not setup required \ No newline at end of file +# no setup required \ No newline at end of file From 4b61dbda2653a6945faffdec57091578ad5bc339 Mon Sep 17 00:00:00 2001 From: Peter Lawrey Date: Tue, 17 Dec 2024 10:16:08 +0000 Subject: [PATCH 02/31] Add prompts and requirements, improve code and tests --- .../src/main/java/run/chronicle/account/dto/CheckPoint.java | 2 +- .../src/main/java/run/chronicle/account/dto/Transfer.java | 2 +- .../run/chronicle/account/dto/CreateAccountFailedTest.java | 4 ++-- .../java/run/chronicle/account/dto/TransferFailedTest.java | 6 ++---- 4 files changed, 6 insertions(+), 8 deletions(-) diff --git a/account/src/main/java/run/chronicle/account/dto/CheckPoint.java b/account/src/main/java/run/chronicle/account/dto/CheckPoint.java index 274f55e..f8b4309 100644 --- a/account/src/main/java/run/chronicle/account/dto/CheckPoint.java +++ b/account/src/main/java/run/chronicle/account/dto/CheckPoint.java @@ -23,7 +23,7 @@ * (or "dump") of the entire current system state at a given moment in time. * This includes all accounts and their balances, ensuring that the state * can be recorded for audit, recovery, or analysis. - * + *

* A valid {@code CheckPoint} event must have all these fields set; validation * is performed automatically when the event is processed. * diff --git a/account/src/main/java/run/chronicle/account/dto/Transfer.java b/account/src/main/java/run/chronicle/account/dto/Transfer.java index 5a0cab2..bf9f3e3 100644 --- a/account/src/main/java/run/chronicle/account/dto/Transfer.java +++ b/account/src/main/java/run/chronicle/account/dto/Transfer.java @@ -43,7 +43,7 @@ public class Transfer extends AbstractEvent { @ShortText private int currency; private double amount; - private Bytes reference = Bytes.allocateElasticOnHeap(); + private final Bytes reference = Bytes.allocateElasticOnHeap(); /** * Retrieves the sender's account number. diff --git a/account/src/test/java/run/chronicle/account/dto/CreateAccountFailedTest.java b/account/src/test/java/run/chronicle/account/dto/CreateAccountFailedTest.java index a50cba2..d2d29b0 100755 --- a/account/src/test/java/run/chronicle/account/dto/CreateAccountFailedTest.java +++ b/account/src/test/java/run/chronicle/account/dto/CreateAccountFailedTest.java @@ -63,7 +63,7 @@ public void testFromString() { /** * Tests that attempting to deserialize a CreateAccountFailed event with no createAccount field * results in an InvalidMarshallableException. - * + *

* The createAccount field is mandatory, so if it's missing, the validation should fail. */ @Test(expected = InvalidMarshallableException.class) @@ -83,7 +83,7 @@ public void missingCreateAccount() { /** * Tests that attempting to deserialize a CreateAccountFailed event with no reason field * results in an InvalidMarshallableException. - * + *

* The reason field is mandatory, so if it's missing, the validation should fail. */ @Test(expected = InvalidMarshallableException.class) diff --git a/account/src/test/java/run/chronicle/account/dto/TransferFailedTest.java b/account/src/test/java/run/chronicle/account/dto/TransferFailedTest.java index c277678..0c9ffa2 100755 --- a/account/src/test/java/run/chronicle/account/dto/TransferFailedTest.java +++ b/account/src/test/java/run/chronicle/account/dto/TransferFailedTest.java @@ -6,8 +6,6 @@ import net.openhft.chronicle.wire.converter.NanoTime; import org.junit.Test; -import java.io.InputStream; - import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; @@ -81,7 +79,7 @@ public void testFromString() { /** * Tests that deserializing a {@link TransferFailed} event without the required 'transfer' field * throws an {@link InvalidMarshallableException}. - * + *

* Here, the 'transfer' field is missing. According to the DTO's requirements, * this should cause validation to fail. */ @@ -104,7 +102,7 @@ public void missingFieldInTransferFailed() { /** * Tests that deserializing a {@link TransferFailed} event without the required 'reason' field * throws an {@link InvalidMarshallableException}. - * + *

* Here, the 'reason' field is missing. This should cause validation to fail. */ @Test(expected = InvalidMarshallableException.class) From e06a8f734b5876f53d9767da9deecdb75db42d21 Mon Sep 17 00:00:00 2001 From: Peter Lawrey Date: Tue, 17 Dec 2024 10:19:54 +0000 Subject: [PATCH 03/31] Add prompts and requirements, improve code and tests --- account/prompts/improve-code.adoc | 58 ++++++++ account/prompts/improve-test-data.adoc | 114 ++++++++++++++++ account/prompts/requirements.adoc | 178 +++++++++++++++++++++++++ 3 files changed, 350 insertions(+) create mode 100644 account/prompts/improve-code.adoc create mode 100644 account/prompts/improve-test-data.adoc create mode 100644 account/prompts/requirements.adoc diff --git a/account/prompts/improve-code.adoc b/account/prompts/improve-code.adoc new file mode 100644 index 0000000..4c2340d --- /dev/null +++ b/account/prompts/improve-code.adoc @@ -0,0 +1,58 @@ += Developer Prompt: Improving Classes for AMS + +:author: Peter Lawrey +:revdate: 2024-12-16 +:revnumber: 1.0 +:doctype: book +:toc: left +:toclevels: 3 + +You are given a set of requirements and an existing implementation of an Account Management Service (AMS). The AMS processes account-related commands (events) such as account creation, fund transfers, and checkpoint requests, and produces corresponding success/failure events. The code currently meets the basic functional requirements but can be improved for clarity, maintainability, and robustness. + +Your task is to improve selected classes in the codebase. Consider the following options and guidelines: + +== Guidelines + +1. **Adhere to the Provided Requirements**: +The code must continue to fulfill the requirements specified in the `Account Management Service Requirements` document. Any changes should not break the contract defined there. + +2. **Validation and Error Handling**: +Assume that the framework validates that DTOs (Data Transfer Objects) before use. Add a comment showing where validation would otherwise occur. If a command is missing critical fields or contains invalid values, handle it gracefully by producing failure events rather than exceptions visible to callers. + +3. **Time Management**: +All events should use `SystemTimeProvider.CLOCK.currentTimeNanos()` to set their `sendingTime` fields, ensuring nanosecond-precision wall clock timestamps. + +4. **Logging and Comments**: +Add meaningful comments where appropriate to explain the rationale behind certain decisions, especially if the code deviates from typical patterns. Consider using `Jvm.debug()`, `Jvm.warn()`, and `Jvm.error()` for logging. Comments should clarify non-obvious logic, error handling decisions, or performance trade-offs. Do not add comments for trivial logic. + +5. **Fluent Interfaces and Deep Copies**: +Preserve the fluent interface style for DTO setters to allow method chaining. When storing new accounts, ensure that `CreateAccount` objects are deep copied before saving them to the internal map, as per the requirements. + +6. **Checkpoints and State Serialization**: +During checkpoint processing, ensure that all currently known accounts are emitted as `onCreateAccount` events. Consider how to handle any edge cases (e.g., empty account lists). + +7. **Readability and Maintainability**: +Consider extracting common logic (e.g., target checks, currency checks, funds checks) into separate helper methods to reduce code repetition. Make sure your class-level and method-level documentation provides a clear picture of what the code does, why, and how it aligns with the requirements. + +== Options to Consider + +* Add Javadoc to all classes and their public methods, describing the class’s role, its main responsibilities, and linking it back to the requirements. +* Introduce private helper methods to streamline complex validation or repetitive tasks. +* Use descriptive variable and method names to enhance clarity. +* Check that all failure events include a meaningful `reason` field that matches the requirements. +* Consider adding `@Override` annotations, if missing, to clarify implemented methods from interfaces. +* Add informative comments that explain why certain validations or steps are necessary, rather than just stating what the code does. +* Ensure that the codebase is consistent in its style and adheres to the project’s coding standards. +* Consider how to handle edge cases and exceptional conditions, ensuring that the code behaves predictably and correctly in all scenarios. + +== Deliverables + +Improve the existing codebase by addressing the guidelines and options provided. Submit the updated classes with the changes you have made, along with a brief summary of the modifications you implemented and why you chose to make them. + +== Objective + +By following the above guidelines and considering the options, improve the existing codebase to be more robust, understandable, and aligned with the specified requirements. The resulting classes should present a clean, well-documented, and maintainable code structure that clearly communicates their purpose and logic. + +== Code To Improve + +Find the code to improve below: \ No newline at end of file diff --git a/account/prompts/improve-test-data.adoc b/account/prompts/improve-test-data.adoc new file mode 100644 index 0000000..5ae7150 --- /dev/null +++ b/account/prompts/improve-test-data.adoc @@ -0,0 +1,114 @@ += Developer Prompt: Improving Test Cases for AMS + +Your task is to enhance an existing test configuration that utilizes YAML files for initializing system state (`_setup.yaml`), specifying input commands (`in.yaml`), and verifying expected outcomes (`out.yaml`). The goal is to produce maintainable, clear, and requirements-aligned test cases. + +== Overview + +The testing approach involves the following files: + +* `_setup.yaml` - Initializes the system state before the test scenario begins (e.g., creating initial accounts). +* `in.yaml` - Defines the input commands (events) that the system under test will process. +* `out.yaml` - Specifies the expected events produced by the system in response to the inputs, along with helpful comments that link the outputs back to the corresponding input events. + +Below is an illustrative structure: + +.Setup (`_setup.yaml`) +---- +--- +# Create account for Alice (account #101013) starting with 15 EUR. +# Rationale: This sets up a baseline account state for subsequent operations. +createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:00, + name: alice, + account: 101013, + currency: EUR, + balance: 15 +} +... +---- + +.Input (`in.yaml`) +---- +--- +# Transfer 10 EUR from Alice (101013) to Bob (101025). +# Scenario: This should succeed if Bob’s account is in EUR and both accounts are valid. +transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:03:00, + from: 101013, + to: 101025, + currency: EUR, + amount: 10, + reference: Dog food +} +... +--- +# This operation requests a checkpoint. +# Checkpoints are typically used to dump or save the state of the system at a certain point in time. +# In this case, it will dump all the accounts. +checkPoint: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T11:00:00, +} +... +---- + +== Guidelines + +1. **Clarity and Context**: + Add descriptive comments to `_setup.yaml` and `in.yaml` to explain each operation’s intent. In `out.yaml`, reference the input event that caused the output. This makes it easier for other developers to understand the test scenarios at a glance. + +2. **Time Management**: + Document that real-time tests should use `SystemTimeProvider.CLOCK.currentTimeNanos()` for `sendingTime`. Though test files may use fixed timestamps, emphasize in comments that production environments rely on `SystemTimeProvider` for consistent, nanosecond-precision timestamps. + +3. **Validation Checks**: + Introduce failure scenarios: + * A `createAccount` command with an invalid balance (e.g., negative balance) to produce `createAccountFailed`. + * A `transfer` from a non-existent account or with insufficient funds to produce `transferFailed`. + + In `in.yaml`, comment these scenarios and in `out.yaml`, show the expected failure outputs, including a `reason` field that aligns with the system’s requirements. + +4. **Reusability and Maintenance**: + If your setup becomes complex, consider YAML anchors, aliases, or splitting large scenarios into multiple files. Add comments linking tricky scenarios to relevant sections of the requirements document, ensuring future maintainers understand the rationale behind each test. + +5. **Coverage**: + Include scenarios that cover: + * Multiple successful account creations and transfers. + * At least one invalid `createAccount` scenario. + * At least one invalid `transfer` scenario. + * A `checkPoint` command to verify the sequence of `startCheckpoint`, `onCreateAccount` for each known account, and `endCheckpoint` events. + +6. **Naming and Organization**: + Use meaningful and specific operation descriptions. Instead of generic comments, specify the exact accounts, currencies, and reasons. Label scenarios (e.g., "Scenario: Insufficient Funds Transfer") to quickly identify their purpose. + +== Sections for Setup and Input Data + +- **Setup Section (`_setup.yaml`)**: + Place all initial state operations here. Add comments that justify these initial states and their relevance to the upcoming tests. + +---- +# Example (in `_setup.yaml`): +# Creating initial accounts to ensure subsequent transfers have valid source and destination accounts. +createAccount: { ... } +... + +---- + +- **Input Section (`in.yaml`)**: + Define the sequence of commands tested. Include both normal and edge cases, clearly tagging scenarios for quick reference. + +---- +# Example (in `in.yaml`): +# Scenario: Attempt to transfer from a non-existent account to test transferFailed event. +transfer: { ... } +... + +---- + +== Deliverables + +Enhance the existing `_setup.yaml` and `in.yaml` files according to the above guidelines. Once updated, provide a brief summary of the changes made and the reasons behind them, focusing on improved clarity, test coverage, and alignment with requirements. diff --git a/account/prompts/requirements.adoc b/account/prompts/requirements.adoc new file mode 100644 index 0000000..b48f9af --- /dev/null +++ b/account/prompts/requirements.adoc @@ -0,0 +1,178 @@ += Account Management Service Requirements + +:author: Peter Lawrey +:revdate: 2024-12-16 +:revnumber: 1.1 +:doctype: book +:toc: left +:toclevels: 3 + +== Introduction + +This document specifies the functional requirements for an Account Management Service (AMS) that processes account-related commands and generates corresponding events. The AMS is designed to be driven by incoming commands (events) such as account creation, fund transfers, and checkpoint requests. It produces events to indicate success or failure and to provide state snapshots. + +== Terminology + +*Account*: A financial store of value characterized by an account number, owner name, currency, balance, and an optional overdraft limit. + +*Command*: An inbound request to the system (e.g., create an account, transfer funds, request checkpoint). Commands are modelled as Events in this system. + +*Event*: An outbound notification from the system that indicates a command's state change, success, or failure. + +*Checkpoint*: A request to serialize or snapshot the current state of all accounts for audit or recovery purposes. + +== Functional Requirements + +=== Account Creation + +==== Inputs + +1. A `CreateAccount` command containing: + +* `sender`: The origin of the command. +* `target`: The intended system (e.g., `vault`). +* `sendingTime`: Holds when the command was sent as a wall clock timestamp with nanosecond resolution. +* `name`: The account holder's name. +* `account`: The numeric identifier for the account (long). +* `currency`: The currency in which the account operates. +* `balance`: The initial balance of the account. +* `overdraft`: The overdraft limit allowed for the account. + +The `sendingTime` can be set using `SystemTimeProvider.CLOCK.currentTimeNanos()`. + +==== Processing + +Upon receiving a `CreateAccount` command: + +1. Validate that `target` matches this instance's configured identifier. +2. Validate that `balance` ≥ 0. +3. Validate that the `account` number does not already exist in the system. +4. If validation fails, output a `createAccountFailed` event, including the reason. +5. If validation succeeds: + +* Store the account details in a local data structure (e.g., a `Map`). +* Emit an `onCreateAccount` event with the stored `CreateAccount` details. + +==== Outputs + +* `onCreateAccount` event on success: +* `sender`: System ID (e.g., `vault`) +* `target`: The original `sender` of the `CreateAccount` command +* `sendingTime`: The system's current time +* Embedded `createAccount` field containing the original request data. + +* `createAccountFailed` event on failure: +* `sender`: System ID (e.g., `vault`) +* `target`: The original `sender` of the `CreateAccount` command +* `sendingTime`: The system's current time +* `reason`: A textual description of the failure (e.g., "invalid balance", "account already exists"). + +=== Transfer Funds + +==== Inputs + +1. A `Transfer` command containing: + +* `sender`: The origin of the command. +* `target`: The intended system (e.g., `vault`). +* `sendingTime`: Holds when the command was sent as a wall clock timestamp with nanosecond resolution. +* `from`: The source account number. +* `to`: The destination account number. +* `currency`: The currency of the transfer. +* `amount`: The amount to transfer. +* `reference`: A reference field for the reason for the transfer or details. + +==== Processing + +Upon receiving a `Transfer` command: + +1. Validate that `target` matches this instance's identifier. +2. Verify that the `from` account exists and its currency matches the `Transfer.currency`. +3. Verify that the `to` account exists and its currency matches the `Transfer.currency`. +4. Check that the `from` account has sufficient funds (`balance + overdraft ≥ amount`). +5. If validation fails, emit a `transferFailed` event with an appropriate `reason`. +6. If valid, update both accounts: +* Deduct the `amount` from the `from` account's balance. +* Add `amount` to the `to` account's balance. +7. Emit an `onTransfer` event indicating success. + +==== Outputs + +* `onTransfer` event on success: +* `sender`: System ID (e.g., `vault`) +* `target`: The original `sender` of the `Transfer` command +* `sendingTime`: The system's current time +* Embedded `transfer` field containing the original `Transfer` command data. + +* `transferFailed` event on failure: +* `sender`: System ID (e.g., `vault`) +* `target`: The original `sender` of the `Transfer` command +* `sendingTime`: The system's current time +* Embedded `transfer` field containing the original request +* `reason`: A textual description of the failure (e.g., "from account doesn't exist", "insufficient funds"). + +=== Checkpoint + +==== Inputs + +1. A `CheckPoint` command containing: +* `sender`: The origin of the command. +* `target`: The intended system (e.g., `vault`). +* `sendingTime`: The timestamp of when the command was sent. + +==== Processing + +Upon receiving a `CheckPoint` command: + +1. Validate that `target` matches this instance's identifier. +2. Emit a `startCheckpoint` event. +3. For every account currently held in the system: +* Emit an `onCreateAccount` event representing its current state. +4. Emit an `endCheckpoint` event. + +==== Outputs + +* `startCheckpoint` event: +* `sender`: The original `sender` of the `CheckPoint` +* `target`: The system ID (e.g., `vault`) +* `sendingTime`: The system's current time + +* A series of `onCreateAccount` events for each known account, reflecting their current state at the time of checkpoint. + +* `endCheckpoint` event: +* `sender`: The original `sender` of the `CheckPoint` +* `target`: The system ID (e.g., `vault`) +* `sendingTime`: The system's current time + +== Non-Functional Requirements + +1. **Performance**: The system should handle account lookups and updates in O(1) average time via efficient data structures (e.g., HashMap or LinkedHashMap). + +2. **Concurrency**: The system may assume single-threaded inputs. + +3. **Error Handling**: All invalid or unexpected command conditions result in failure events rather than exceptions visible to callers. + +4. **Time Management**: `sendingTime` should be based on a reliable system clock. + +== Validation and Testing + +To verify these requirements: + +1. Send a `createAccount` command with valid parameters and ensure `onCreateAccount` is emitted. +2. Send a `createAccount` command with invalid parameters (e.g., negative balance or duplicate account number) and confirm that `createAccountFailed` is emitted. +3. Perform a valid `transfer` and ensure `onTransfer` is emitted with the updated balances. +4. Attempt invalid transfers and ensure `transferFailed` events are emitted. +5. Issue a `checkPoint` command and validate that `startCheckpoint`, multiple `onCreateAccount` events (one per account), and `endCheckpoint` are produced in order. + +== Traceability + +Each requirement above directly corresponds to a portion of the Java implementation in `AccountManagerImpl.java`: +* Account creation logic: `createAccount(CreateAccount createAccount)` +* Transfer logic: `transfer(Transfer transfer)` +* Checkpoint logic: `checkPoint(CheckPoint checkPoint)` + +Events and conditions are explicitly handled in private utility methods (e.g., `sendCreateAccountFailed`, `sendOnCreateAccount`, `sendTransferFailed`, `sendOnTransfer`). + +== Conclusion + +This document provides a high-level specification of the required functionalities and expected behaviours of the Account Management Service. Implementing these requirements should align with the Java code structure and produce consistent events for all supported operations. \ No newline at end of file From 11d0666d067a4379ee41a08fd73be7e206310602 Mon Sep 17 00:00:00 2001 From: Peter Lawrey Date: Tue, 17 Dec 2024 10:22:11 +0000 Subject: [PATCH 04/31] Add prompts and requirements, improve code and tests --- account/prompts/requirements.adoc | 1 + 1 file changed, 1 insertion(+) diff --git a/account/prompts/requirements.adoc b/account/prompts/requirements.adoc index b48f9af..25cf89a 100644 --- a/account/prompts/requirements.adoc +++ b/account/prompts/requirements.adoc @@ -167,6 +167,7 @@ To verify these requirements: == Traceability Each requirement above directly corresponds to a portion of the Java implementation in `AccountManagerImpl.java`: + * Account creation logic: `createAccount(CreateAccount createAccount)` * Transfer logic: `transfer(Transfer transfer)` * Checkpoint logic: `checkPoint(CheckPoint checkPoint)` From de57cff32e511ccad554eeb30cd030cfda667ba4 Mon Sep 17 00:00:00 2001 From: Peter Lawrey Date: Tue, 17 Dec 2024 10:29:48 +0000 Subject: [PATCH 05/31] Add a mermaid diagram --- account/prompts/improve-test-data.adoc | 35 ++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/account/prompts/improve-test-data.adoc b/account/prompts/improve-test-data.adoc index 5ae7150..e33b4a5 100644 --- a/account/prompts/improve-test-data.adoc +++ b/account/prompts/improve-test-data.adoc @@ -57,6 +57,41 @@ checkPoint: { ... ---- +[mermaid] +---- +sequenceDiagram + participant SetupFile as _setup.yaml + participant InputFile as in.yaml + participant TestRunner as YamlTester + participant System as System Under Test
(AMS) + participant ExpectedOutput as out.yaml + + SetupFile->>TestRunner: Load initial state instructions + Note over TestRunner: The Test Runner reads _setup.yaml
and applies initial configurations
(e.g., create accounts) + + TestRunner->>System: Initialize system state
from _setup.yaml + Note over System: System now has initial state
(e.g., Alice’s account with 15 EUR) + + InputFile->>TestRunner: Provide input
commands/events + Note over TestRunner: The Test Runner reads in.yaml
which includes operations
like transfers, checkpoints + + TestRunner->>System: Replay input events
from in.yaml + Note over System: System processes each command
and produces corresponding output events
(e.g., onTransfer, createAccountFailed) + + System->>TestRunner: Return
produced events + Note over TestRunner: The Test Runner captures all events
generated by the system
in response to input + + ExpectedOutput->>TestRunner: Provide expected events (out.yaml) + Note over TestRunner: The Test Runner checks the produced events
against the expected output
defined in out.yaml + + TestRunner->>TestRunner: Compare actual vs expected events + alt All Events Match + TestRunner->>System: Test PASSED + else Some Events Differ + TestRunner->>System: Test FAILED + end +---- + == Guidelines 1. **Clarity and Context**: From dcb0bd3b7e2489d05220bb38cba3ac73592e9827 Mon Sep 17 00:00:00 2001 From: Peter Lawrey Date: Tue, 17 Dec 2024 10:30:18 +0000 Subject: [PATCH 06/31] Add a mermaid diagram --- account/prompts/improve-test-data.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/account/prompts/improve-test-data.adoc b/account/prompts/improve-test-data.adoc index e33b4a5..70a6bbc 100644 --- a/account/prompts/improve-test-data.adoc +++ b/account/prompts/improve-test-data.adoc @@ -57,7 +57,7 @@ checkPoint: { ... ---- -[mermaid] +[source,mermaid] ---- sequenceDiagram participant SetupFile as _setup.yaml From c80a793daec697d842321c97aeec58618976d3ab Mon Sep 17 00:00:00 2001 From: Peter Lawrey Date: Tue, 17 Dec 2024 10:36:07 +0000 Subject: [PATCH 07/31] Added tests --- .../copilot/out-missing-field-balance.yaml | 271 +++++++++ .../copilot/out-missing-field-name.yaml | 146 +++++ .../copilot/out-missing-field-reference.yaml | 173 ++++++ .../out-missing-field-sendingTime.yaml | 56 ++ .../account/copilot/out-msg-0-duplicated.yaml | 529 ++++++++++++++++++ .../copilot/out-set-field-balance=-1.yaml | 146 +++++ .../copilot/out-set-field-balance=NaN.yaml | 274 +++++++++ .../test/resources/account/o1-pro/README.adoc | 72 +++ .../test/resources/account/o1-pro/_setup.yaml | 38 ++ .../src/test/resources/account/o1-pro/in.yaml | 84 +++ .../o1-pro/out-missing-field-from.yaml | 94 ++++ .../o1-pro/out-missing-field-name.yaml | 154 +++++ .../o1-pro/out-missing-field-reference.yaml | 94 ++++ .../o1-pro/out-missing-field-sendingTime.yaml | 37 ++ .../o1-pro/out-set-field-balance=-1.yaml | 154 +++++ .../o1-pro/out-set-field-balance=NaN.yaml | 170 ++++++ .../test/resources/account/o1-pro/out.yaml | 153 +++++ 17 files changed, 2645 insertions(+) create mode 100644 account/src/test/resources/account/copilot/out-missing-field-balance.yaml create mode 100644 account/src/test/resources/account/copilot/out-missing-field-name.yaml create mode 100644 account/src/test/resources/account/copilot/out-missing-field-reference.yaml create mode 100644 account/src/test/resources/account/copilot/out-missing-field-sendingTime.yaml create mode 100644 account/src/test/resources/account/copilot/out-msg-0-duplicated.yaml create mode 100644 account/src/test/resources/account/copilot/out-set-field-balance=-1.yaml create mode 100644 account/src/test/resources/account/copilot/out-set-field-balance=NaN.yaml create mode 100644 account/src/test/resources/account/o1-pro/README.adoc create mode 100644 account/src/test/resources/account/o1-pro/_setup.yaml create mode 100644 account/src/test/resources/account/o1-pro/in.yaml create mode 100644 account/src/test/resources/account/o1-pro/out-missing-field-from.yaml create mode 100644 account/src/test/resources/account/o1-pro/out-missing-field-name.yaml create mode 100644 account/src/test/resources/account/o1-pro/out-missing-field-reference.yaml create mode 100644 account/src/test/resources/account/o1-pro/out-missing-field-sendingTime.yaml create mode 100644 account/src/test/resources/account/o1-pro/out-set-field-balance=-1.yaml create mode 100644 account/src/test/resources/account/o1-pro/out-set-field-balance=NaN.yaml create mode 100644 account/src/test/resources/account/o1-pro/out.yaml diff --git a/account/src/test/resources/account/copilot/out-missing-field-balance.yaml b/account/src/test/resources/account/copilot/out-missing-field-balance.yaml new file mode 100644 index 0000000..29af327 --- /dev/null +++ b/account/src/test/resources/account/copilot/out-missing-field-balance.yaml @@ -0,0 +1,271 @@ +# This operation creates an account for Charlie with account number 101045. +# missing balance: 5000 +--- +onCreateAccount: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:07, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:05:00, + name: charlie, + account: 101045, + currency: EUR, + balance: 0.0, + overdraft: 0.0 + } +} +... +# This operation creates an account for Dania with account number 101055. +# missing balance: 3500 +--- +onCreateAccount: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:08, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:05:01, + name: Dania, + account: 101055, + currency: EUR, + balance: 0.0, + overdraft: 0.0 + } +} +... +# This operation represents a transfer of 500 EUR from Charlie to Dania. +--- +transferFailed: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:09, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:06:00, + from: 101045, + to: 101055, + currency: EUR, + amount: 500.0, + reference: Grocery Shopping + }, + reason: insufficient funds +} +... +# This operation attempts to make a transfer from a non-existent account to Charlie. +--- +transferFailed: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:10, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:07:00, + from: 2, + to: 101045, + currency: EUR, + amount: 500.0, + reference: Unexpected Income + }, + reason: from account doesn't exist +} +... +# This operation attempts to transfer 1000 EUR from Dania to a non-existent account. +--- +transferFailed: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:11, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:08:00, + from: 101055, + to: 9999, + currency: EUR, + amount: 1E3, + reference: Rent + }, + reason: insufficient funds +} +... +# This operation creates an account for Alice with account number 101013. +# missing balance: 15 +--- +createAccountFailed: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:12, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:00, + name: alice, + account: 101013, + currency: EUR, + balance: 0.0, + overdraft: 0.0 + }, + reason: account already exists +} +... +# This operation creates an account for Bob with account number 101025. +# missing balance: 1000 +--- +createAccountFailed: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:13, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:01, + name: bob, + account: 101025, + currency: EUR, + balance: 0.0, + overdraft: 0.0 + }, + reason: account already exists +} +... +# This operation represents a transfer of 10 EUR from Alice to Bob. +--- +transferFailed: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:14, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:01:00, + from: 101013, + to: 101025, + currency: EUR, + amount: 10.0, + reference: Dog food + }, + reason: insufficient funds +} +... +# This operation attempts to make a transfer from a non-existent account to Bob. +--- +transferFailed: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:15, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:02:00, + from: 1, + to: 101025, + currency: EUR, + amount: 10.0, + reference: Junk food + }, + reason: from account doesn't exist +} +... +# This operation attempts to transfer 10 EUR from Alice to a non-existent account. +--- +transferFailed: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:16, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:03:00, + from: 101013, + to: 9999, + currency: EUR, + amount: 10.0, + reference: Cat food + }, + reason: insufficient funds +} +... +# This operation creates an account for Alice with account number 101013. +# missing balance: 15.0, +--- +createAccountFailed: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:17, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:00, + name: alice, + account: 101013, + currency: EUR, + balance: 0.0, + overdraft: 0.0 + }, + reason: account already exists +} +... +# This operation creates an account for Bob with account number 101025. +# missing balance: 1E3, +--- +createAccountFailed: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:18, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:01, + name: bob, + account: 101025, + currency: USD, + balance: 0.0, + overdraft: 0.0 + }, + reason: account already exists +} +... +# This operation creates an account for Charlie with account number 101040. +# missing balance: 60.0, +--- +onCreateAccount: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:19, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:02, + name: charlie, + account: 101040, + currency: EUR, + balance: 0.0, + overdraft: 0.0 + } +} +... +# This operation creates an account for Alice with account number 101013. +# missing balance: 15.0, +--- +createAccountFailed: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:20, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:00, + name: alice, + account: 101013, + currency: EUR, + balance: 0.0, + overdraft: 0.0 + }, + reason: account already exists +} +... diff --git a/account/src/test/resources/account/copilot/out-missing-field-name.yaml b/account/src/test/resources/account/copilot/out-missing-field-name.yaml new file mode 100644 index 0000000..4c2e452 --- /dev/null +++ b/account/src/test/resources/account/copilot/out-missing-field-name.yaml @@ -0,0 +1,146 @@ +# This operation creates an account for Charlie with account number 101045. +# missing name: charlie, +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: name must be set and not empty" +... +# This operation creates an account for Dania with account number 101055. +# missing name: Dania, +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: name must be set and not empty" +... +# This operation represents a transfer of 500 EUR from Charlie to Dania. +--- +transferFailed: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:07, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:06:00, + from: 101045, + to: 101055, + currency: EUR, + amount: 500.0, + reference: Grocery Shopping + }, + reason: from account doesn't exist +} +... +# This operation attempts to make a transfer from a non-existent account to Charlie. +--- +transferFailed: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:08, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:07:00, + from: 2, + to: 101045, + currency: EUR, + amount: 500.0, + reference: Unexpected Income + }, + reason: from account doesn't exist +} +... +# This operation attempts to transfer 1000 EUR from Dania to a non-existent account. +--- +transferFailed: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:09, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:08:00, + from: 101055, + to: 9999, + currency: EUR, + amount: 1E3, + reference: Rent + }, + reason: from account doesn't exist +} +... +# This operation creates an account for Alice with account number 101013. +# missing name: alice, +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: name must be set and not empty" +... +# This operation creates an account for Bob with account number 101025. +# missing name: bob, +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: name must be set and not empty" +... +# This operation represents a transfer of 10 EUR from Alice to Bob. +--- +transferFailed: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:10, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:01:00, + from: 101013, + to: 101025, + currency: EUR, + amount: 10.0, + reference: Dog food + }, + reason: insufficient funds +} +... +# This operation attempts to make a transfer from a non-existent account to Bob. +--- +transferFailed: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:11, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:02:00, + from: 1, + to: 101025, + currency: EUR, + amount: 10.0, + reference: Junk food + }, + reason: from account doesn't exist +} +... +# This operation attempts to transfer 10 EUR from Alice to a non-existent account. +--- +transferFailed: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:12, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:03:00, + from: 101013, + to: 9999, + currency: EUR, + amount: 10.0, + reference: Cat food + }, + reason: insufficient funds +} +... +# This operation creates an account for Alice with account number 101013. +# missing name: alice, +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: name must be set and not empty" +... +# This operation creates an account for Bob with account number 101025. +# missing name: bob, +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: name must be set and not empty" +... +# This operation creates an account for Charlie with account number 101040. +# missing name: charlie, +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: name must be set and not empty" +... +# This operation creates an account for Alice with account number 101013. +# missing name: alice, +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: name must be set and not empty" +... diff --git a/account/src/test/resources/account/copilot/out-missing-field-reference.yaml b/account/src/test/resources/account/copilot/out-missing-field-reference.yaml new file mode 100644 index 0000000..8579011 --- /dev/null +++ b/account/src/test/resources/account/copilot/out-missing-field-reference.yaml @@ -0,0 +1,173 @@ +# This operation creates an account for Charlie with account number 101045. +--- +onCreateAccount: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:07, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:05:00, + name: charlie, + account: 101045, + currency: EUR, + balance: 5E3, + overdraft: 0.0 + } +} +... +# This operation creates an account for Dania with account number 101055. +--- +onCreateAccount: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:08, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:05:01, + name: Dania, + account: 101055, + currency: EUR, + balance: 3500.0, + overdraft: 0.0 + } +} +... +# This operation represents a transfer of 500 EUR from Charlie to Dania. +# missing reference: Grocery Shopping +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: reference must be set" +... +# This operation attempts to make a transfer from a non-existent account to Charlie. +# missing reference: Unexpected Income +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: reference must be set" +... +# This operation attempts to transfer 1000 EUR from Dania to a non-existent account. +# missing reference: Rent +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: reference must be set" +... +# This operation creates an account for Alice with account number 101013. +--- +createAccountFailed: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:09, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:00, + name: alice, + account: 101013, + currency: EUR, + balance: 15.0, + overdraft: 0.0 + }, + reason: account already exists +} +... +# This operation creates an account for Bob with account number 101025. +--- +createAccountFailed: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:10, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:01, + name: bob, + account: 101025, + currency: EUR, + balance: 1E3, + overdraft: 0.0 + }, + reason: account already exists +} +... +# This operation represents a transfer of 10 EUR from Alice to Bob. +# missing reference: Dog food +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: reference must be set" +... +# This operation attempts to make a transfer from a non-existent account to Bob. +# missing reference: Junk food +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: reference must be set" +... +# This operation attempts to transfer 10 EUR from Alice to a non-existent account. +# missing reference: Cat food +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: reference must be set" +... +# This operation creates an account for Alice with account number 101013. +--- +createAccountFailed: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:11, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:00, + name: alice, + account: 101013, + currency: EUR, + balance: 15.0, + overdraft: 0.0 + }, + reason: account already exists +} +... +# This operation creates an account for Bob with account number 101025. +--- +createAccountFailed: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:12, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:01, + name: bob, + account: 101025, + currency: USD, + balance: 1E3, + overdraft: 0.0 + }, + reason: account already exists +} +... +# This operation creates an account for Charlie with account number 101040. +--- +onCreateAccount: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:13, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:02, + name: charlie, + account: 101040, + currency: EUR, + balance: 60.0, + overdraft: 0.0 + } +} +... +# This operation creates an account for Alice with account number 101013. +--- +createAccountFailed: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:14, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:00, + name: alice, + account: 101013, + currency: EUR, + balance: 15.0, + overdraft: 0.0 + }, + reason: account already exists +} +... diff --git a/account/src/test/resources/account/copilot/out-missing-field-sendingTime.yaml b/account/src/test/resources/account/copilot/out-missing-field-sendingTime.yaml new file mode 100644 index 0000000..ff0af02 --- /dev/null +++ b/account/src/test/resources/account/copilot/out-missing-field-sendingTime.yaml @@ -0,0 +1,56 @@ +# This operation creates an account for Charlie with account number 101045. +# missing sendingTime: 2023-01-20T10:05:00, +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: sendingTime must be set" +... +# This operation creates an account for Dania with account number 101055. +# missing sendingTime: 2023-01-20T10:05:01, +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: sendingTime must be set" +... +# This operation represents a transfer of 500 EUR from Charlie to Dania. +# missing sendingTime: 2023-01-20T10:06:00, +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: sendingTime must be set" +... +# This operation attempts to make a transfer from a non-existent account to Charlie. +# missing sendingTime: 2023-01-20T10:07:00, +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: sendingTime must be set" +... +# This operation attempts to transfer 1000 EUR from Dania to a non-existent account. +# missing sendingTime: 2023-01-20T10:08:00, +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: sendingTime must be set" +... +# This operation creates an account for Alice with account number 101013. +# missing sendingTime: 2023-01-20T10:00:00, +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: sendingTime must be set" +... +# This operation creates an account for Bob with account number 101025. +# missing sendingTime: 2023-01-20T10:00:01, +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: sendingTime must be set" +... +# This operation represents a transfer of 10 EUR from Alice to Bob. +# missing sendingTime: 2023-01-20T10:01:00, +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: sendingTime must be set" +... +# This operation attempts to make a transfer from a non-existent account to Bob. +# missing sendingTime: 2023-01-20T10:02:00, +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: sendingTime must be set" +... +# This operation attempts to transfer 10 EUR from Alice to a non-existent account. +# missing sendingTime: 2023-01-20T10:03:00, +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: sendingTime must be set" +... +# This operation creates an account for Alice with account number 101013. +# missing sendingTime: 2023-01-20T10:00:00, +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: sendingTime must be set" +... +# This operation creates an account for Bob with account number 101025. +# missing sendingTime: 2023-01-20T10:00:01, +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: sendingTime must be set" +... +# This operation creates an account for Charlie with account number 101040. +# missing sendingTime: 2023-01-20T10:00:02, +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: sendingTime must be set" +... +# This operation creates an account for Alice with account number 101013. +# missing sendingTime: 2023-01-20T10:00:00, +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: sendingTime must be set" +... diff --git a/account/src/test/resources/account/copilot/out-msg-0-duplicated.yaml b/account/src/test/resources/account/copilot/out-msg-0-duplicated.yaml new file mode 100644 index 0000000..6463e0a --- /dev/null +++ b/account/src/test/resources/account/copilot/out-msg-0-duplicated.yaml @@ -0,0 +1,529 @@ +# This operation creates an account for Charlie with account number 101045. +--- +onCreateAccount: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:07, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:05:00, + name: charlie, + account: 101045, + currency: EUR, + balance: 5E3, + overdraft: 0.0 + } +} +... +# This operation creates an account for Dania with account number 101055. +--- +onCreateAccount: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:08, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:05:01, + name: Dania, + account: 101055, + currency: EUR, + balance: 3500.0, + overdraft: 0.0 + } +} +... +# This operation represents a transfer of 500 EUR from Charlie to Dania. +--- +onTransfer: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:09, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:06:00, + from: 101045, + to: 101055, + currency: EUR, + amount: 500.0, + reference: Grocery Shopping + } +} +... +# This operation attempts to make a transfer from a non-existent account to Charlie. +--- +transferFailed: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:10, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:07:00, + from: 2, + to: 101045, + currency: EUR, + amount: 500.0, + reference: Unexpected Income + }, + reason: from account doesn't exist +} +... +# This operation attempts to transfer 1000 EUR from Dania to a non-existent account. +--- +transferFailed: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:11, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:08:00, + from: 101055, + to: 9999, + currency: EUR, + amount: 1E3, + reference: Rent + }, + reason: to account doesn't exist +} +... +# This operation creates an account for Alice with account number 101013. +--- +createAccountFailed: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:12, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:00, + name: alice, + account: 101013, + currency: EUR, + balance: 15.0, + overdraft: 0.0 + }, + reason: account already exists +} +... +# This operation creates an account for Bob with account number 101025. +--- +createAccountFailed: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:13, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:01, + name: bob, + account: 101025, + currency: EUR, + balance: 1E3, + overdraft: 0.0 + }, + reason: account already exists +} +... +# This operation represents a transfer of 10 EUR from Alice to Bob. +--- +transferFailed: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:14, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:01:00, + from: 101013, + to: 101025, + currency: EUR, + amount: 10.0, + reference: Dog food + }, + reason: insufficient funds +} +... +# This operation attempts to make a transfer from a non-existent account to Bob. +--- +transferFailed: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:15, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:02:00, + from: 1, + to: 101025, + currency: EUR, + amount: 10.0, + reference: Junk food + }, + reason: from account doesn't exist +} +... +# This operation attempts to transfer 10 EUR from Alice to a non-existent account. +--- +transferFailed: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:16, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:03:00, + from: 101013, + to: 9999, + currency: EUR, + amount: 10.0, + reference: Cat food + }, + reason: insufficient funds +} +... +# This operation creates an account for Alice with account number 101013. +--- +createAccountFailed: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:17, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:00, + name: alice, + account: 101013, + currency: EUR, + balance: 15.0, + overdraft: 0.0 + }, + reason: account already exists +} +... +# This operation creates an account for Bob with account number 101025. +--- +createAccountFailed: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:18, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:01, + name: bob, + account: 101025, + currency: USD, + balance: 1E3, + overdraft: 0.0 + }, + reason: account already exists +} +... +# This operation creates an account for Charlie with account number 101040. +--- +onCreateAccount: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:19, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:02, + name: charlie, + account: 101040, + currency: EUR, + balance: 60.0, + overdraft: 0.0 + } +} +... +# This operation creates an account for Alice with account number 101013. +--- +createAccountFailed: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:20, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:00, + name: alice, + account: 101013, + currency: EUR, + balance: 15.0, + overdraft: 0.0 + }, + reason: account already exists +} +... +jvmError: "java.lang.UnsupportedOperationException: TEXT ---..." +... +# This operation creates an account for Charlie with account number 101045. +--- +createAccountFailed: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:21, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:05:00, + name: charlie, + account: 101045, + currency: EUR, + balance: 5E3, + overdraft: 0.0 + }, + reason: account already exists +} +... +# This operation creates an account for Dania with account number 101055. +--- +createAccountFailed: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:22, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:05:01, + name: Dania, + account: 101055, + currency: EUR, + balance: 3500.0, + overdraft: 0.0 + }, + reason: account already exists +} +... +# This operation represents a transfer of 500 EUR from Charlie to Dania. +--- +onTransfer: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:23, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:06:00, + from: 101045, + to: 101055, + currency: EUR, + amount: 500.0, + reference: Grocery Shopping + } +} +... +# This operation attempts to make a transfer from a non-existent account to Charlie. +--- +transferFailed: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:24, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:07:00, + from: 2, + to: 101045, + currency: EUR, + amount: 500.0, + reference: Unexpected Income + }, + reason: from account doesn't exist +} +... +# This operation attempts to transfer 1000 EUR from Dania to a non-existent account. +--- +transferFailed: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:25, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:08:00, + from: 101055, + to: 9999, + currency: EUR, + amount: 1E3, + reference: Rent + }, + reason: to account doesn't exist +} +... +# This operation creates an account for Alice with account number 101013. +--- +createAccountFailed: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:26, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:00, + name: alice, + account: 101013, + currency: EUR, + balance: 15.0, + overdraft: 0.0 + }, + reason: account already exists +} +... +# This operation creates an account for Bob with account number 101025. +--- +createAccountFailed: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:27, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:01, + name: bob, + account: 101025, + currency: EUR, + balance: 1E3, + overdraft: 0.0 + }, + reason: account already exists +} +... +# This operation represents a transfer of 10 EUR from Alice to Bob. +--- +transferFailed: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:28, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:01:00, + from: 101013, + to: 101025, + currency: EUR, + amount: 10.0, + reference: Dog food + }, + reason: insufficient funds +} +... +# This operation attempts to make a transfer from a non-existent account to Bob. +--- +transferFailed: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:29, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:02:00, + from: 1, + to: 101025, + currency: EUR, + amount: 10.0, + reference: Junk food + }, + reason: from account doesn't exist +} +... +# This operation attempts to transfer 10 EUR from Alice to a non-existent account. +--- +transferFailed: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:30, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:03:00, + from: 101013, + to: 9999, + currency: EUR, + amount: 10.0, + reference: Cat food + }, + reason: insufficient funds +} +... +# This operation creates an account for Alice with account number 101013. +--- +createAccountFailed: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:31, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:00, + name: alice, + account: 101013, + currency: EUR, + balance: 15.0, + overdraft: 0.0 + }, + reason: account already exists +} +... +# This operation creates an account for Bob with account number 101025. +--- +createAccountFailed: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:32, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:01, + name: bob, + account: 101025, + currency: USD, + balance: 1E3, + overdraft: 0.0 + }, + reason: account already exists +} +... +# This operation creates an account for Charlie with account number 101040. +--- +createAccountFailed: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:33, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:02, + name: charlie, + account: 101040, + currency: EUR, + balance: 60.0, + overdraft: 0.0 + }, + reason: account already exists +} +... +# This operation creates an account for Alice with account number 101013. +--- +createAccountFailed: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:34, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:00, + name: alice, + account: 101013, + currency: EUR, + balance: 15.0, + overdraft: 0.0 + }, + reason: account already exists +} +... diff --git a/account/src/test/resources/account/copilot/out-set-field-balance=-1.yaml b/account/src/test/resources/account/copilot/out-set-field-balance=-1.yaml new file mode 100644 index 0000000..28bb342 --- /dev/null +++ b/account/src/test/resources/account/copilot/out-set-field-balance=-1.yaml @@ -0,0 +1,146 @@ +# This operation creates an account for Charlie with account number 101045. +# override balance: 5000 to balance: -1 +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: balance must be positive or zero" +... +# This operation creates an account for Dania with account number 101055. +# override balance: 3500 to balance: -1 +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: balance must be positive or zero" +... +# This operation represents a transfer of 500 EUR from Charlie to Dania. +--- +transferFailed: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:07, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:06:00, + from: 101045, + to: 101055, + currency: EUR, + amount: 500.0, + reference: Grocery Shopping + }, + reason: from account doesn't exist +} +... +# This operation attempts to make a transfer from a non-existent account to Charlie. +--- +transferFailed: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:08, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:07:00, + from: 2, + to: 101045, + currency: EUR, + amount: 500.0, + reference: Unexpected Income + }, + reason: from account doesn't exist +} +... +# This operation attempts to transfer 1000 EUR from Dania to a non-existent account. +--- +transferFailed: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:09, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:08:00, + from: 101055, + to: 9999, + currency: EUR, + amount: 1E3, + reference: Rent + }, + reason: from account doesn't exist +} +... +# This operation creates an account for Alice with account number 101013. +# override balance: 15 to balance: -1 +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: balance must be positive or zero" +... +# This operation creates an account for Bob with account number 101025. +# override balance: 1000 to balance: -1 +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: balance must be positive or zero" +... +# This operation represents a transfer of 10 EUR from Alice to Bob. +--- +transferFailed: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:10, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:01:00, + from: 101013, + to: 101025, + currency: EUR, + amount: 10.0, + reference: Dog food + }, + reason: insufficient funds +} +... +# This operation attempts to make a transfer from a non-existent account to Bob. +--- +transferFailed: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:11, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:02:00, + from: 1, + to: 101025, + currency: EUR, + amount: 10.0, + reference: Junk food + }, + reason: from account doesn't exist +} +... +# This operation attempts to transfer 10 EUR from Alice to a non-existent account. +--- +transferFailed: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:12, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:03:00, + from: 101013, + to: 9999, + currency: EUR, + amount: 10.0, + reference: Cat food + }, + reason: insufficient funds +} +... +# This operation creates an account for Alice with account number 101013. +# override balance: 15.0 to balance: -1 +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: balance must be positive or zero" +... +# This operation creates an account for Bob with account number 101025. +# override balance: 1E3 to balance: -1 +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: balance must be positive or zero" +... +# This operation creates an account for Charlie with account number 101040. +# override balance: 60.0 to balance: -1 +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: balance must be positive or zero" +... +# This operation creates an account for Alice with account number 101013. +# override balance: 15.0 to balance: -1 +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: balance must be positive or zero" +... diff --git a/account/src/test/resources/account/copilot/out-set-field-balance=NaN.yaml b/account/src/test/resources/account/copilot/out-set-field-balance=NaN.yaml new file mode 100644 index 0000000..373a840 --- /dev/null +++ b/account/src/test/resources/account/copilot/out-set-field-balance=NaN.yaml @@ -0,0 +1,274 @@ +# This operation creates an account for Charlie with account number 101045. +# override balance: 5000 to balance: NaN +--- +createAccountFailed: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:07, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:05:00, + name: charlie, + account: 101045, + currency: EUR, + balance: NaN, + overdraft: 0.0 + }, + reason: invalid balance +} +... +# This operation creates an account for Dania with account number 101055. +# override balance: 3500 to balance: NaN +--- +createAccountFailed: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:08, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:05:01, + name: Dania, + account: 101055, + currency: EUR, + balance: NaN, + overdraft: 0.0 + }, + reason: invalid balance +} +... +# This operation represents a transfer of 500 EUR from Charlie to Dania. +--- +transferFailed: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:09, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:06:00, + from: 101045, + to: 101055, + currency: EUR, + amount: 500.0, + reference: Grocery Shopping + }, + reason: from account doesn't exist +} +... +# This operation attempts to make a transfer from a non-existent account to Charlie. +--- +transferFailed: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:10, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:07:00, + from: 2, + to: 101045, + currency: EUR, + amount: 500.0, + reference: Unexpected Income + }, + reason: from account doesn't exist +} +... +# This operation attempts to transfer 1000 EUR from Dania to a non-existent account. +--- +transferFailed: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:11, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:08:00, + from: 101055, + to: 9999, + currency: EUR, + amount: 1E3, + reference: Rent + }, + reason: from account doesn't exist +} +... +# This operation creates an account for Alice with account number 101013. +# override balance: 15 to balance: NaN +--- +createAccountFailed: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:12, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:00, + name: alice, + account: 101013, + currency: EUR, + balance: NaN, + overdraft: 0.0 + }, + reason: invalid balance +} +... +# This operation creates an account for Bob with account number 101025. +# override balance: 1000 to balance: NaN +--- +createAccountFailed: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:13, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:01, + name: bob, + account: 101025, + currency: EUR, + balance: NaN, + overdraft: 0.0 + }, + reason: invalid balance +} +... +# This operation represents a transfer of 10 EUR from Alice to Bob. +--- +transferFailed: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:14, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:01:00, + from: 101013, + to: 101025, + currency: EUR, + amount: 10.0, + reference: Dog food + }, + reason: insufficient funds +} +... +# This operation attempts to make a transfer from a non-existent account to Bob. +--- +transferFailed: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:15, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:02:00, + from: 1, + to: 101025, + currency: EUR, + amount: 10.0, + reference: Junk food + }, + reason: from account doesn't exist +} +... +# This operation attempts to transfer 10 EUR from Alice to a non-existent account. +--- +transferFailed: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:16, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:03:00, + from: 101013, + to: 9999, + currency: EUR, + amount: 10.0, + reference: Cat food + }, + reason: insufficient funds +} +... +# This operation creates an account for Alice with account number 101013. +# override balance: 15.0 to balance: NaN +--- +createAccountFailed: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:17, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:00, + name: alice, + account: 101013, + currency: EUR, + balance: NaN, + overdraft: 0.0 + }, + reason: invalid balance +} +... +# This operation creates an account for Bob with account number 101025. +# override balance: 1E3 to balance: NaN +--- +createAccountFailed: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:18, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:01, + name: bob, + account: 101025, + currency: USD, + balance: NaN, + overdraft: 0.0 + }, + reason: invalid balance +} +... +# This operation creates an account for Charlie with account number 101040. +# override balance: 60.0 to balance: NaN +--- +createAccountFailed: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:19, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:02, + name: charlie, + account: 101040, + currency: EUR, + balance: NaN, + overdraft: 0.0 + }, + reason: invalid balance +} +... +# This operation creates an account for Alice with account number 101013. +# override balance: 15.0 to balance: NaN +--- +createAccountFailed: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:20, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:00, + name: alice, + account: 101013, + currency: EUR, + balance: NaN, + overdraft: 0.0 + }, + reason: invalid balance +} +... diff --git a/account/src/test/resources/account/o1-pro/README.adoc b/account/src/test/resources/account/o1-pro/README.adoc new file mode 100644 index 0000000..9060e4a --- /dev/null +++ b/account/src/test/resources/account/o1-pro/README.adoc @@ -0,0 +1,72 @@ += Test Scenarios for Account Management Service + +This directory contains three YAML files that define and verify specific test scenarios for an Account Management Service (AMS). The goal is to ensure that the AMS behaves as expected when processing various account creation, transfer, and checkpoint commands. + +== Files Overview + +* **_setup.yaml**: Establishes the initial system state before the tests begin. +* **in.yaml**: Specifies the input commands (events) to be replayed against the AMS. +* **out.yaml**: Details the expected output events corresponding to each input scenario. + +== _setup.yaml + +The `_setup.yaml` file configures a baseline state for testing: + +* **Alice (101013, 15 EUR)**: Provides a known starting point for EUR-to-EUR transfers. +* **Bob (101025, 1000 USD)**: Tests scenarios involving currency mismatch, ensuring that transfers fail when currencies differ. +* **Charlie (101040, 50 EUR)**: Allows testing successful EUR-to-EUR transfers and verifying proper balance updates. + +All initial accounts and balances are established here, ensuring that subsequent operations in `in.yaml` have a known and stable starting point. + +== in.yaml + +The `in.yaml` file contains a series of input scenarios that exercise both successful and failing operations: + +1. **Valid EUR-to-EUR Transfer (Alice -> Charlie)**: +Tests a successful transfer, confirming that balances are updated and `onTransfer` events are generated as expected. + +2. **Currency Mismatch Transfer (Alice -> Bob)**: +Attempts a EUR-to-USD transfer and expects a `transferFailed` event due to currency mismatch. + +3. **Invalid Create Account (Derek with -20 EUR)**: +Attempts to create an account with a negative balance and expects a `createAccountFailed` event. + +4. **Non-Existent Account Transfer**: +Tries to transfer funds from an account that doesn't exist, expecting a `transferFailed` event with a "from account doesn't exist" reason. + +5. **Insufficient Funds Transfer (Alice -> Charlie)**: +After a successful transfer, Alice has reduced funds. A large transfer request should fail with `transferFailed` due to insufficient funds. + +6. **Checkpoint Request**: +Requests a checkpoint, verifying `startCheckpoint`, `onCreateAccount` for each known account, and `endCheckpoint` events to confirm correct state serialization. + +Each scenario is accompanied by comments explaining the rationale, expected outcomes, and relevant details for easy reference. + +== out.yaml + +The `out.yaml` file defines the expected output events for each scenario in `in.yaml`: + +* **onTransfer** events for successful transfers. +* **transferFailed** and **createAccountFailed** events for invalid operations. +* **jvmError** entries if invalid input triggers a validation exception. +* **startCheckpoint**, `onCreateAccount` (for each known account), and `endCheckpoint` events for the checkpoint scenario. + +Comments within `out.yaml` map each output event back to the corresponding input scenario, making it clear which events should be produced in response to each command. + +== General Guidelines + +1. **Clarity and Context**: +The comments in `_setup.yaml`, `in.yaml`, and `out.yaml` provide context and reasoning for each scenario, making the tests more understandable and maintainable. + +2. **Time Management**: +Although fixed timestamps are used here, in production and performance tests, consider using `SystemTimeProvider.CLOCK.currentTimeNanos()` for nanosecond-precision wall clock times. + +3. **Validation Checks**: +Scenarios include invalid operations to ensure `createAccountFailed` and `transferFailed` events are emitted correctly when requirements are not met. + +4. **Coverage and Maintenance**: +The test set covers successful and failed account creations, valid and invalid transfers, currency mismatches, insufficient funds scenarios, and checkpointing. This broad coverage ensures confidence in the AMS's correctness and resilience. + +== Conclusion + +These YAML-based test inputs and expected outputs form a comprehensive testing framework for the Account Management Service. By examining the scenarios in `_setup.yaml`, `in.yaml`, and `out.yaml`, developers and testers can quickly understand the expected behavior of the system and ensure that the AMS meets all specified requirements. diff --git a/account/src/test/resources/account/o1-pro/_setup.yaml b/account/src/test/resources/account/o1-pro/_setup.yaml new file mode 100644 index 0000000..1f28d38 --- /dev/null +++ b/account/src/test/resources/account/o1-pro/_setup.yaml @@ -0,0 +1,38 @@ +--- +# Create an account for Alice (101013) with 15 EUR initial balance. +# Rationale: Alice is used in transfer scenarios that require a known starting balance. +createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:00, + name: alice, + account: 101013, + currency: EUR, + balance: 15 +} +... +--- +# Create an account for Bob (101025) with 1000 USD initial balance. +# Rationale: Bob's account is in a different currency (USD) to test currency mismatch failures. +createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:01, + name: bob, + account: 101025, + currency: USD, + balance: 1000 +} + +--- +# Create an account for Charlie (101040) with 50 EUR initial balance. +# Rationale: Charlie has an EUR account to test successful EUR-to-EUR transfers. +createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:02, + name: charlie, + account: 101040, + currency: EUR, + balance: 50 +} diff --git a/account/src/test/resources/account/o1-pro/in.yaml b/account/src/test/resources/account/o1-pro/in.yaml new file mode 100644 index 0000000..ad56ff7 --- /dev/null +++ b/account/src/test/resources/account/o1-pro/in.yaml @@ -0,0 +1,84 @@ +--- +# Scenario: Valid EUR-to-EUR Transfer (Alice -> Charlie) +# Alice (101013) has 15 EUR, Charlie (101040) has 50 EUR. +# Transfer 10 EUR from Alice to Charlie should succeed. +transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:01:00, + from: 101013, + to: 101040, + currency: EUR, + amount: 10, + reference: Groceries Payment +} +... +--- +# Scenario: Currency Mismatch Transfer (Alice -> Bob) +# Attempting a transfer of 10 EUR to Bob who holds a USD account. +# This should fail due to currency mismatch. +transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:02:00, + from: 101013, + to: 101025, + currency: EUR, + amount: 10, + reference: "Dog food" +} + +--- +# Scenario: Invalid Create Account +# Attempt to create an account with a negative initial balance (-20). +# This should fail with a createAccountFailed event. +createAccount: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:03:00, + name: "derek", + account: 101050, + currency: EUR, + balance: -20 +} + +--- +# Scenario: Non-Existent Account Transfer +# Attempting to transfer 5 EUR from a non-existent account (999999) to Charlie (101040). +# This should fail with reason "accountNotFound". +transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:04:00, + from: 999999, + to: 101040, + currency: EUR, + amount: 5, + reference: "Mystery Sender" +} + +--- +# Scenario: Insufficient Funds Transfer +# After the first successful transfer, Alice has 5 EUR left (15 - 10 = 5). +# Attempting to transfer 100 EUR from Alice to Charlie should fail with "insufficientFunds". +transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:05:00, + from: 101013, + to: 101040, + currency: EUR, + amount: 100, + reference: "Big Purchase" +} + +--- +# Scenario: Checkpoint +# Requesting a checkpoint after all operations. +# Expected output: startCheckpoint, onCreateAccount events for all existing accounts, +# and endCheckpoint, reflecting the final state of known accounts. +checkPoint: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T11:00:00 +} diff --git a/account/src/test/resources/account/o1-pro/out-missing-field-from.yaml b/account/src/test/resources/account/o1-pro/out-missing-field-from.yaml new file mode 100644 index 0000000..adb9fe4 --- /dev/null +++ b/account/src/test/resources/account/o1-pro/out-missing-field-from.yaml @@ -0,0 +1,94 @@ +# Scenario: Valid EUR-to-EUR Transfer (Alice -> Charlie) +# Alice (101013) has 15 EUR, Charlie (101040) has 50 EUR. +# Transfer 10 EUR from Alice to Charlie should succeed. +# missing from: 101013, +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: from must be set" +... +# Scenario: Currency Mismatch Transfer (Alice -> Bob) +# Attempting a transfer of 10 EUR to Bob who holds a USD account. +# This should fail due to currency mismatch. +# missing from: 101013, +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: from must be set" +... +# Scenario: Invalid Create Account +# Attempt to create an account with a negative initial balance (-20). +# This should fail with a createAccountFailed event. +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: balance must be positive or zero" +... +# Scenario: Non-Existent Account Transfer +# Attempting to transfer 5 EUR from a non-existent account (999999) to Charlie (101040). +# This should fail with reason "accountNotFound". +# missing from: 999999, +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: from must be set" +... +# Scenario: Insufficient Funds Transfer +# After the first successful transfer, Alice has 5 EUR left (15 - 10 = 5). +# Attempting to transfer 100 EUR from Alice to Charlie should fail with "insufficientFunds". +# missing from: 101013, +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: from must be set" +... +# Scenario: Checkpoint +# Requesting a checkpoint after all operations. +# Expected output: startCheckpoint, onCreateAccount events for all existing accounts, +# and endCheckpoint, reflecting the final state of known accounts. +--- +startCheckpoint: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T11:00:00 +} +... +onCreateAccount: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:03, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:00, + name: alice, + account: 101013, + currency: EUR, + balance: 15.0, + overdraft: 0.0 + } +} +... +onCreateAccount: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:04, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:01, + name: bob, + account: 101025, + currency: USD, + balance: 1E3, + overdraft: 0.0 + } +} +... +onCreateAccount: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:05, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:02, + name: charlie, + account: 101040, + currency: EUR, + balance: 50.0, + overdraft: 0.0 + } +} +... +endCheckpoint: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T11:00:00 +} +... diff --git a/account/src/test/resources/account/o1-pro/out-missing-field-name.yaml b/account/src/test/resources/account/o1-pro/out-missing-field-name.yaml new file mode 100644 index 0000000..3cbfbde --- /dev/null +++ b/account/src/test/resources/account/o1-pro/out-missing-field-name.yaml @@ -0,0 +1,154 @@ +# Scenario: Valid EUR-to-EUR Transfer (Alice -> Charlie) +# Alice (101013) has 15 EUR, Charlie (101040) has 50 EUR. +# Transfer 10 EUR from Alice to Charlie should succeed. +--- +onTransfer: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:03, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:01:00, + from: 101013, + to: 101040, + currency: EUR, + amount: 10.0, + reference: Groceries Payment + } +} +... +# Scenario: Currency Mismatch Transfer (Alice -> Bob) +# Attempting a transfer of 10 EUR to Bob who holds a USD account. +# This should fail due to currency mismatch. +--- +transferFailed: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:04, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:02:00, + from: 101013, + to: 101025, + currency: EUR, + amount: 10.0, + reference: Dog food + }, + reason: insufficient funds +} +... +# Scenario: Invalid Create Account +# Attempt to create an account with a negative initial balance (-20). +# This should fail with a createAccountFailed event. +# missing name: "derek", +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: name must be set and not empty" +... +# Scenario: Non-Existent Account Transfer +# Attempting to transfer 5 EUR from a non-existent account (999999) to Charlie (101040). +# This should fail with reason "accountNotFound". +--- +transferFailed: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:05, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:04:00, + from: 999999, + to: 101040, + currency: EUR, + amount: 5.0, + reference: Mystery Sender + }, + reason: from account doesn't exist +} +... +# Scenario: Insufficient Funds Transfer +# After the first successful transfer, Alice has 5 EUR left (15 - 10 = 5). +# Attempting to transfer 100 EUR from Alice to Charlie should fail with "insufficientFunds". +--- +transferFailed: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:06, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:05:00, + from: 101013, + to: 101040, + currency: EUR, + amount: 100.0, + reference: Big Purchase + }, + reason: insufficient funds +} +... +# Scenario: Checkpoint +# Requesting a checkpoint after all operations. +# Expected output: startCheckpoint, onCreateAccount events for all existing accounts, +# and endCheckpoint, reflecting the final state of known accounts. +--- +startCheckpoint: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T11:00:00 +} +... +onCreateAccount: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:07, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:00, + name: alice, + account: 101013, + currency: EUR, + balance: 5.0, + overdraft: 0.0 + } +} +... +onCreateAccount: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:08, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:01, + name: bob, + account: 101025, + currency: USD, + balance: 1E3, + overdraft: 0.0 + } +} +... +onCreateAccount: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:09, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:02, + name: charlie, + account: 101040, + currency: EUR, + balance: 60.0, + overdraft: 0.0 + } +} +... +endCheckpoint: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T11:00:00 +} +... diff --git a/account/src/test/resources/account/o1-pro/out-missing-field-reference.yaml b/account/src/test/resources/account/o1-pro/out-missing-field-reference.yaml new file mode 100644 index 0000000..7941f8e --- /dev/null +++ b/account/src/test/resources/account/o1-pro/out-missing-field-reference.yaml @@ -0,0 +1,94 @@ +# Scenario: Valid EUR-to-EUR Transfer (Alice -> Charlie) +# Alice (101013) has 15 EUR, Charlie (101040) has 50 EUR. +# Transfer 10 EUR from Alice to Charlie should succeed. +# missing reference: Groceries Payment +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: reference must be set" +... +# Scenario: Currency Mismatch Transfer (Alice -> Bob) +# Attempting a transfer of 10 EUR to Bob who holds a USD account. +# This should fail due to currency mismatch. +# missing reference: "Dog food" +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: reference must be set" +... +# Scenario: Invalid Create Account +# Attempt to create an account with a negative initial balance (-20). +# This should fail with a createAccountFailed event. +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: balance must be positive or zero" +... +# Scenario: Non-Existent Account Transfer +# Attempting to transfer 5 EUR from a non-existent account (999999) to Charlie (101040). +# This should fail with reason "accountNotFound". +# missing reference: "Mystery Sender" +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: reference must be set" +... +# Scenario: Insufficient Funds Transfer +# After the first successful transfer, Alice has 5 EUR left (15 - 10 = 5). +# Attempting to transfer 100 EUR from Alice to Charlie should fail with "insufficientFunds". +# missing reference: "Big Purchase" +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: reference must be set" +... +# Scenario: Checkpoint +# Requesting a checkpoint after all operations. +# Expected output: startCheckpoint, onCreateAccount events for all existing accounts, +# and endCheckpoint, reflecting the final state of known accounts. +--- +startCheckpoint: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T11:00:00 +} +... +onCreateAccount: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:03, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:00, + name: alice, + account: 101013, + currency: EUR, + balance: 15.0, + overdraft: 0.0 + } +} +... +onCreateAccount: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:04, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:01, + name: bob, + account: 101025, + currency: USD, + balance: 1E3, + overdraft: 0.0 + } +} +... +onCreateAccount: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:05, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:02, + name: charlie, + account: 101040, + currency: EUR, + balance: 50.0, + overdraft: 0.0 + } +} +... +endCheckpoint: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T11:00:00 +} +... diff --git a/account/src/test/resources/account/o1-pro/out-missing-field-sendingTime.yaml b/account/src/test/resources/account/o1-pro/out-missing-field-sendingTime.yaml new file mode 100644 index 0000000..985a152 --- /dev/null +++ b/account/src/test/resources/account/o1-pro/out-missing-field-sendingTime.yaml @@ -0,0 +1,37 @@ +# Scenario: Valid EUR-to-EUR Transfer (Alice -> Charlie) +# Alice (101013) has 15 EUR, Charlie (101040) has 50 EUR. +# Transfer 10 EUR from Alice to Charlie should succeed. +# missing sendingTime: 2023-01-20T10:01:00, +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: sendingTime must be set" +... +# Scenario: Currency Mismatch Transfer (Alice -> Bob) +# Attempting a transfer of 10 EUR to Bob who holds a USD account. +# This should fail due to currency mismatch. +# missing sendingTime: 2023-01-20T10:02:00, +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: sendingTime must be set" +... +# Scenario: Invalid Create Account +# Attempt to create an account with a negative initial balance (-20). +# This should fail with a createAccountFailed event. +# missing sendingTime: 2023-01-20T10:03:00, +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: sendingTime must be set" +... +# Scenario: Non-Existent Account Transfer +# Attempting to transfer 5 EUR from a non-existent account (999999) to Charlie (101040). +# This should fail with reason "accountNotFound". +# missing sendingTime: 2023-01-20T10:04:00, +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: sendingTime must be set" +... +# Scenario: Insufficient Funds Transfer +# After the first successful transfer, Alice has 5 EUR left (15 - 10 = 5). +# Attempting to transfer 100 EUR from Alice to Charlie should fail with "insufficientFunds". +# missing sendingTime: 2023-01-20T10:05:00, +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: sendingTime must be set" +... +# Scenario: Checkpoint +# Requesting a checkpoint after all operations. +# Expected output: startCheckpoint, onCreateAccount events for all existing accounts, +# and endCheckpoint, reflecting the final state of known accounts. +# missing sendingTime: 2023-01-20T11:00:00 +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: sendingTime must be set" +... diff --git a/account/src/test/resources/account/o1-pro/out-set-field-balance=-1.yaml b/account/src/test/resources/account/o1-pro/out-set-field-balance=-1.yaml new file mode 100644 index 0000000..c3a34b3 --- /dev/null +++ b/account/src/test/resources/account/o1-pro/out-set-field-balance=-1.yaml @@ -0,0 +1,154 @@ +# Scenario: Valid EUR-to-EUR Transfer (Alice -> Charlie) +# Alice (101013) has 15 EUR, Charlie (101040) has 50 EUR. +# Transfer 10 EUR from Alice to Charlie should succeed. +--- +onTransfer: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:03, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:01:00, + from: 101013, + to: 101040, + currency: EUR, + amount: 10.0, + reference: Groceries Payment + } +} +... +# Scenario: Currency Mismatch Transfer (Alice -> Bob) +# Attempting a transfer of 10 EUR to Bob who holds a USD account. +# This should fail due to currency mismatch. +--- +transferFailed: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:04, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:02:00, + from: 101013, + to: 101025, + currency: EUR, + amount: 10.0, + reference: Dog food + }, + reason: insufficient funds +} +... +# Scenario: Invalid Create Account +# Attempt to create an account with a negative initial balance (-20). +# This should fail with a createAccountFailed event. +# override balance: -20 to balance: -1 +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: balance must be positive or zero" +... +# Scenario: Non-Existent Account Transfer +# Attempting to transfer 5 EUR from a non-existent account (999999) to Charlie (101040). +# This should fail with reason "accountNotFound". +--- +transferFailed: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:05, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:04:00, + from: 999999, + to: 101040, + currency: EUR, + amount: 5.0, + reference: Mystery Sender + }, + reason: from account doesn't exist +} +... +# Scenario: Insufficient Funds Transfer +# After the first successful transfer, Alice has 5 EUR left (15 - 10 = 5). +# Attempting to transfer 100 EUR from Alice to Charlie should fail with "insufficientFunds". +--- +transferFailed: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:06, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:05:00, + from: 101013, + to: 101040, + currency: EUR, + amount: 100.0, + reference: Big Purchase + }, + reason: insufficient funds +} +... +# Scenario: Checkpoint +# Requesting a checkpoint after all operations. +# Expected output: startCheckpoint, onCreateAccount events for all existing accounts, +# and endCheckpoint, reflecting the final state of known accounts. +--- +startCheckpoint: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T11:00:00 +} +... +onCreateAccount: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:07, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:00, + name: alice, + account: 101013, + currency: EUR, + balance: 5.0, + overdraft: 0.0 + } +} +... +onCreateAccount: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:08, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:01, + name: bob, + account: 101025, + currency: USD, + balance: 1E3, + overdraft: 0.0 + } +} +... +onCreateAccount: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:09, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:02, + name: charlie, + account: 101040, + currency: EUR, + balance: 60.0, + overdraft: 0.0 + } +} +... +endCheckpoint: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T11:00:00 +} +... diff --git a/account/src/test/resources/account/o1-pro/out-set-field-balance=NaN.yaml b/account/src/test/resources/account/o1-pro/out-set-field-balance=NaN.yaml new file mode 100644 index 0000000..1975955 --- /dev/null +++ b/account/src/test/resources/account/o1-pro/out-set-field-balance=NaN.yaml @@ -0,0 +1,170 @@ +# Scenario: Valid EUR-to-EUR Transfer (Alice -> Charlie) +# Alice (101013) has 15 EUR, Charlie (101040) has 50 EUR. +# Transfer 10 EUR from Alice to Charlie should succeed. +--- +onTransfer: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:03, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:01:00, + from: 101013, + to: 101040, + currency: EUR, + amount: 10.0, + reference: Groceries Payment + } +} +... +# Scenario: Currency Mismatch Transfer (Alice -> Bob) +# Attempting a transfer of 10 EUR to Bob who holds a USD account. +# This should fail due to currency mismatch. +--- +transferFailed: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:04, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:02:00, + from: 101013, + to: 101025, + currency: EUR, + amount: 10.0, + reference: Dog food + }, + reason: insufficient funds +} +... +# Scenario: Invalid Create Account +# Attempt to create an account with a negative initial balance (-20). +# This should fail with a createAccountFailed event. +# override balance: -20 to balance: NaN +--- +createAccountFailed: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:05, + createAccount: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:03:00, + name: derek, + account: 101050, + currency: EUR, + balance: NaN, + overdraft: 0.0 + }, + reason: invalid balance +} +... +# Scenario: Non-Existent Account Transfer +# Attempting to transfer 5 EUR from a non-existent account (999999) to Charlie (101040). +# This should fail with reason "accountNotFound". +--- +transferFailed: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:06, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:04:00, + from: 999999, + to: 101040, + currency: EUR, + amount: 5.0, + reference: Mystery Sender + }, + reason: from account doesn't exist +} +... +# Scenario: Insufficient Funds Transfer +# After the first successful transfer, Alice has 5 EUR left (15 - 10 = 5). +# Attempting to transfer 100 EUR from Alice to Charlie should fail with "insufficientFunds". +--- +transferFailed: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:07, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:05:00, + from: 101013, + to: 101040, + currency: EUR, + amount: 100.0, + reference: Big Purchase + }, + reason: insufficient funds +} +... +# Scenario: Checkpoint +# Requesting a checkpoint after all operations. +# Expected output: startCheckpoint, onCreateAccount events for all existing accounts, +# and endCheckpoint, reflecting the final state of known accounts. +--- +startCheckpoint: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T11:00:00 +} +... +onCreateAccount: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:08, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:00, + name: alice, + account: 101013, + currency: EUR, + balance: 5.0, + overdraft: 0.0 + } +} +... +onCreateAccount: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:09, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:01, + name: bob, + account: 101025, + currency: USD, + balance: 1E3, + overdraft: 0.0 + } +} +... +onCreateAccount: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:10, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:02, + name: charlie, + account: 101040, + currency: EUR, + balance: 60.0, + overdraft: 0.0 + } +} +... +endCheckpoint: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T11:00:00 +} +... diff --git a/account/src/test/resources/account/o1-pro/out.yaml b/account/src/test/resources/account/o1-pro/out.yaml new file mode 100644 index 0000000..21d6cbe --- /dev/null +++ b/account/src/test/resources/account/o1-pro/out.yaml @@ -0,0 +1,153 @@ +# Scenario: Valid EUR-to-EUR Transfer (Alice -> Charlie) +# Alice (101013) has 15 EUR, Charlie (101040) has 50 EUR. +# Transfer 10 EUR from Alice to Charlie should succeed. +--- +onTransfer: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:03, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:01:00, + from: 101013, + to: 101040, + currency: EUR, + amount: 10.0, + reference: Groceries Payment + } +} +... +# Scenario: Currency Mismatch Transfer (Alice -> Bob) +# Attempting a transfer of 10 EUR to Bob who holds a USD account. +# This should fail due to currency mismatch. +--- +transferFailed: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:04, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:02:00, + from: 101013, + to: 101025, + currency: EUR, + amount: 10.0, + reference: Dog food + }, + reason: insufficient funds +} +... +# Scenario: Invalid Create Account +# Attempt to create an account with a negative initial balance (-20). +# This should fail with a createAccountFailed event. +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: balance must be positive or zero" +... +# Scenario: Non-Existent Account Transfer +# Attempting to transfer 5 EUR from a non-existent account (999999) to Charlie (101040). +# This should fail with reason "accountNotFound". +--- +transferFailed: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:05, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:04:00, + from: 999999, + to: 101040, + currency: EUR, + amount: 5.0, + reference: Mystery Sender + }, + reason: from account doesn't exist +} +... +# Scenario: Insufficient Funds Transfer +# After the first successful transfer, Alice has 5 EUR left (15 - 10 = 5). +# Attempting to transfer 100 EUR from Alice to Charlie should fail with "insufficientFunds". +--- +transferFailed: { + sender: vault, + target: gw2, + sendingTime: 2023-01-21T11:00:06, + transfer: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T10:05:00, + from: 101013, + to: 101040, + currency: EUR, + amount: 100.0, + reference: Big Purchase + }, + reason: insufficient funds +} +... +# Scenario: Checkpoint +# Requesting a checkpoint after all operations. +# Expected output: startCheckpoint, onCreateAccount events for all existing accounts, +# and endCheckpoint, reflecting the final state of known accounts. +--- +startCheckpoint: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T11:00:00 +} +... +onCreateAccount: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:07, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:00, + name: alice, + account: 101013, + currency: EUR, + balance: 5.0, + overdraft: 0.0 + } +} +... +onCreateAccount: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:08, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:01, + name: bob, + account: 101025, + currency: USD, + balance: 1E3, + overdraft: 0.0 + } +} +... +onCreateAccount: { + sender: vault, + target: gw1, + sendingTime: 2023-01-21T11:00:09, + createAccount: { + sender: gw1, + target: vault, + sendingTime: 2023-01-20T10:00:02, + name: charlie, + account: 101040, + currency: EUR, + balance: 60.0, + overdraft: 0.0 + } +} +... +endCheckpoint: { + sender: gw2, + target: vault, + sendingTime: 2023-01-20T11:00:00 +} +... From 87b7573bb93f5cf55dc8ebdb27617893bc2cb9ad Mon Sep 17 00:00:00 2001 From: Peter Lawrey Date: Tue, 17 Dec 2024 12:51:42 +0000 Subject: [PATCH 08/31] Fix Javadoc --- .../main/java/run/chronicle/account/util/ErrorListener.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/account/src/main/java/run/chronicle/account/util/ErrorListener.java b/account/src/main/java/run/chronicle/account/util/ErrorListener.java index 8305eba..cbd3ae9 100644 --- a/account/src/main/java/run/chronicle/account/util/ErrorListener.java +++ b/account/src/main/java/run/chronicle/account/util/ErrorListener.java @@ -26,14 +26,12 @@ * the impact of GC or IO the monitoring system might have. * *

Example usage: - *

{@code
+ * 
  * public class LoggingErrorListener implements ErrorListener {
- *     @Override
  *     public void jvmError(String msg) {
  *         // Log the error to monitoring system, a file, or console
  *         logger.error(msg);
  *     }
- * }
  * }
* *

By providing different {@code ErrorListener} implementations, the system From ea4b24dfd41f206f434241ca4ebac2d834431bf2 Mon Sep 17 00:00:00 2001 From: Peter Lawrey Date: Wed, 18 Dec 2024 17:09:23 +0000 Subject: [PATCH 09/31] Second iteration of review --- account/README.adoc | 60 ++++- account/list_files_asciidoc.sh | 20 ++ account/pom.xml | 36 +++ account/prompts/improve-code.adoc | 36 ++- account/prompts/improve-test-data.adoc | 42 ++-- account/prompts/requirements.adoc | 10 +- .../account/AccountManagerBenchmarkMain.java | 31 --- .../account/AccountManagerClientMain.java | 1 + .../chronicle/account/benchmark-results.txt | 29 +++ .../account/domain/AccountService.java | 94 ++++++++ .../account/impl/AccountManagerImpl.java | 181 +++----------- .../account/domain/AccountServiceTest.java | 192 +++++++++++++++ .../account/impl/AccountManagerImplTest.java | 220 ++++++++++++++++++ .../resources/account/bard-gen/_setup.yaml | 2 +- .../test/resources/account/bard-gen/in.yaml | 4 +- .../test/resources/account/bard-jinja/in.yaml | 22 +- .../test/resources/account/copilot/in.yaml | 4 +- .../resources/account/gpt-gen/_setup.yaml | 2 +- .../test/resources/account/gpt-gen/in.yaml | 2 +- .../test/resources/account/gpt-jinja/in.yaml | 34 +-- .../test/resources/account/mixed/_setup.yaml | 2 +- .../src/test/resources/account/mixed/in.yaml | 2 +- .../test/resources/account/simple/_setup.yaml | 2 +- .../resources/account/waterfall/_setup.yaml | 2 +- .../test/resources/account/waterfall/in.yaml | 2 +- 25 files changed, 773 insertions(+), 259 deletions(-) create mode 100644 account/list_files_asciidoc.sh create mode 100644 account/src/main/java/run/chronicle/account/benchmark-results.txt create mode 100644 account/src/main/java/run/chronicle/account/domain/AccountService.java create mode 100644 account/src/test/java/run/chronicle/account/domain/AccountServiceTest.java create mode 100644 account/src/test/java/run/chronicle/account/impl/AccountManagerImplTest.java diff --git a/account/README.adoc b/account/README.adoc index 29dfb25..a72e0d1 100644 --- a/account/README.adoc +++ b/account/README.adoc @@ -2,10 +2,17 @@ :author: Peter Lawrey :revdate: 2024-12-16 :revnumber: 1.3 +:toc: left +:toclevels: 3 -This project showcases a low-latency, high-performance account management microservice accessible via TCP or shared memory. Each inbound and outbound message is persisted in a Chronicle Queue, ensuring durability and traceability. The system is designed to produce minimal garbage and reduce latency jitter, making it suitable for high-frequency financial operations. +An event-driven account management system utilising Chronicle libraries for high-performance and low-latency operations. This project demonstrates a reference architecture for creating accounts, performing transfers, and capturing checkpoints of system state, supported by comprehensive testing and benchmarking tools. -Built on Chronicle Queue and leveraging YAML-based input data, this service uses a fluent interface pattern to handle various account operations, such as account creation, fund transfers, and state checkpoints. It also includes benchmarking and testing utilities for performance evaluation. +This project showcases a low-latency, high-performance account management microservice accessible via TCP or shared memory. +Each inbound and outbound message is persisted in a Chronicle Queue, ensuring durability and traceability. +The system is designed to produce minimal garbage and reduce latency jitter, making it suitable for high-frequency financial operations. + +Built on Chronicle Queue and leveraging YAML-based input data, this service uses a fluent interface pattern to handle various account operations, such as account creation, fund transfers, and state checkpoints. +It also includes benchmarking and testing utilities for performance evaluation. == Key Features @@ -23,7 +30,8 @@ Built on Chronicle Queue and leveraging YAML-based input data, this service uses Create new accounts with specified details (name, account ID, currency, initial balance, and optional overdraft). . **Fund Transfers**: -Transfer funds between accounts, validating currencies, balances, and overdrafts. Produces events indicating success or failure. +Transfer funds between accounts, validating currencies, balances, and overdrafts. +Produces events indicating success or failure. . **Checkpointing**: Emit a snapshot of all accounts' current state at a given time for auditing or recovery. @@ -36,15 +44,41 @@ The service can operate in various configurations: * **TCP Client to Shared Memory Service**: Achieves better performance, typically ~10 µs latency. * **Shared Memory Client and Service**: This method achieves the lowest latency, around 1.5 µs for typical operations. -Chronicle Services' `channels` package and Chronicle Queue facilitate fast, low-overhead message passing. State changes are persistently recorded for replay or state restoration. +Chronicle Services' `channels` package and Chronicle Queue facilitate fast, low-overhead message passing. +State changes are persistently recorded for replay or state restoration. + +== Architecture and Key Components + +=== Overview +The system is composed of the following layers: + +* **Input/Output Layer**: Handles external requests (e.g., through Chronicle Channels) and emits responses/events. +* **Service Layer**: Contains domain services (e.g., `AccountService`) that enforce business rules like account creation, validation, currency checks, and balance updates. +* **DTOs (Data Transfer Objects)**: Represent business events such as `CreateAccount`, `Transfer`, `OnTransfer`, etc. DTOs are validated before processing to ensure data integrity. +* **Event-Driven Model**: Uses Chronicle’s method readers/writers and queues to handle requests asynchronously and at low latency. +* **Benchmarking and Testing Tools**: Includes JLBH for microsecond-level latency testing and YAML-driven scenario tests. + +=== Key Classes +* `AccountManagerImpl`: Orchestrates incoming events, delegating domain logic to `AccountService` and communicating results back via `AccountManagerOut`. +* `AccountService`: Encapsulates the domain logic for account management (e.g., validating accounts, performing transfers). +* `DTO Classes (CreateAccount, Transfer, OnTransfer, etc.)`: Define the structure and validation rules for events. +* `BenchmarkMain Classes`: Run performance tests and measure end-to-end latencies under various throughput settings. + +A typical flow: +1. A request (e.g., `createAccount`) arrives via a Chronicle channel. +2. `AccountManagerImpl` receives the request, delegates validation and business logic to `AccountService`. +3. `AccountService` returns success or a failure reason. +4. `AccountManagerImpl` emits corresponding events (e.g., `onCreateAccount` or `createAccountFailed`). === Testability and Benchmarking -A suite of tests and benchmarks (e.g., `AccountManagerBenchmarkMain`) allows easy verification of correctness and performance. You can measure latency, throughput, and other key metrics to ensure the service meets performance goals. +A suite of tests and benchmarks (e.g., `AccountManagerBenchmarkMain`) allows easy verification of correctness and performance. +You can measure latency, throughput, and other key metrics to ensure the service meets performance goals. == Making It Production-Ready with Chronicle Services -To further enhance this solution for production environments, consider using https://chronicle.software/services/[Chronicle Services]. Chronicle Services provides advanced features to ensure high availability, resilience, and smooth operations, including: +To further enhance this solution for production environments, consider using https://chronicle.software/services/[Chronicle Services]. +Chronicle Services provides advanced features to ensure high availability, resilience, and smooth operations, including: * **Failover Support**: If the primary fails, the system automatically switches to a standby service instance, minimizing downtime. * **Process Restarting**: Automatically restart services after unexpected terminations or during scheduled maintenance windows. @@ -66,7 +100,8 @@ Consider running the service on a server with: * **High-Performance Storage (NVMe)**: Reduces jitter from IO operations. * **Fast Memory**: Memory-intensive operations benefit from faster RAM. * **Small Heap Size**: A modest heap (e.g., 2 GB) is sufficient since most data is off-heap. -* **Tuned Eden Space**: A large Eden space can reduce GC frequency. For modest volumes, ~1 GB Eden size may suffice. +* **Tuned Eden Space**: A large Eden space can reduce GC frequency. +For modest volumes, ~1 GB Eden size may suffice. == Network Layouts @@ -112,7 +147,8 @@ This can be benchmarked all-in-one with the command line properties `-Durl=tcp:/ === Example: TCP Client, Shared Memory Service -This approach offers the most distributed option. The typical latencies are around 10 µs with the 99%ile latency not much higher. +This approach offers the most distributed option. +The typical latencies are around 10 µs with the 99%ile latency not much higher. [source,mermaid] .... @@ -141,7 +177,8 @@ This can be benchmarked all-in-one with the command line properties `-Durl=tcp:/ === Example: Shared Memory Client and Service -This approach offers the most distributed option. The typical latencies are under 2 µs with the 99%ile latency about double this. +This approach offers the most distributed option. +The typical latencies are under 2 µs with the 99%ile latency about double this. [source,mermaid] .... @@ -233,8 +270,9 @@ transfer: { == Limitations and Future Work -This demo does not handle all edge cases and lacks a user-friendly interface. Future enhancements may include: +This demo does not handle all edge cases and lacks a user-friendly interface. +Future enhancements may include: * Improved error handling * Better user interfaces or REST/HTTP endpoints -* More robust fault tolerance and recovery strategies, possibly leveraging Chronicle Services further \ No newline at end of file +* More robust fault tolerance and recovery strategies, possibly leveraging Chronicle Services further diff --git a/account/list_files_asciidoc.sh b/account/list_files_asciidoc.sh new file mode 100644 index 0000000..c331765 --- /dev/null +++ b/account/list_files_asciidoc.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash + +# Usage: ./list_files_asciidoc.sh [directory] > output.adoc +# If no directory is provided, the script defaults to the current directory. + +DIR="${1:-.}" + +cd $DIR +echo "= Directory Content" +echo + +# Find all regular files recursively +find "pom.xml" "src" "*.adoc" -type f | grep -v out- | sort | while read -r file; do + echo "== File: $file" + echo "[source]" + echo "----" + cat "$file" + echo "----" + echo +done diff --git a/account/pom.xml b/account/pom.xml index 51b3eb6..ccffd7d 100644 --- a/account/pom.xml +++ b/account/pom.xml @@ -54,5 +54,41 @@ 4.11.0 test + + + + org.junit.jupiter + junit-jupiter-api + 5.10.0 + test + + + org.junit.jupiter + junit-jupiter-engine + 5.10.0 + test + + + + + org.assertj + assertj-core + 3.24.2 + test + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.0.0-M9 + + false + + + + diff --git a/account/prompts/improve-code.adoc b/account/prompts/improve-code.adoc index 4c2340d..e79548d 100644 --- a/account/prompts/improve-code.adoc +++ b/account/prompts/improve-code.adoc @@ -7,32 +7,44 @@ :toc: left :toclevels: 3 -You are given a set of requirements and an existing implementation of an Account Management Service (AMS). The AMS processes account-related commands (events) such as account creation, fund transfers, and checkpoint requests, and produces corresponding success/failure events. The code currently meets the basic functional requirements but can be improved for clarity, maintainability, and robustness. +You are given a set of requirements and an existing implementation of an Account Management Service (AMS). +The AMS processes account-related commands (events) such as account creation, fund transfers, and checkpoint requests, and produces corresponding success/failure events. +The code currently meets the basic functional requirements but can be improved for clarity, maintainability, and robustness. -Your task is to improve selected classes in the codebase. Consider the following options and guidelines: +Your task is to improve selected classes in the codebase. +Consider the following options and guidelines: == Guidelines 1. **Adhere to the Provided Requirements**: -The code must continue to fulfill the requirements specified in the `Account Management Service Requirements` document. Any changes should not break the contract defined there. +The code must continue to fulfill the requirements specified in the `Account Management Service Requirements` document. +Any changes should not break the contract defined there. 2. **Validation and Error Handling**: -Assume that the framework validates that DTOs (Data Transfer Objects) before use. Add a comment showing where validation would otherwise occur. If a command is missing critical fields or contains invalid values, handle it gracefully by producing failure events rather than exceptions visible to callers. +Assume that the framework validates that DTOs (Data Transfer Objects) before use. +Add a comment showing where validation would otherwise occur. +If a command is missing critical fields or contains invalid values, handle it gracefully by producing failure events rather than exceptions visible to callers. 3. **Time Management**: All events should use `SystemTimeProvider.CLOCK.currentTimeNanos()` to set their `sendingTime` fields, ensuring nanosecond-precision wall clock timestamps. 4. **Logging and Comments**: -Add meaningful comments where appropriate to explain the rationale behind certain decisions, especially if the code deviates from typical patterns. Consider using `Jvm.debug()`, `Jvm.warn()`, and `Jvm.error()` for logging. Comments should clarify non-obvious logic, error handling decisions, or performance trade-offs. Do not add comments for trivial logic. +Add meaningful comments where appropriate to explain the rationale behind certain decisions, especially if the code deviates from typical patterns. +Consider using `Jvm.debug()`, `Jvm.warn()`, and `Jvm.error()` for logging. +Comments should clarify non-obvious logic, error handling decisions, or performance trade-offs. +Do not add comments for trivial logic. 5. **Fluent Interfaces and Deep Copies**: -Preserve the fluent interface style for DTO setters to allow method chaining. When storing new accounts, ensure that `CreateAccount` objects are deep copied before saving them to the internal map, as per the requirements. +Preserve the fluent interface style for DTO setters to allow method chaining. +When storing new accounts, ensure that `CreateAccount` objects are deep copied before saving them to the internal map, as per the requirements. 6. **Checkpoints and State Serialization**: -During checkpoint processing, ensure that all currently known accounts are emitted as `onCreateAccount` events. Consider how to handle any edge cases (e.g., empty account lists). +During checkpoint processing, ensure that all currently known accounts are emitted as `onCreateAccount` events. +Consider how to handle any edge cases (e.g., empty account lists). 7. **Readability and Maintainability**: -Consider extracting common logic (e.g., target checks, currency checks, funds checks) into separate helper methods to reduce code repetition. Make sure your class-level and method-level documentation provides a clear picture of what the code does, why, and how it aligns with the requirements. +Consider extracting common logic (e.g., target checks, currency checks, funds checks) into separate helper methods to reduce code repetition. +Make sure your class-level and method-level documentation provides a clear picture of what the code does, why, and how it aligns with the requirements. == Options to Consider @@ -47,12 +59,14 @@ Consider extracting common logic (e.g., target checks, currency checks, funds ch == Deliverables -Improve the existing codebase by addressing the guidelines and options provided. Submit the updated classes with the changes you have made, along with a brief summary of the modifications you implemented and why you chose to make them. +Improve the existing codebase by addressing the guidelines and options provided. +Submit the updated classes with the changes you have made, along with a brief summary of the modifications you implemented and why you chose to make them. == Objective -By following the above guidelines and considering the options, improve the existing codebase to be more robust, understandable, and aligned with the specified requirements. The resulting classes should present a clean, well-documented, and maintainable code structure that clearly communicates their purpose and logic. +By following the above guidelines and considering the options, improve the existing codebase to be more robust, understandable, and aligned with the specified requirements. +The resulting classes should present a clean, well-documented, and maintainable code structure that clearly communicates their purpose and logic. == Code To Improve -Find the code to improve below: \ No newline at end of file +Find the code to improve below: diff --git a/account/prompts/improve-test-data.adoc b/account/prompts/improve-test-data.adoc index 70a6bbc..b79e29e 100644 --- a/account/prompts/improve-test-data.adoc +++ b/account/prompts/improve-test-data.adoc @@ -1,6 +1,7 @@ = Developer Prompt: Improving Test Cases for AMS -Your task is to enhance an existing test configuration that utilizes YAML files for initializing system state (`_setup.yaml`), specifying input commands (`in.yaml`), and verifying expected outcomes (`out.yaml`). The goal is to produce maintainable, clear, and requirements-aligned test cases. +Your task is to enhance an existing test configuration that utilizes YAML files for initializing system state (`_setup.yaml`), specifying input commands (`in.yaml`), and verifying expected outcomes (`out.yaml`). +The goal is to produce maintainable, clear, and requirements-aligned test cases. == Overview @@ -95,35 +96,42 @@ sequenceDiagram == Guidelines 1. **Clarity and Context**: - Add descriptive comments to `_setup.yaml` and `in.yaml` to explain each operation’s intent. In `out.yaml`, reference the input event that caused the output. This makes it easier for other developers to understand the test scenarios at a glance. +Add descriptive comments to `_setup.yaml` and `in.yaml` to explain each operation’s intent. +In `out.yaml`, reference the input event that caused the output. +This makes it easier for other developers to understand the test scenarios at a glance. 2. **Time Management**: - Document that real-time tests should use `SystemTimeProvider.CLOCK.currentTimeNanos()` for `sendingTime`. Though test files may use fixed timestamps, emphasize in comments that production environments rely on `SystemTimeProvider` for consistent, nanosecond-precision timestamps. +Document that real-time tests should use `SystemTimeProvider.CLOCK.currentTimeNanos()` for `sendingTime`. +Though test files may use fixed timestamps, emphasize in comments that production environments rely on `SystemTimeProvider` for consistent, nanosecond-precision timestamps. 3. **Validation Checks**: - Introduce failure scenarios: - * A `createAccount` command with an invalid balance (e.g., negative balance) to produce `createAccountFailed`. - * A `transfer` from a non-existent account or with insufficient funds to produce `transferFailed`. +Introduce failure scenarios: +* A `createAccount` command with an invalid balance (e.g., negative balance) to produce `createAccountFailed`. +* A `transfer` from a non-existent account or with insufficient funds to produce `transferFailed`. In `in.yaml`, comment these scenarios and in `out.yaml`, show the expected failure outputs, including a `reason` field that aligns with the system’s requirements. 4. **Reusability and Maintenance**: - If your setup becomes complex, consider YAML anchors, aliases, or splitting large scenarios into multiple files. Add comments linking tricky scenarios to relevant sections of the requirements document, ensuring future maintainers understand the rationale behind each test. +If your setup becomes complex, consider YAML anchors, aliases, or splitting large scenarios into multiple files. +Add comments linking tricky scenarios to relevant sections of the requirements document, ensuring future maintainers understand the rationale behind each test. 5. **Coverage**: - Include scenarios that cover: - * Multiple successful account creations and transfers. - * At least one invalid `createAccount` scenario. - * At least one invalid `transfer` scenario. - * A `checkPoint` command to verify the sequence of `startCheckpoint`, `onCreateAccount` for each known account, and `endCheckpoint` events. +Include scenarios that cover: +* Multiple successful account creations and transfers. +* At least one invalid `createAccount` scenario. +* At least one invalid `transfer` scenario. +* A `checkPoint` command to verify the sequence of `startCheckpoint`, `onCreateAccount` for each known account, and `endCheckpoint` events. 6. **Naming and Organization**: - Use meaningful and specific operation descriptions. Instead of generic comments, specify the exact accounts, currencies, and reasons. Label scenarios (e.g., "Scenario: Insufficient Funds Transfer") to quickly identify their purpose. +Use meaningful and specific operation descriptions. +Instead of generic comments, specify the exact accounts, currencies, and reasons. +Label scenarios (e.g., "Scenario: Insufficient Funds Transfer") to quickly identify their purpose. == Sections for Setup and Input Data - **Setup Section (`_setup.yaml`)**: - Place all initial state operations here. Add comments that justify these initial states and their relevance to the upcoming tests. +Place all initial state operations here. +Add comments that justify these initial states and their relevance to the upcoming tests. ---- # Example (in `_setup.yaml`): @@ -134,7 +142,8 @@ createAccount: { ... } ---- - **Input Section (`in.yaml`)**: - Define the sequence of commands tested. Include both normal and edge cases, clearly tagging scenarios for quick reference. +Define the sequence of commands tested. +Include both normal and edge cases, clearly tagging scenarios for quick reference. ---- # Example (in `in.yaml`): @@ -146,4 +155,5 @@ transfer: { ... } == Deliverables -Enhance the existing `_setup.yaml` and `in.yaml` files according to the above guidelines. Once updated, provide a brief summary of the changes made and the reasons behind them, focusing on improved clarity, test coverage, and alignment with requirements. +Enhance the existing `_setup.yaml` and `in.yaml` files according to the above guidelines. +Once updated, provide a brief summary of the changes made and the reasons behind them, focusing on improved clarity, test coverage, and alignment with requirements. diff --git a/account/prompts/requirements.adoc b/account/prompts/requirements.adoc index 25cf89a..495d36f 100644 --- a/account/prompts/requirements.adoc +++ b/account/prompts/requirements.adoc @@ -9,13 +9,16 @@ == Introduction -This document specifies the functional requirements for an Account Management Service (AMS) that processes account-related commands and generates corresponding events. The AMS is designed to be driven by incoming commands (events) such as account creation, fund transfers, and checkpoint requests. It produces events to indicate success or failure and to provide state snapshots. +This document specifies the functional requirements for an Account Management Service (AMS) that processes account-related commands and generates corresponding events. +The AMS is designed to be driven by incoming commands (events) such as account creation, fund transfers, and checkpoint requests. +It produces events to indicate success or failure and to provide state snapshots. == Terminology *Account*: A financial store of value characterized by an account number, owner name, currency, balance, and an optional overdraft limit. -*Command*: An inbound request to the system (e.g., create an account, transfer funds, request checkpoint). Commands are modelled as Events in this system. +*Command*: An inbound request to the system (e.g., create an account, transfer funds, request checkpoint). +Commands are modelled as Events in this system. *Event*: An outbound notification from the system that indicates a command's state change, success, or failure. @@ -176,4 +179,5 @@ Events and conditions are explicitly handled in private utility methods (e.g., ` == Conclusion -This document provides a high-level specification of the required functionalities and expected behaviours of the Account Management Service. Implementing these requirements should align with the Java code structure and produce consistent events for all supported operations. \ No newline at end of file +This document provides a high-level specification of the required functionalities and expected behaviours of the Account Management Service. +Implementing these requirements should align with the Java code structure and produce consistent events for all supported operations. diff --git a/account/src/main/java/run/chronicle/account/AccountManagerBenchmarkMain.java b/account/src/main/java/run/chronicle/account/AccountManagerBenchmarkMain.java index b8ce7f7..c355f24 100644 --- a/account/src/main/java/run/chronicle/account/AccountManagerBenchmarkMain.java +++ b/account/src/main/java/run/chronicle/account/AccountManagerBenchmarkMain.java @@ -25,37 +25,6 @@ import static net.openhft.chronicle.core.time.SystemTimeProvider.CLOCK; -/* --Xmx64m -Dthroughput=100000 -DrunTime=30 -Dbuffered=false -Durl=tcp://localhost:1248 -DaccountForCoordinatedOmission=false --------------------------------- SUMMARY (end to end) us ------------------------------------------- -Percentile run1 run2 run3 run4 run5 % Variation -50.0: 10.99 10.99 11.02 11.02 10.99 0.19 -90.0: 18.02 17.89 15.70 11.22 11.12 28.86 -99.0: 20.96 19.94 15.86 15.86 15.79 14.89 -99.7: 34.11 21.02 16.11 15.92 15.89 17.73 -99.9: 42.18 22.62 16.67 16.34 16.18 21.00 -99.97: 2021.38 26.08 18.21 17.44 17.25 25.45 -99.99: 5480.45 60.48 44.48 22.82 47.68 52.39 -99.997: 6938.62 332.29 426.50 80.26 451.07 75.49 -worst: 7593.98 728.06 820.22 303.62 838.66 54.02 - -Windows 11 laptop, i7-1360P, Java 11 --Dthroughput=20000 -Durl=internal:// --------------------------------- SUMMARY (end to end) us ------------------------------------------- -Percentile run1 run2 run3 run4 run5 % Variation -50.0: 1.60 1.70 1.60 1.60 1.60 3.84 -90.0: 2.10 2.10 2.10 2.10 2.00 3.16 -99.0: 23.39 22.62 22.18 20.70 17.12 17.65 -99.7: 155.90 168.19 191.74 177.92 170.24 8.54 -99.9: 857.09 723.97 824.32 816.13 764.93 8.46 - --Dthroughput=20000 --------------------------------- SUMMARY (end to end) us ------------------------------------------- -Percentile run1 run2 run3 run4 run5 % Variation -50.0: 24.93 24.67 24.93 24.80 24.93 0.69 -90.0: 37.95 35.26 38.34 35.52 35.14 5.72 -99.0: 1198.08 250.62 1243.14 469.50 477.70 72.53 - */ @SuppressWarnings("deprecation") public class AccountManagerBenchmarkMain { public static final int THROUGHPUT = Integer.getInteger("throughput", OS.isLinux() ? 100_000 : 10_000); diff --git a/account/src/main/java/run/chronicle/account/AccountManagerClientMain.java b/account/src/main/java/run/chronicle/account/AccountManagerClientMain.java index c55f18a..69cb476 100644 --- a/account/src/main/java/run/chronicle/account/AccountManagerClientMain.java +++ b/account/src/main/java/run/chronicle/account/AccountManagerClientMain.java @@ -14,6 +14,7 @@ import run.chronicle.account.util.LogsAccountManagerOut; import java.util.concurrent.atomic.AtomicBoolean; + /** * This class acts as the main entry point for the AccountManagerClient. * It creates a client which connects to a Chronicle server and performs various actions. diff --git a/account/src/main/java/run/chronicle/account/benchmark-results.txt b/account/src/main/java/run/chronicle/account/benchmark-results.txt new file mode 100644 index 0000000..45fb9d1 --- /dev/null +++ b/account/src/main/java/run/chronicle/account/benchmark-results.txt @@ -0,0 +1,29 @@ +-Xmx64m -Dthroughput=100000 -DrunTime=30 -Dbuffered=false -Durl=tcp://localhost:1248 -DaccountForCoordinatedOmission=false +-------------------------------- SUMMARY (end to end) us ------------------------------------------- +Percentile run1 run2 run3 run4 run5 % Variation +50.0: 10.99 10.99 11.02 11.02 10.99 0.19 +90.0: 18.02 17.89 15.70 11.22 11.12 28.86 +99.0: 20.96 19.94 15.86 15.86 15.79 14.89 +99.7: 34.11 21.02 16.11 15.92 15.89 17.73 +99.9: 42.18 22.62 16.67 16.34 16.18 21.00 +99.97: 2021.38 26.08 18.21 17.44 17.25 25.45 +99.99: 5480.45 60.48 44.48 22.82 47.68 52.39 +99.997: 6938.62 332.29 426.50 80.26 451.07 75.49 +worst: 7593.98 728.06 820.22 303.62 838.66 54.02 + +Windows 11 laptop, i7-1360P, Java 11 +-Dthroughput=20000 -Durl=internal:// +-------------------------------- SUMMARY (end to end) us ------------------------------------------- +Percentile run1 run2 run3 run4 run5 % Variation +50.0: 1.60 1.70 1.60 1.60 1.60 3.84 +90.0: 2.10 2.10 2.10 2.10 2.00 3.16 +99.0: 23.39 22.62 22.18 20.70 17.12 17.65 +99.7: 155.90 168.19 191.74 177.92 170.24 8.54 +99.9: 857.09 723.97 824.32 816.13 764.93 8.46 + +-Dthroughput=20000 +-------------------------------- SUMMARY (end to end) us ------------------------------------------- +Percentile run1 run2 run3 run4 run5 % Variation +50.0: 24.93 24.67 24.93 24.80 24.93 0.69 +90.0: 37.95 35.26 38.34 35.52 35.14 5.72 +99.0: 1198.08 250.62 1243.14 469.50 477.70 72.53 diff --git a/account/src/main/java/run/chronicle/account/domain/AccountService.java b/account/src/main/java/run/chronicle/account/domain/AccountService.java new file mode 100644 index 0000000..5f42c0e --- /dev/null +++ b/account/src/main/java/run/chronicle/account/domain/AccountService.java @@ -0,0 +1,94 @@ +package run.chronicle.account.domain; + +import net.openhft.chronicle.core.io.InvalidMarshallableException; +import run.chronicle.account.dto.CreateAccount; +import run.chronicle.account.dto.Transfer; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Encapsulates all domain logic related to account creation, validation, and funds transfers. + * This service is responsible for: + * - Managing the in-memory store of accounts. + * - Validating currency, balances, and overdraft limits. + * - Updating account balances upon successful transfers. + */ +public class AccountService { + + // In-memory account store + private final Map accountsMap = new LinkedHashMap<>(); + + /** + * Attempts to create a new account. Throws an InvalidMarshallableException if invalid, + * or returns a reason string if creation should fail due to domain rules. + */ + public String tryCreateAccount(CreateAccount createAccount, long expectedTargetId) throws InvalidMarshallableException { + // Validate DTO properties + createAccount.validate(); + + if (createAccount.target() != expectedTargetId) { + return "target mismatch"; + } + + if (!(createAccount.balance() >= 0)) { + return "invalid balance"; + } + + Long accountNumber = createAccount.account(); + if (accountsMap.containsKey(accountNumber)) { + return "account already exists"; + } + + // If valid, store the account + accountsMap.put(accountNumber, createAccount.deepCopy()); + return null; // Indicates success + } + + /** + * Attempts to perform a transfer. Returns a reason string if transfer fails domain checks, + * or null if the transfer is successful. + */ + public String tryTransfer(Transfer transfer, long expectedTargetId) throws InvalidMarshallableException { + // Validate DTO properties + transfer.validate(); + + if (transfer.target() != expectedTargetId) { + return "target mismatch"; + } + + CreateAccount fromAccount = accountsMap.get(transfer.from()); + if (fromAccount == null) { + return "from account doesn't exist"; + } + if (fromAccount.currency() != transfer.currency()) { + return "from account currency doesn't match"; + } + + double amount = transfer.amount(); + if (fromAccount.balance() + fromAccount.overdraft() < amount) { + return "insufficient funds"; + } + + CreateAccount toAccount = accountsMap.get(transfer.to()); + if (toAccount == null) { + return "to account doesn't exist"; + } + if (toAccount.currency() != transfer.currency()) { + return "to account currency doesn't match"; + } + + // Perform the transfer + fromAccount.balance(fromAccount.balance() - amount); + toAccount.balance(toAccount.balance() + amount); + + return null; // Indicates success + } + + /** + * Provides access to all accounts for checkpoint operations. + */ + public Map getAllAccounts() { + return accountsMap; + } +} diff --git a/account/src/main/java/run/chronicle/account/impl/AccountManagerImpl.java b/account/src/main/java/run/chronicle/account/impl/AccountManagerImpl.java index b64fa3b..d233661 100644 --- a/account/src/main/java/run/chronicle/account/impl/AccountManagerImpl.java +++ b/account/src/main/java/run/chronicle/account/impl/AccountManagerImpl.java @@ -22,34 +22,21 @@ import net.openhft.chronicle.wire.SelfDescribingMarshallable; import run.chronicle.account.api.AccountManagerIn; import run.chronicle.account.api.AccountManagerOut; +import run.chronicle.account.domain.AccountService; import run.chronicle.account.dto.*; -import java.util.LinkedHashMap; -import java.util.Map; - import static net.openhft.chronicle.core.time.SystemTimeProvider.CLOCK; /** - * Implementation of the AccountManagerIn interface, handling: - *

    - *
  • Account creation
  • - *
  • Fund transfers
  • - *
  • Checkpoints (state snapshots)
  • - *
- * - * This service receives commands (events) and produces corresponding result events. - * It stores account states and ensures validation according to the defined requirements. + * This class now primarily orchestrates the handling of events, delegating all + * domain logic (account validation, fund transfers, currency checks) to the + * AccountService. */ -public class AccountManagerImpl - extends SelfDescribingMarshallable - implements AccountManagerIn { - private transient final AccountManagerOut out; - - // Store accounts keyed by account number - // TODO Use a primitive lop to account map - private final Map accountsMap = new LinkedHashMap<>(); +public class AccountManagerImpl extends SelfDescribingMarshallable implements AccountManagerIn { + private final AccountManagerOut out; + private final AccountService accountService; - // Event instances reused to reduce object creation + // Reusable event objects private final OnCreateAccount onCreateAccount = new OnCreateAccount(); private final CreateAccountFailed createAccountFailed = new CreateAccountFailed(); private final OnTransfer onTransfer = new OnTransfer(); @@ -57,157 +44,55 @@ public class AccountManagerImpl private long id; - /** - * The constructor for the AccountManagerImpl class. - * - * @param out An instance of AccountManagerOut, which handles output events. - */ public AccountManagerImpl(AccountManagerOut out) { + this(out, new AccountService()); + } + + public AccountManagerImpl(AccountManagerOut out, AccountService accountService) { this.out = out; + this.accountService = accountService; } - /** - * Sets the unique ID for this AccountManager instance, used to match incoming commands. - * - * @param id A long representing the id to be set. - * @return This AccountManagerImpl instance. - */ public AccountManagerImpl id(long id) { this.id = id; return this; } - /** - * Handles account creation. - * - * @param createAccount An instance of CreateAccount containing details of the account to be created. - * @throws InvalidMarshallableException If there's an error during the process. - */ @Override public void createAccount(CreateAccount createAccount) throws InvalidMarshallableException { - // Assume createAccount.validate() has been called - - // Verify if the account creation request is intended for this instance by checking the target of the request against the id of this instance - // If they don't match, a failure message is sent with the reason "target mismatch" and the method returns - if (createAccount.target() != id) { - sendCreateAccountFailed(createAccount, "target mismatch"); - return; - } - - // Verify if the initial balance for the account is greater than or equal to 0 - // If it isn't, a failure message is sent with the reason "invalid balance" and the method returns - // NOTE this is !(x >= 0) required to reject NaN - if (!(createAccount.balance() >= 0)) { - sendCreateAccountFailed(createAccount, "invalid balance"); - return; - } - - // Get the account number for the new account - Long account = createAccount.account(); - - // Check if the account already exists in the accounts map - // If it does, a failure message is sent with the reason "account already exists" and the method returns - if (accountsMap.containsKey(account)) { - sendCreateAccountFailed(createAccount, "account already exists"); - return; + String failureReason = accountService.tryCreateAccount(createAccount, id); + if (failureReason == null) { + sendOnCreateAccount(createAccount); + } else { + sendCreateAccountFailed(createAccount, failureReason); } - - // If all checks pass, create a copy of the CreateAccount object and add it to the accounts map - // This is to ensure we retain a version of the data, even if the original CreateAccount object changes later - accountsMap.put(account, createAccount.deepCopy()); - - // Send a confirmation message indicating the account was successfully created - sendOnCreateAccount(createAccount); } - /** - * Processes a transfer command. Validates input, checks conditions (existence of accounts, - * matching currencies, available funds), updates state if valid, and emits success or failure events. - * - * @param transfer An instance of Transfer containing details of the transfer to be performed. - */ @Override public void transfer(Transfer transfer) { - // Assume transfer.validate() has been called - - // Verify if the transfer is intended for this instance by checking the target of the transfer against the id of this instance - // If it doesn't match, a failure message is sent with the reason "target mismatch" and the method returns - if (transfer.target() != id) { - sendTransferFailed(transfer, "target mismatch"); - return; - } - - // Get the account from which funds are to be transferred - // If it doesn't exist, a failure message is sent with the reason "from account doesn't exist" and the method returns - CreateAccount fromAccount = accountsMap.get(transfer.from()); - if (fromAccount == null) { - sendTransferFailed(transfer, "from account doesn't exist"); - return; - } - - // Check if the currency of the transfer matches the currency of the "from" account - // If they don't match, a failure message is sent with the reason "from account currency doesn't match" and the method returns - if (fromAccount.currency() != transfer.currency()) { - sendTransferFailed(transfer, "from account currency doesn't match"); - return; - } - - double amount = transfer.amount(); - - // Check if the balance of the "from" account is sufficient to perform the transfer - // If it isn't, a failure message is sent with the reason "insufficient funds" and the method returns - if (fromAccount.balance() + fromAccount.overdraft() < amount) { - sendTransferFailed(transfer, "insufficient funds"); - return; - } - - // Get the account to which funds are to be transferred - // If it doesn't exist, a failure message is sent with the reason "to account doesn't exist" and the method returns - CreateAccount toAccount = accountsMap.get(transfer.to()); - if (toAccount == null) { - sendTransferFailed(transfer, "to account doesn't exist"); - return; + try { + String failureReason = accountService.tryTransfer(transfer, id); + if (failureReason == null) { + sendOnTransfer(transfer); + } else { + sendTransferFailed(transfer, failureReason); + } + } catch (InvalidMarshallableException e) { + sendTransferFailed(transfer, e.getMessage()); } - - // Check if the currency of the transfer matches the currency of the "to" account - // If they don't match, a failure message is sent with the reason "to account currency doesn't match" and the method returns - if (toAccount.currency() != transfer.currency()) { - sendTransferFailed(transfer, "to account currency doesn't match"); - return; - } - - // Perform the transfer: deduct the amount from the "from" account and add it to the "to" account - fromAccount.balance(fromAccount.balance() - amount); - toAccount.balance(toAccount.balance() + amount); - - // Send a confirmation message indicating the transfer was successful - sendOnTransfer(transfer); } - /** - * Handles checkpointing in the account management system. - * - * @param checkPoint An instance of CheckPoint which provides details of the checkpoint. - */ @Override public void checkPoint(CheckPoint checkPoint) { - // Check if the checkpoint target matches the ID of this instance. If it does not match, ignore this checkpoint. - if (checkPoint.target() != id) - return; // ignored + if (checkPoint.target() != id) { + // Ignoring checkpoint as target does not match + return; + } - // Start the checkpoint process. This is typically when we serialize and store the state of the system - // to an output stream (out). CheckPoint instance contains details about the checkpoint. out.startCheckpoint(checkPoint); - - // Iterate over all accounts in the accountsMap. For each account, send a "create account" event - // This essentially stores the current state of each account to the output stream. This is useful in case - // we need to restore the state of the system from this checkpoint in the future. - for (CreateAccount createAccount : accountsMap.values()) { - sendOnCreateAccount(createAccount); + for (CreateAccount ca : accountService.getAllAccounts().values()) { + sendOnCreateAccount(ca); } - - // End the checkpoint process. This is typically when we complete the serialization of the system state - // and finalize the checkpoint data in the output stream. out.endCheckpoint(checkPoint); } diff --git a/account/src/test/java/run/chronicle/account/domain/AccountServiceTest.java b/account/src/test/java/run/chronicle/account/domain/AccountServiceTest.java new file mode 100644 index 0000000..37d54d4 --- /dev/null +++ b/account/src/test/java/run/chronicle/account/domain/AccountServiceTest.java @@ -0,0 +1,192 @@ +package run.chronicle.account.domain; + +import net.openhft.chronicle.core.io.InvalidMarshallableException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import run.chronicle.account.dto.CreateAccount; +import run.chronicle.account.dto.Transfer; +import net.openhft.chronicle.wire.converter.ShortText; +import net.openhft.chronicle.wire.converter.NanoTime; + +import static net.openhft.chronicle.bytes.Bytes.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class AccountServiceTest { + private static final long VAULT_ID = ShortText.INSTANCE.parse("vault"); + private static final int EUR = (int) ShortText.INSTANCE.parse("EUR"); + private static final int USD = (int) ShortText.INSTANCE.parse("USD"); + + private AccountService accountService; + + @BeforeEach + void setUp() { + accountService = new AccountService(); + } + + @Test + void shouldCreateAccountSuccessfully() throws InvalidMarshallableException { + CreateAccount ca = validCreateAccount("alice", 101013, EUR, 100, 10); + String failureReason = accountService.tryCreateAccount(ca, VAULT_ID); + + assertThat(failureReason).isNull(); + assertThat(accountService.getAllAccounts()).containsKey(101013L); + } + + @Test + void shouldFailToCreateAccountIfTargetMismatch() throws InvalidMarshallableException { + CreateAccount ca = validCreateAccount("alice", 101013, EUR, 100, 10); + String failureReason = accountService.tryCreateAccount(ca, ShortText.INSTANCE.parse("other")); + + assertThat(failureReason).isEqualTo("target mismatch"); + assertThat(accountService.getAllAccounts()).doesNotContainKey(101013L); + } + + @Test + void shouldFailToCreateAccountIfAlreadyExists() throws InvalidMarshallableException { + CreateAccount ca1 = validCreateAccount("charlie", 101040, EUR, 50, 0); + CreateAccount ca2 = validCreateAccount("charlie2", 101040, EUR, 60, 0); + + accountService.tryCreateAccount(ca1, VAULT_ID); + String failureReason = accountService.tryCreateAccount(ca2, VAULT_ID); + + assertThat(failureReason).isEqualTo("account already exists"); + assertThat(accountService.getAllAccounts()).hasSize(1); + } + + @Test + void shouldTransferFundsSuccessfully() throws InvalidMarshallableException { + CreateAccount from = validCreateAccount("alice", 101013, EUR, 100, 10); + CreateAccount to = validCreateAccount("bob", 101025, EUR, 50, 0); + + accountService.tryCreateAccount(from, VAULT_ID); + accountService.tryCreateAccount(to, VAULT_ID); + + Transfer transfer = validTransfer(101013, 101025, EUR, 30, "Payment"); + String failureReason = accountService.tryTransfer(transfer, VAULT_ID); + + assertThat(failureReason).isNull(); + assertThat(accountService.getAllAccounts().get(101013L).balance()).isEqualTo(70.0); + assertThat(accountService.getAllAccounts().get(101025L).balance()).isEqualTo(80.0); + } + + @Test + void shouldFailTransferIfTargetMismatch() throws InvalidMarshallableException { + CreateAccount from = validCreateAccount("alice", 101013, EUR, 100, 10); + CreateAccount to = validCreateAccount("bob", 101025, EUR, 50, 0); + + accountService.tryCreateAccount(from, VAULT_ID); + accountService.tryCreateAccount(to, VAULT_ID); + + Transfer transfer = validTransfer(101013, 101025, EUR, 10, "Test"); + String failureReason = accountService.tryTransfer(transfer, ShortText.INSTANCE.parse("other")); + + assertThat(failureReason).isEqualTo("target mismatch"); + } + + @Test + void shouldFailTransferIfFromAccountDoesNotExist() throws InvalidMarshallableException { + CreateAccount to = validCreateAccount("bob", 101025, EUR, 50, 0); + accountService.tryCreateAccount(to, VAULT_ID); + + Transfer transfer = validTransfer(999999, 101025, EUR, 10, "Test"); + String failureReason = accountService.tryTransfer(transfer, VAULT_ID); + + assertThat(failureReason).isEqualTo("from account doesn't exist"); + } + + @Test + void shouldFailTransferIfToAccountDoesNotExist() throws InvalidMarshallableException { + CreateAccount from = validCreateAccount("alice", 101013, EUR, 100, 10); + accountService.tryCreateAccount(from, VAULT_ID); + + Transfer transfer = validTransfer(101013, 999999, EUR, 10, "Test"); + String failureReason = accountService.tryTransfer(transfer, VAULT_ID); + + assertThat(failureReason).isEqualTo("to account doesn't exist"); + } + + @Test + void shouldFailTransferIfCurrencyMismatch() throws InvalidMarshallableException { + CreateAccount from = validCreateAccount("alice", 101013, EUR, 100, 10); + CreateAccount to = validCreateAccount("bob", 101025, USD, 50, 0); + + accountService.tryCreateAccount(from, VAULT_ID); + accountService.tryCreateAccount(to, VAULT_ID); + + Transfer transfer = validTransfer(101013, 101025, EUR, 10, "Mismatched Currency"); + String failureReason = accountService.tryTransfer(transfer, VAULT_ID); + + assertThat(failureReason).isEqualTo("to account currency doesn't match"); + } + + @Test + void shouldFailTransferIfInsufficientFunds() throws InvalidMarshallableException { + CreateAccount from = validCreateAccount("alice", 101013, EUR, 10, 0); + CreateAccount to = validCreateAccount("charlie", 101040, EUR, 50, 0); + + accountService.tryCreateAccount(from, VAULT_ID); + accountService.tryCreateAccount(to, VAULT_ID); + + Transfer transfer = validTransfer(101013, 101040, EUR, 100, "Too Big"); + String failureReason = accountService.tryTransfer(transfer, VAULT_ID); + + assertThat(failureReason).isEqualTo("insufficient funds"); + } + + @Test + void shouldThrowExceptionForInvalidCreateAccountDto() { + CreateAccount invalidDto = new CreateAccount() + .sender(0) // invalid sender + .target(VAULT_ID) + .sendingTime(NanoTime.INSTANCE.parse("2023-01-20T10:00:00")) + .name("invalid") + .account(0) // invalid account + .currency(EUR) + .balance(10); + + assertThatThrownBy(() -> accountService.tryCreateAccount(invalidDto, VAULT_ID)) + .isInstanceOf(InvalidMarshallableException.class); + } + + @Test + void shouldThrowExceptionForInvalidTransferDto() { + Transfer invalidTransfer = new Transfer() + .sender(0) // invalid sender + .target(VAULT_ID) + .sendingTime(NanoTime.INSTANCE.parse("2023-01-20T10:00:00")) + .from(0) // invalid from + .to(101025) + .currency(EUR) + .amount(10) + .reference(from("Test")); + + assertThatThrownBy(() -> accountService.tryTransfer(invalidTransfer, VAULT_ID)) + .isInstanceOf(InvalidMarshallableException.class); + } + + // Helper methods to create valid DTOs + private CreateAccount validCreateAccount(String name, long accountNum, int currency, double balance, double overdraft) { + return new CreateAccount() + .sender(ShortText.INSTANCE.parse("gw1")) + .target(VAULT_ID) + .sendingTime(NanoTime.INSTANCE.parse("2023-01-20T10:00:00")) + .name(name) + .account(accountNum) + .currency(currency) + .balance(balance) + .overdraft(overdraft); + } + + private Transfer validTransfer(long from, long to, int currency, double amount, String reference) { + return new Transfer() + .sender(ShortText.INSTANCE.parse("gw2")) + .target(VAULT_ID) + .sendingTime(NanoTime.INSTANCE.parse("2023-01-20T10:01:00")) + .from(from) + .to(to) + .currency(currency) + .amount(amount) + .reference(from(reference)); + } +} diff --git a/account/src/test/java/run/chronicle/account/impl/AccountManagerImplTest.java b/account/src/test/java/run/chronicle/account/impl/AccountManagerImplTest.java new file mode 100644 index 0000000..bbadd62 --- /dev/null +++ b/account/src/test/java/run/chronicle/account/impl/AccountManagerImplTest.java @@ -0,0 +1,220 @@ +package run.chronicle.account.impl; + +import net.openhft.chronicle.core.io.InvalidMarshallableException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import run.chronicle.account.api.AccountManagerOut; +import run.chronicle.account.domain.AccountService; +import run.chronicle.account.dto.*; +import net.openhft.chronicle.wire.converter.ShortText; +import net.openhft.chronicle.wire.converter.NanoTime; + +import java.util.LinkedHashMap; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.*; + +class AccountManagerImplTest { + + private static final long VAULT_ID = ShortText.INSTANCE.parse("vault"); + private static final long SENDER_ID = ShortText.INSTANCE.parse("gw1"); + private static final int EUR = (int) ShortText.INSTANCE.parse("EUR"); + private static final int USD = (int) ShortText.INSTANCE.parse("USD"); + + private AccountManagerOut out; + private AccountService accountService; + private AccountManagerImpl manager; + + @BeforeEach + void setUp() { + out = mock(AccountManagerOut.class); + accountService = mock(AccountService.class); + manager = new AccountManagerImpl(out, accountService).id(VAULT_ID); + } + + @Test + void createAccount_Success() throws InvalidMarshallableException { + CreateAccount ca = validCreateAccount("alice", 101013, EUR, 100, 10); + + when(accountService.tryCreateAccount(ca, VAULT_ID)).thenReturn(null); + + manager.createAccount(ca); + + // Verify onCreateAccount event was sent + ArgumentCaptor captor = ArgumentCaptor.forClass(OnCreateAccount.class); + verify(out).onCreateAccount(captor.capture()); + OnCreateAccount event = captor.getValue(); + assertThat(event.createAccount()).isEqualTo(ca); + assertThat(event.sender()).isEqualTo(VAULT_ID); + assertThat(event.target()).isEqualTo(SENDER_ID); + } + + @Test + void createAccount_Failure() throws InvalidMarshallableException { + CreateAccount ca = validCreateAccount("bob", 101025, EUR, -1, 0); + + when(accountService.tryCreateAccount(ca, VAULT_ID)).thenReturn("invalid balance"); + + manager.createAccount(ca); + + // Verify createAccountFailed event was sent + ArgumentCaptor captor = ArgumentCaptor.forClass(CreateAccountFailed.class); + verify(out).createAccountFailed(captor.capture()); + CreateAccountFailed event = captor.getValue(); + assertThat(event.createAccount()).isEqualTo(ca); + assertThat(event.reason()).isEqualTo("invalid balance"); + assertThat(event.sender()).isEqualTo(VAULT_ID); + assertThat(event.target()).isEqualTo(SENDER_ID); + } + + @Test + void createAccount_InvalidDtoThrows() throws InvalidMarshallableException { + CreateAccount invalidDto = new CreateAccount() + .sender(0) // invalid sender + .target(VAULT_ID) + .sendingTime(NanoTime.INSTANCE.parse("2023-01-20T10:00:00")) + .name("invalid") + .account(0) // invalid account + .currency(EUR) + .balance(10); + + // Simulate that AccountService will throw InvalidMarshallableException + doThrow(new InvalidMarshallableException("sender must be set")) + .when(accountService).tryCreateAccount(invalidDto, VAULT_ID); + + try { + manager.createAccount(invalidDto); + } catch (InvalidMarshallableException e) { + // expected + } + + // Since an exception was thrown, no events should be emitted + verifyNoInteractions(out); + } + + @Test + void transfer_Success() throws InvalidMarshallableException { + Transfer transfer = validTransfer(101013, 101025, EUR, 50, "Payment"); + + when(accountService.tryTransfer(transfer, VAULT_ID)).thenReturn(null); + + manager.transfer(transfer); + + // Verify onTransfer event was sent + ArgumentCaptor captor = ArgumentCaptor.forClass(OnTransfer.class); + verify(out).onTransfer(captor.capture()); + OnTransfer event = captor.getValue(); + assertThat(event.transfer()).isEqualTo(transfer); + assertThat(event.sender()).isEqualTo(VAULT_ID); + assertThat(event.target()).isEqualTo(transfer.sender()); + } + + @Test + void transfer_Failure() throws InvalidMarshallableException { + Transfer transfer = validTransfer(101013, 101025, USD, 1000, "HugePayment"); + + when(accountService.tryTransfer(transfer, VAULT_ID)).thenReturn("insufficient funds"); + + manager.transfer(transfer); + + // Verify transferFailed event was sent + ArgumentCaptor captor = ArgumentCaptor.forClass(TransferFailed.class); + verify(out).transferFailed(captor.capture()); + TransferFailed event = captor.getValue(); + assertThat(event.reason()).isEqualTo("insufficient funds"); + assertThat(event.transfer()).isEqualTo(transfer); + assertThat(event.sender()).isEqualTo(VAULT_ID); + assertThat(event.target()).isEqualTo(transfer.sender()); + } + + @Test + void transfer_InvalidDtoThrows() throws InvalidMarshallableException { + Transfer invalidTransfer = new Transfer() + .sender(0) // invalid sender + .target(VAULT_ID) + .sendingTime(NanoTime.INSTANCE.parse("2023-01-20T10:01:00")) + .from(0) // invalid from + .to(101025) + .currency(EUR) + .amount(10) + .reference(net.openhft.chronicle.bytes.Bytes.from("Test")); + + doThrow(new InvalidMarshallableException("sender must be set")) + .when(accountService).tryTransfer(invalidTransfer, VAULT_ID); + + manager.transfer(invalidTransfer); + + // Verify transferFailed event due to exception in service + ArgumentCaptor captor = ArgumentCaptor.forClass(TransferFailed.class); + verify(out).transferFailed(captor.capture()); + TransferFailed event = captor.getValue(); + assertThat(event.reason()).contains("sender must be set"); + assertThat(event.sender()).isEqualTo(VAULT_ID); + assertThat(event.target()).isEqualTo(invalidTransfer.sender()); // which is 0 + } + + @Test + void checkPoint_SameTarget() { + CheckPoint cp = new CheckPoint() + .sender(ShortText.INSTANCE.parse("gw2")) + .target(VAULT_ID) + .sendingTime(NanoTime.INSTANCE.parse("2023-01-20T11:00:00")); + + Map accounts = new LinkedHashMap<>(); + CreateAccount ca = validCreateAccount("alice", 101013, EUR, 50, 0); + accounts.put(101013L, ca); + + when(accountService.getAllAccounts()).thenReturn(accounts); + + manager.checkPoint(cp); + + // Verify startCheckpoint, onCreateAccount for each account, and endCheckpoint + verify(out).startCheckpoint(cp); + ArgumentCaptor captor = ArgumentCaptor.forClass(OnCreateAccount.class); + verify(out).onCreateAccount(captor.capture()); + OnCreateAccount event = captor.getValue(); + assertThat(event.createAccount()).isEqualTo(ca); + verify(out).endCheckpoint(cp); + } + + @Test + void checkPoint_DifferentTargetIgnored() { + CheckPoint cp = new CheckPoint() + .sender(ShortText.INSTANCE.parse("gw2")) + .target(ShortText.INSTANCE.parse("other")) + .sendingTime(NanoTime.INSTANCE.parse("2023-01-20T11:00:00")); + + manager.checkPoint(cp); + + // Different target means no interaction + verifyNoInteractions(out); + verifyNoInteractions(accountService); + } + + // Helper methods + private CreateAccount validCreateAccount(String name, long accountNum, int currency, double balance, double overdraft) { + return new CreateAccount() + .sender(SENDER_ID) + .target(VAULT_ID) + .sendingTime(NanoTime.INSTANCE.parse("2023-01-20T10:00:00")) + .name(name) + .account(accountNum) + .currency(currency) + .balance(balance) + .overdraft(overdraft); + } + + private Transfer validTransfer(long from, long to, int currency, double amount, String reference) { + return new Transfer() + .sender(ShortText.INSTANCE.parse("gw2")) + .target(VAULT_ID) + .sendingTime(NanoTime.INSTANCE.parse("2023-01-20T10:01:00")) + .from(from) + .to(to) + .currency(currency) + .amount(amount) + .reference(net.openhft.chronicle.bytes.Bytes.from(reference)); + } +} diff --git a/account/src/test/resources/account/bard-gen/_setup.yaml b/account/src/test/resources/account/bard-gen/_setup.yaml index b9c892c..f7772e8 100755 --- a/account/src/test/resources/account/bard-gen/_setup.yaml +++ b/account/src/test/resources/account/bard-gen/_setup.yaml @@ -62,4 +62,4 @@ transfer: { amount: 10, reference: Cat food } -... \ No newline at end of file +... diff --git a/account/src/test/resources/account/bard-gen/in.yaml b/account/src/test/resources/account/bard-gen/in.yaml index b6e7d22..3c0d878 100755 --- a/account/src/test/resources/account/bard-gen/in.yaml +++ b/account/src/test/resources/account/bard-gen/in.yaml @@ -1,4 +1,4 @@ -{# Generated by Bard #} +{ # Generated by Bard #} --- # This operation creates an account for Charlie with account number 101037. createAccount: { @@ -48,4 +48,4 @@ checkPoint: { target: vault, sendingTime: 2023-01-20T11:00:00, } -... \ No newline at end of file +... diff --git a/account/src/test/resources/account/bard-jinja/in.yaml b/account/src/test/resources/account/bard-jinja/in.yaml index 957115f..035b33c 100755 --- a/account/src/test/resources/account/bard-jinja/in.yaml +++ b/account/src/test/resources/account/bard-jinja/in.yaml @@ -1,28 +1,30 @@ -{# Generated by Bard #} +{ # Generated by Bard #} --- # This operation creates 10 accounts. -{% for i in range(10, 20) %} + { % for i in range(10, 20) % } createAccount: { sender: gw1, target: vault, - sendingTime: 2023-01-20T10:00:{{ i }}, + sendingTime: + 2023-01-20T10:00: { { i } }, name: "account_{{ i }}", - account: {{ i }}, + account: { { i } }, currency: EUR, balance: 100 } -{% endfor %} + { % endfor % } ... --- # This operation creates 10 transfers between the accounts. -{% for i in range(10, 20) %} + { % for i in range(10, 20) % } transfer: { - sender: {{ i }}, - target: {{ i + 1 }}, - sendingTime: 2023-01-20T10:00: + {{ i }}, + sender: { { i } }, + target: { { i + 1 } }, + sendingTime: + 2023-01-20T10:00: + { { i } }, currency: EUR, amount: 10, reference: "transfer_{{ i }}" } -{% endfor %} + { % endfor % } ... diff --git a/account/src/test/resources/account/copilot/in.yaml b/account/src/test/resources/account/copilot/in.yaml index d4c79ec..21905bf 100755 --- a/account/src/test/resources/account/copilot/in.yaml +++ b/account/src/test/resources/account/copilot/in.yaml @@ -1,4 +1,4 @@ -{# Generated by copilot #} +{ # Generated by copilot #} --- # This operation creates an account for Charlie with account number 101045. createAccount: { @@ -163,4 +163,4 @@ createAccount: { balance: 15.0, overdraft: 0.0 } ---- \ No newline at end of file +--- diff --git a/account/src/test/resources/account/gpt-gen/_setup.yaml b/account/src/test/resources/account/gpt-gen/_setup.yaml index 80d1564..768142c 100755 --- a/account/src/test/resources/account/gpt-gen/_setup.yaml +++ b/account/src/test/resources/account/gpt-gen/_setup.yaml @@ -1 +1 @@ -# This is GPT-4 generated \ No newline at end of file +# This is GPT-4 generated diff --git a/account/src/test/resources/account/gpt-gen/in.yaml b/account/src/test/resources/account/gpt-gen/in.yaml index 5e67bd1..2bd5644 100755 --- a/account/src/test/resources/account/gpt-gen/in.yaml +++ b/account/src/test/resources/account/gpt-gen/in.yaml @@ -1,4 +1,4 @@ -{# Generated by GPT-4 #} +{ # Generated by GPT-4 #} --- # This operation creates an account for Charlie with account number 101045. # The 'sender' is 'gw1' and the 'target' is 'vault'. diff --git a/account/src/test/resources/account/gpt-jinja/in.yaml b/account/src/test/resources/account/gpt-jinja/in.yaml index b7b1819..805f48c 100644 --- a/account/src/test/resources/account/gpt-jinja/in.yaml +++ b/account/src/test/resources/account/gpt-jinja/in.yaml @@ -1,30 +1,30 @@ -{# Generated by GPT-4 #} +{ # Generated by GPT-4 #} --- # Starting from account number 10, we create 10 new accounts -{% for i in range(10, 20) %} + { % for i in range(10, 20) % } createAccount: { sender: gw1, target: vault, sendingTime: "2023-01-21T10:{{ i }}:00", name: "user{{ i }}", - account: {{ i }}, + account: { { i } }, currency: EUR, balance: 1000 } -{% endfor %} + { % endfor % } ... --- # Starting from account number 10, we initiate 10 transfers to the next account -{% for i in range(10, 20-1) %} {# -1 because the last account won't have a next account to transfer to #} -transfer: { - sender: gw2, - target: vault, - sendingTime: "2023-01-21T11:{{ i }}:00", - from: {{ i }}, - to: {{ i|add(1) }}, - currency: EUR, - amount: 100, - reference: Transfer {{ i }} to {{ i|add(1) }} -} -{% endfor %} -... \ No newline at end of file + { % for i in range(10, 20-1) % } { # -1 because the last account won't have a next account to transfer to #} + transfer: { + sender: gw2, + target: vault, + sendingTime: "2023-01-21T11:{{ i }}:00", + from: { { i } }, + to: { { i|add(1) } }, + currency: EUR, + amount: 100, + reference: Transfer { { i } } to { { i|add(1) } } + } + { % endfor % } + ... diff --git a/account/src/test/resources/account/mixed/_setup.yaml b/account/src/test/resources/account/mixed/_setup.yaml index 05e8b53..cb5b7ef 100644 --- a/account/src/test/resources/account/mixed/_setup.yaml +++ b/account/src/test/resources/account/mixed/_setup.yaml @@ -21,4 +21,4 @@ createAccount: { currency: USD, balance: 1000 } -... \ No newline at end of file +... diff --git a/account/src/test/resources/account/mixed/in.yaml b/account/src/test/resources/account/mixed/in.yaml index cbdd2e7..34ead7b 100644 --- a/account/src/test/resources/account/mixed/in.yaml +++ b/account/src/test/resources/account/mixed/in.yaml @@ -23,4 +23,4 @@ transfer: { amount: 10, reference: Dog food, } -... \ No newline at end of file +... diff --git a/account/src/test/resources/account/simple/_setup.yaml b/account/src/test/resources/account/simple/_setup.yaml index 58b85ba..f5506b6 100644 --- a/account/src/test/resources/account/simple/_setup.yaml +++ b/account/src/test/resources/account/simple/_setup.yaml @@ -1 +1 @@ -# no setup required \ No newline at end of file +# no setup required diff --git a/account/src/test/resources/account/waterfall/_setup.yaml b/account/src/test/resources/account/waterfall/_setup.yaml index cd2c271..f135ce4 100644 --- a/account/src/test/resources/account/waterfall/_setup.yaml +++ b/account/src/test/resources/account/waterfall/_setup.yaml @@ -33,4 +33,4 @@ createAccount: { currency: EUR, balance: 10 } -... \ No newline at end of file +... diff --git a/account/src/test/resources/account/waterfall/in.yaml b/account/src/test/resources/account/waterfall/in.yaml index bf73f37..1ddbdc3 100644 --- a/account/src/test/resources/account/waterfall/in.yaml +++ b/account/src/test/resources/account/waterfall/in.yaml @@ -23,4 +23,4 @@ transfer: { amount: 20, reference: Coffee } -... \ No newline at end of file +... From 7b3670d5f0f88f9dda6b6f65da0c2d87dd6ad3fb Mon Sep 17 00:00:00 2001 From: Peter Lawrey Date: Wed, 1 Jan 2025 08:32:55 +0000 Subject: [PATCH 10/31] First phase updates --- order-processor/aide/README.adoc | 28 + order-processor/aide/aide-glossary.adoc | 53 + order-processor/aide/aide-style-guide.adoc | 98 + order-processor/aide/aide-workflow.adoc | 155 + order-processor/aide/examples.adoc | 316 ++ order-processor/aide/notes-on-using-aide.adoc | 73 + order-processor/context.asciidoc | 3199 +++++++++++++++++ .../src/main/adoc/oms-api-requirements.adoc | 484 +++ .../src/main/adoc/oms-requirements.adoc | 181 + 9 files changed, 4587 insertions(+) create mode 100644 order-processor/aide/README.adoc create mode 100644 order-processor/aide/aide-glossary.adoc create mode 100644 order-processor/aide/aide-style-guide.adoc create mode 100644 order-processor/aide/aide-workflow.adoc create mode 100644 order-processor/aide/examples.adoc create mode 100644 order-processor/aide/notes-on-using-aide.adoc create mode 100644 order-processor/context.asciidoc create mode 100644 order-processor/src/main/adoc/oms-api-requirements.adoc create mode 100644 order-processor/src/main/adoc/oms-requirements.adoc diff --git a/order-processor/aide/README.adoc b/order-processor/aide/README.adoc new file mode 100644 index 0000000..6bb259b --- /dev/null +++ b/order-processor/aide/README.adoc @@ -0,0 +1,28 @@ += Documents Supporting AI +:doctype: index +:author: Peter Lawrey +:lang: en-GB +:toc: + +This folder contains supplemental AsciiDoc files that guide usage, workflows, or style conventions for AI-based projects. + +== Overview + +---- +aide/ +├─ aide-glossary.adoc → Terminology definitions +├─ aide-style-guide.adoc → Project coding style guidelines +├─ aide-workflow.adoc → AI workflow explanation +├─ examples.adoc → Example using AI +└─ notes-on-using-aide.adoc → Notes on using AIDE +---- + +Refer to each `.adoc` for details: + +* link:aide-glossary.adoc[AIDE Glossary]: Definitions of key terms (tokens, AIDE, tokens/line, etc.). +* link:aide-style-guide.adoc[AIDE Style Guide: Conventions for naming, coding, and documentation. +* link:aide-workflow.adoc[AIDE Workflow]: The AIDE approach to iterative, documentation-driven engineering. +* link:aide/notes-on-using-aide.adoc[Notes on Using AIDE]: Notes on previous revisions the AI can draw on for further suggestions. +* link:examples.adoc[Examples using AIDE]: Sample AI queries using AIDE. + +These documents are stored in version control alongside the code, ensuring they evolve together. diff --git a/order-processor/aide/aide-glossary.adoc b/order-processor/aide/aide-glossary.adoc new file mode 100644 index 0000000..3da4097 --- /dev/null +++ b/order-processor/aide/aide-glossary.adoc @@ -0,0 +1,53 @@ +[#aide-glossary] += AIDE Glossary +:doctype: glossary +:author: Peter Lawrey +:lang: en-GB +:toc: + +This glossary is a list of key terms used in the project, along with their definitions. + +[quote, Oxford English Dictionary] +aide (noun) an assistant to an important person, especially a political leader. Pronounced _ayd_. + +== Terms in this Project + +* **AIDE**: Artificial Intelligence Development Environment. +* **AsciiDoc**: A lightweight markup language for authoring technical documentation. +* **Context Mode**: The process of generating a `context.asciidoc` file that captures the entire project’s documentation, requirements, and code. +* **Incremental Mode**: The process of generating an `increment.asciidoc` which captures only the newly or recently updated files since the last full `context.asciidoc` build, as described in the “Next Level AIDE” post. +* **Token**: Underlying words or subword units used by AI. +* **Tokens/Line**: A ratio indicating how many tokens appear per non-blank line of text (calculated as total tokens / total lines). +Used as a quick measure of text “density”. +* **Blank Line**: A line containing only whitespace characters (spaces, tabs, etc.). + +== Different Asciidoctor extensions + +Below is a simple table clarifying when to create a shorter `.ad` summary or use a `.asciidoc` file for larger documents. + +[cols="1,1,4",options="header"] +|=== +| File Type +| Typical Size/Usage +| When to Use + +| `.ad` +| Short Summaries +| Generated or manually curated summaries of large docs. Use these when you have huge files that rarely change. Keeps token counts low for AI-based processes without losing essential context. + +| `.ad` +| Alt Text for Binary Files +| A description of an image or binary file can be stored in a `.ad` file for inclusion in the `context.asciidoc` file. + +| `.adoc.ad` +| Short Summary of an AsciiDoc File +| A short summary of a larger AsciiDoc file that is too large to include in the `context.asciidoc` file. + +| `.adoc` +| Medium +| Full documentation files that change relatively often or are critical references. It is useful when your readers or your AI need complete details. + +| `.asciidoc` +| Large +| A long-form extension for AsciiDoc files for larger documents that the tools will ignore as too large. +|=== \ No newline at end of file diff --git a/order-processor/aide/aide-style-guide.adoc b/order-processor/aide/aide-style-guide.adoc new file mode 100644 index 0000000..98de857 --- /dev/null +++ b/order-processor/aide/aide-style-guide.adoc @@ -0,0 +1,98 @@ +[#aide-style-guide] += AIDE Style Guide +:doctype: style-guide +:author: Peter Lawrey +:lang: en-GB +:toc: + +This guide outlines the core conventions for naming, coding, and documentation. By following these practices, the team maintains clarity, consistency, and a high-quality codebase overall. + +== 1. Language and Wording + +1. Use **British English** spelling (e.g., `colour`, `initialise`) in code comments, documentation, and commit messages. +2. Maintain a **professional, concise** tone; avoid slang or overly informal expressions. +3. Define all project-specific or domain-specific terms in a link:aide-glossary.adoc[AIDE Glossary]. +4. Use straight apostrophes and single and double quotes in code and documentation. +5. Code identifiers may use British or American spellings as needed; consistency within a project is key. +6. When adding suggested improvements to `.adoc` files, use comment syntax like `// consider:` +7. **Document-First Approach**: +* As recommended in the "Next Level AIDE" post, write or update `.adoc` content as early as possible, either in parallel with feature development or before coding (similar to TDD). +* This ensures that the single source of truth is always current and that AI-driven code suggestions align with the actual requirements. + +== 2. Directory and Package Structure + +1. **Directory Names**: all lowercase; use hyphens or underscores if necessary (e.g., `my-project-utils/`). +2. **File Names**: +* Java: match the class name (e.g., `MyService.java`). +* Documentation: use `.adoc` (e.g., `project-overview.adoc`) with lowercase, hyphenated file names. +* Configuration: `.yaml` or `.properties`, describing purpose (e.g., `app-config.yaml`). +3. **Packages**: all lowercase, reflecting the maven `groupId` and `artifactId` and subsystem (e.g., `groupId.artifactId.service`). + +== 3. Java Code Style + +=== 3.1 Formatting +1. **Indentation**: 4 spaces; no tabs. +2. **Line Length**: Keep to 128 characters or fewer if possible. +3. **Braces**: +* Open brace on the same line (e.g., `if (condition) {`). +* Close brace on a new line. +4. **Blank Lines**: Separate method definitions with one blank line; group related lines logically. + +=== 3.2 Naming Conventions +1. **Classes**: PascalCase (e.g., `OrderService`). +2. **Methods and Variables**: camelCase (e.g., `processOrder`, `userName`). +3. **Constants**: ALL_CAPS_WITH_UNDERSCORES (e.g., `MAX_BUFFER_SIZE`). + +=== 3.3 Comments and Documentation +1. Use Javadoc for **public** classes and methods, explaining purpose, parameters, and returns. +2. Only add inline comments (`//`) if the logic isn't apparent. +3. Maintain descriptive commit messages in the present tense (e.g., "Add new caching layer"). + +== 4. Testing + +=== 4.1 Test Naming Conventions +1. Test classes should be named by appending `Test` to the class they test (e.g., `OrderServiceTest` for `OrderService`). +2. Test method names should describe the test scenario and expected behavior, using camelCase (e.g., `shouldReturnOrderDetailsWhenOrderExists`). + +=== 4.2 Directory Structure +1. Mirror the main package structure under `src/test/java/...`. +2. Use a logical grouping of tests, such as: +- Unit tests under `src/test/java` +- Integration tests under `src/integrationTest/java` (if applicable). + +=== 4.3 Test Coverage +1. Ensure at least **80% test coverage** for critical business logic. +2. Focus on covering edge cases, particularly for methods with complex branching. +3. Write integration tests for new features or significant architectural changes. + +=== 4.4 Writing Effective Tests +1. Each test should focus on a single behavior or scenario. +2. Use parameterized tests for scenarios with repetitive logic. +3. Avoid hardcoding data when reusable fixtures or factories can be applied. + +=== 4.5 Mocking and Stubbing +1. Use mocking frameworks (e.g., Mockito) to isolate the unit under test. +2. Stub external dependencies to ensure consistent and repeatable test outcomes. +3. Prefer fakes or in-memory implementations for lightweight data operations (e.g., an in-memory database for integration tests). + +=== 4.6 Test Documentation +1. Include comments to clarify the intent of complex test cases. +2. Document preconditions, inputs, and expected outputs within the test itself or in its name. +3. Ensure test cases reference any associated requirements or bug IDs in the comments. + +== 5. Documentation (AsciiDoc) +1. Start each `.adoc` file with a title line (`= Title`) and optional metadata. +2. Organise content under headings: `==`, `===`, `====`. +3. Use `[source,java]` blocks (or similar) for code highlighting. +4. Keep lines concise, highlighting key points in bullet lists or admonitions (e.g., `NOTE:`). + +== 6. Version Control and Commit Messages +1. Write short, clear commit messages in the **present tense** (e.g., "Fix login bug in UserService"). +2. Reference issue IDs if applicable (e.g., "Fix #42: Add advanced search feature"). + +== 7. Additional Best Practices +1. **AutoCloseable Usage**: Where possible, consider implementing `AutoCloseable` to be used in a try-with-resources block, ensuring deterministic cleanup. +2. **Template Management**: Use templates for AI prompts and text blocks to ensure consistency and reduce duplication. + +== 8. Summary +By following these concise guidelines—especially consistent, clear Java formatting and straightforward documentation practices—teams ensure a streamlined, maintainable codebase. Remember to evolve this guide as the project grows and needs change. diff --git a/order-processor/aide/aide-workflow.adoc b/order-processor/aide/aide-workflow.adoc new file mode 100644 index 0000000..55a593d --- /dev/null +++ b/order-processor/aide/aide-workflow.adoc @@ -0,0 +1,155 @@ +[#aide-workflow] += AIDE Workflow +:doctype: workflow +:author: Peter Lawrey +:lang: en-GB +:toc: + +AIDE combines AI-powered code generation's agility with the reliability of Documentation-Driven Development. +AIDE is an artificial intelligence development environment that relies on AsciiDoc for rich documentation and document context uploads to align your team with AI tooling. + +AIDE focuses on two principles: + +1. Use AI for repetitive code tasks such as keeping adoc, code, and tests in sync. +2. Keep requirements, tests, and code in sync with AsciiDoc. + +== Iterative Workflow + +The development follows a simple loop: + +1. **Document Requirements** – Write or update features in AsciiDoc so everyone understands goals. +These can be derived and updated from code and tests as these change. +This can be as much or as little detail as you have. +You can start by asking the AI to generate a detailed functional requirements document from the code you have or an idea. +2. **Write or Update Tests** – Create tests that reflect documented requirements. +AI can suggest skeletons; you refine domain logic. +3. **Write or Update Code** – Let AI produce a boilerplate or refactor existing code. +You verify its validity. +4. **Continuous Review** – AI flags discrepancies; humans resolve them. +5. **Repeat** – Each new or changed requirement triggers another loop. + +== AI <-> Human Collaboration + +[mermaid] +---- +flowchart LR + A((Write/Update
Requirements)) <--> B((Write/Update
Tests)) + B <--> C((Write/Refactor
Code)) + C <--> A +---- + +- **AI**: Proposes repetitive code, test stubs, or refactors. +- **Human**: Reviews correctness, ensures domain fit, and finalises decisions. +- **Outcome**: Software matches documented requirements and passes tests. + +== Elevating the Workflow with Real-Time Documentation + +Building upon our existing Iterative Workflow, the newest recommendation is to emphasise *real-time updates* to documentation. +Ensure the relevant `.adoc` files are updated as soon as features, requirements, or tests change. +This tight loop informs the AI and creates immediate clarity for all team members. + +=== Benefits + +- **Confidence in Documentation**: As the newest post highlights, accurate docs keep you from "miscommunications" that derail real-world outcomes. +- **Better Onboarding**: An up-to-date AsciiDoc set means new developers grasp the system's design and requirements more quickly. +- **Incremental Changes**: Thanks to the incremental mode, AIDE flags any newly updated files so you can keep the documentation in sync. + +== Benefits of Keeping Requirements, Tests, and Code In Sync + +- **Reduced Drift**: Fewer gaps between documentation and iimplementation +- **Faster Feedback**: AI can quickly generate stubs and highlight inconsistencies. +- **Better Quality**: Frequent checks align the code with tests and docs. +- **Smoother Onboarding**: Up-to-date AsciiDoc clarifies the system for new developers. + +== Best Practices + +1. **Single Source of Truth** +Keep docs (AsciiDoc), tests, and code in version control. + +2. **Frequent Iterations** +Update documentation first, then let AI produce or refine code and tests. + +3. **Small Commits** +Each commit should relate to a single requirement or story, making reviewing changes easier. + +4. **Team Buy-In** +Encourage everyone to review AI outputs. +Use a consistent coding style guided by the style guide. + +== Summarising Large Files + +When a code or test file grows very large, it can complicate AI-based reviews or exceed token limits. +AIDE allows you to generate a condensed `.ad` summary for these files. +This step is handy for rarely updated classes or tests that do not need frequent in-depth reference. + +Here is the recommended process: + +1. **Identify Large Files** +Track files that exceed a certain token threshold (as noted in `notes-on-using-aide.adoc`), or are not actively changing. +2. **Use AI to Summarise** +Run the "Summarise this class as an adoc" command or an equivalent script/tool. +This produces a concise `.ad` file containing essential details. +3. **Commit** +Include a concise commit message like "Summarise large class as `.ad` to reduce token length." + +This approach keeps your AI-based tooling efficient and your repository lean while preserving a high-level understanding of classes that do not require frequent full-detail scrutiny. + +''' + +== Why Integrate Chronicle Products? + +Chronicle products are designed to handle high-performance requirements for latency-critical systems. By integrating them with this project: +- **Throughput**: Achieve message processing speeds exceeding 1M messages/second. +- **Latency**: Sub-microsecond latencies for in-memory operations. +- **Scalability**: Handle billions of records with minimal memory overhead. +- **Fault Tolerance**: Ensure robustness and reliability in distributed systems. + +[mermaid] +---- +flowchart TD + A[Your Application] -->|Queue Events| B[Chronicle Queue] + B -->|Shared State| C[Chronicle Map] + A -->|FIX Messages| D[Chronicle FIX Engine] +---- + +When building applications with AIDE, it's important to consider performance, especially for latency-sensitive or high-throughput systems. Chronicle libraries provide specialised tools to optimise various aspects of your architecture: + +=== Chronicle Queue + +https://github.com/OpenHFT/Chronicle-Queue[Chronicle Queue] is a persisted low-latency messaging framework for high-performance applications. Event `MethodReader` and `MethodWriter` can be used to read and write messages at sub-microsecond latencies in an effective high-level way. + +- **Best for:** High-throughput messaging or event-driven architectures. +- **Use Case:** Logging, replaying, or processing large message volumes. +- **Why Use It:** Microsecond-level latencies, append-only logging, and distributed processing. + +=== Chronicle Map + +https://github.com/OpenHFT/Chronicle-Map[Chronicle Map] is a fast, in-memory, non-blocking key-value store. + +- **Best for:** High-performance, persistent key-value stores. +- **Use Case:** Concurrent access to shared data across threads or processes. +- **Why Use It:** Supports billions of entries, low-latency reads/writes, and thread-safe operations. + +=== Chronicle Services + +https://chronicle.software/services/[Chronicle Services] is a framework for high-performance distributed microservices. + +- **Best for:** Scalable, low-latency microservices. +- **Use Case:** Event-driven architectures with robust inter-service communication. +- **Why Use It:** Simplifies development, supports fault tolerance, and optimises sub-millisecond interactions. + +=== Chronicle FIX Engine + +https://chronicle.software/fix-engine/[Chronicle FIX Engine] is a high-performance FIX protocol engine. + +- **Best for:** Financial systems needing low-latency FIX protocol support. +- **Use Case:** Trading platforms, market data systems, or FIX-based gateways. +- **Why Use It:** Deterministic latencies, FIX message handling, and minimal overhead. + +Choosing the correct Chronicle library ensures your applications meet modern performance demands while maintaining a streamlined AIDE-driven development workflow. + +== Conclusion + +AIDE streamlines development by pairing AI's efficiency with clear human guidance. +Document requirements, run AI-driven code generation, verify via tests, and repeat in small steps. +This keeps your project consistent, accurate, and easy to maintain. diff --git a/order-processor/aide/examples.adoc b/order-processor/aide/examples.adoc new file mode 100644 index 0000000..be8dde1 --- /dev/null +++ b/order-processor/aide/examples.adoc @@ -0,0 +1,316 @@ +[#aide-examples] += AIDE Examples + +== Prompt: Event Driven Example + +---- +Give a high performance sample program for event logging and event processing. +---- + +**Result** + +Here’s an example that utilises Chronicle Queue's **Method Readers** and **Method Writers** for event logging and processing. +This approach leverages Chronicle Queue's built-in serialization and method invocation capabilities, offering cleaner and more efficient code. + +''' +=== Maven Dependency + +Add the following dependency to your `pom.xml`: + +[source,xml] +---- + + + net.openhft + chronicle-queue + 3.27ea0 + + +---- + +=== Interface Definitions + +Define the interfaces for the events to be logged and processed. + +==== EventInterface.java + +[source,java] +---- +public interface EventInterface { + void onEvent(String event); +} +---- + +''' + +=== EventLogger.java + +The `EventLogger` writes events to the queue using a **Method Writer**. + +[source,java] +---- +import net.openhft.chronicle.queue.ChronicleQueue; + +public class EventLogger implements AutoCloseable { + + private final ChronicleQueue queue; + private final EventInterface eventWriter; + + public EventLogger(String path) { + queue = ChronicleQueue.singleBuilder(path).build(); + eventWriter = queue.methodWriter(EventInterface.class); + } + + public void logEvent(String event) { + eventWriter.onEvent(event); + } + + public void close() { + queue.close(); + } + + public static void main(String[] args) { + try (EventLogger logger = new EventLogger("events-queue")) { + logger.logEvent("UserLogin: user123"); + logger.logEvent("FileUpload: fileX.pdf"); + } + } +} +---- + +''' + +=== EventProcessor.java + +The `EventProcessor` reads and processes events using a **Method Reader**. + +[source,java] +---- +import net.openhft.chronicle.queue.ChronicleQueue; +import net.openhft.chronicle.bytes.MethodReader; +public class EventProcessor { + + private final ChronicleQueue queue; + private final MethodReader reader; + + public EventProcessor(String path) { + queue = ChronicleQueue.singleBuilder(path).build(); + reader = queue.createTailer().methodReader((EventInterface) this::processEvent); + } + + private void processEvent(String event) { + System.out.println("Processing event: " + event); + } + + public void startProcessing() { + while (true) { + if (!reader.readOne()) { + try { + Thread.sleep(10); // Prevent busy spinning + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + break; + } + } + } + } + + public static void main(String[] args) { + EventProcessor processor = new EventProcessor("events-queue"); + processor.startProcessing(); + } +} +---- + +''' + +=== Key Changes + +1. **Event Writer**: +- Uses `ChronicleQueue.methodWriter(EventInterface.class)` to generate a proxy implementation of `EventInterface`. +- Simplifies the logging of events by invoking interface methods. + +2. **Event Reader**: +- Uses `ChronicleQueue.methodReader(...)` to process events by calling the specified interface method. + +3. **Event Processing Logic**: +- The `processEvent` method is invoked directly for each event by the `MethodReader`. + +''' + +=== Execution + +1. Run `EventLogger` to log events. +2. Start `EventProcessor` to process the logged events in real-time. + +''' + +=== Benefits of Using Method Readers and Writers + +- **Clean API**: Method calls directly represent events, reducing boilerplate. +- **Type Safety**: Ensures compile-time validation of event definitions. +- **High Performance**: Optimized serialization and deserialization with minimal latency. + +This approach streamlines event-driven systems while maintaining high performance and simplicity. + +== Prompt: Key Value Store Example + +---- +Given a java example of a high performance key value store +---- + +**Result** + +Here’s an example of a high-performance key-value store implemented in Java using **Chronicle Map**, a low-latency, in-memory, persisted key-value store suitable for high-throughput applications. + +''' + +=== Maven Dependency + +Add the following dependency to your `pom.xml`: + +[source,xml] +---- + + + net.openhft + chronicle-map + 3.27ea0 + + +---- + +''' + +=== Implementation + +==== KeyValueStore.java + +This class provides a simple interface to interact with a Chronicle Map. + +[source,java] +---- +package build.chronicle.aide; + +import net.openhft.chronicle.map.ChronicleMap; + +import java.io.File; +import java.io.IOException; + +public class KeyValueStore implements AutoCloseable { + + private final ChronicleMap map; + + public KeyValueStore(String filePath) throws IOException { + // Create or load a persisted Chronicle Map + map = ChronicleMap + .of(String.class, String.class) + .averageKey("exampleKey") + .averageValue("exampleValue") + .entries(1_000_000) // Number of expected entries + .createPersistedTo(new File(filePath)); + } + + public void put(String key, String value) { + map.put(key, value); + } + + public String get(String key) { + return map.get(key); + } + + public void remove(String key) { + map.remove(key); + } + + public void close() { + map.close(); + } + + public static void main(String[] args) throws IOException { + String filePath = "key-value-store.dat"; + + try (KeyValueStore store = new KeyValueStore(filePath)) { + // Insert key-value pairs + store.put("user:1", "Alice"); + store.put("user:2", "Bob"); + + // Retrieve and print values + System.out.println("user:1 -> " + store.get("user:1")); + System.out.println("user:2 -> " + store.get("user:2")); + + // Remove a key + store.remove("user:1"); + System.out.println("user:1 -> " + store.get("user:1")); // Should print null + } + } +} +---- + +''' + +=== Features + +1. **Persistence**: +- The data is stored in a file (`key-value-store.dat`) and survives application restarts. +- Use `.createOrRecoverPersistedTo(filePath)` to create or recover a persisted map. + +2. **Performance**: +- Optimized for low-latency reads and writes. +- Suitable for applications requiring millions of operations per second. + +3. **Scalability**: +- Supports billions of entries with minimal memory overhead. + +4. **Ease of Use**: +- Simple API for `put`, `get`, and `remove` operations. + +''' + +=== How It Works + +1. **Initialization**: +- The Chronicle Map is configured with average key/value sizes and the expected number of entries. + +2. **Operations**: +- Keys and values are stored as `String`. +This can be replaced with any serializable class for more complex use cases. + +3. **Persistence**: +- The map persists to a file, enabling recovery after application restarts. + +4. **Cleanup**: +- The `close` method ensures all resources are released and the map is safely persisted. + +''' + +=== Example Output + +[source,text] +---- +user:1 -> Alice +user:2 -> Bob +user:1 -> null +---- + +''' + +=== Advanced Usage + +1. **Custom Key/Value Types**: +Replace `String.class` with custom types for both keys and values. + +[source,java] +---- +ChronicleMap.of(MyKey.class, MyValue.class) + .averageKey(new MyKey()) + .averageValue(new MyValue()) + .entries(1_000_000) + .createOrRecoverPersistedTo("custom-map.dat"); +---- + +2. **Concurrency**: +- Chronicle Map supports concurrent access across threads and processes. +- It’s thread-safe without additional locking mechanisms. + +This example demonstrates a robust and efficient key-value store suitable for high-performance applications like caching, distributed systems, or analytics. diff --git a/order-processor/aide/notes-on-using-aide.adoc b/order-processor/aide/notes-on-using-aide.adoc new file mode 100644 index 0000000..b6fe6e0 --- /dev/null +++ b/order-processor/aide/notes-on-using-aide.adoc @@ -0,0 +1,73 @@ +[#notes-on-using-aide] += Notes on using AIDE +:doctype: revision-notes +:author: Peter Lawrey +:lang: en-GB +:toc: + + +Keeping notes on how the project is progressing and what changes you have made is helpful. This can help you understand the project better and make the necessary changes at the end to complete it. + +NOTE: This document can be a template for recording notes on your own project and for helping the AI suggest improvements, as it is also a source of information between phases. + +== First Phase +Create a skeleton for the project and see what the AI can generate for you. These are more a source of inspiration than a source of truth. + +NOTE: Using `o1` is usally better than `4o` as it is more likely to be understood by the AI and produce a better result. `4o` works better for converting images, or web pages to asciidoc. + +- `Generate a functional requirements asciidoc of OMSImpl detailed enough to recreate this implementation including a prompt to execute it.` (Paste the `context.asciidoc`) +- `suggest improvements to this requirements document` + +.Request to clarify assumptions to the requirements document. +---- +Add to the requirements document to note that this component is intended to be in an event driven framework. Each input event comes from a Chronicle Queue, and each output is written to a Chronicle Queue, providing traceability. Validation is performed by the framework calling validate() on the input DTO before calling the matching method, and validate() called on the output DTO before writing to the queue. +Also note; +- On receiving a NewOrderSingle, the system must create an ExecutionReport. If the request is invalid, the system must create and send an OrderCancelReject. +- When an exception is thrown, the framework captures the exception and calls jvmError. +- for a simple example such as this, the component should be able to consistently handle 100k msg/s with low jitter on the 99% latency or better. +---- + +The improved generated requirements document was written to `src/main/adoc/oms-requirements.adoc` + +- `generate a functional requirements document for the packages lost.town.oms.api and lost.town.oms.dto as an asciidoc with enough detail to recreate them` +- `suggest improvements to these requirements` + +.Request to clarify details +---- +add to the api requirements document that the frame work is responsible for calling validate() before passing the event to the component, and for an output DTO before an output is written. +There is an assumption of single threadedness +The YAML tests describe sample usage. see BDD tests, Given `_setup.yaml` events, When `in.yaml` events are tested and Then we expect `out.yaml` events +The `jvmError` is called for every Exception thrown. +This demo relies on Chronicle Wire however a production example would require Chronicle Services to support failover to a replicated system and restartability. +More YAML based tests can be added by including a new directory in the list of paths in the OMSImplTest class. +---- + +- `make the requirements document detailed enough to recreate the API and dto packages` + +Output written as `src/main/adoc/oms-api-requirements.adoc` + +== Second Phase +Generate requirements docs for each events and the API + +- Update the requirements from the code and tests +- Update the tests from the requirements and code +- Update the code from the requirements and tests + +== Third Phase +Review the project + +- Look at more specific improvements +- Make selective changes + +== Fourth Phase +Restructure the project to make it easier to work with + +- Refactor the requirements, code, and/or tests to make them easier for the AI to work with and understand. + +== Fifth Phase +Finalize the project + +- Review the changes made in the previous phases to ensure they are consistent and correct. +- Make any necessary corrections or adjustments once you feel you aren't advancing. + +NOTE: You must understand the project to make the necessary changes at the end to make it complete. If you don't understand something, ask for clarification or skip it and possibly return to it later. diff --git a/order-processor/context.asciidoc b/order-processor/context.asciidoc new file mode 100644 index 0000000..aedf202 --- /dev/null +++ b/order-processor/context.asciidoc @@ -0,0 +1,3199 @@ += Directory Content + +== File: pom.xml +.... + + + 4.0.0 + + + run.chronicle + chronicle-queue-demo + 1.0-SNAPSHOT + ../pom.xml + + + order-processor + OpenHFT/${project.parent.artifactId}/${project.artifactId} + + + + net.openhft + jlbh + + + + + + + org.codehaus.mojo + exec-maven-plugin + + + bench + + exec + + + ${java.home}/bin/java + ${jvm.requiredArgs} -classpath %classpath town.lost.oms.OMSBenchmarkMain + + + + adder + + exec + + + ${java.home}/bin/java + ${jvm.requiredArgs} -classpath %classpath town.lost.oms.OrderAdderMain + + + + viewer + + exec + + + ${java.home}/bin/java + ${jvm.requiredArgs} -classpath %classpath town.lost.oms.OrderViewerMain + + + + dump + + exec + + + ${java.home}/bin/java + ${jvm.requiredArgs} -classpath %classpath net.openhft.chronicle.queue.DumpQueueMain in + + + + + + + +.... + +Lines 70, Blanks 4, Tokens 645 + +== File: README.adoc +.... += Order Processor Demo +Peter Lawrey + +The Order Processor Demo showcases the implementation of services using messages based on the Financial Information Exchange (FIX) protocol. This project simulates an order processing system, including features such as creating new orders, cancelling specific orders, and cancelling all orders. The project is designed to mimic real-world financial trading systems, demonstrating how such a system can be built using Java. + +== Table of Contents + +* <> +* <> +* <> +* <> +* <> +* <> + +== Getting Started + +To get started with the Order Processor Demo, you need to clone the repository to your local machine. Once you have cloned the repository, you can open it in your IDE of choice (we recommend IntelliJ CE for this project). + +== Prerequisites + +Before you begin, ensure you have the following installed: + +- **Java Development Kit (JDK)**: Java 8 Update 202+, Java 11, Java 17, or Java 21 +- **Apache Maven**: Version 3.6.5 or higher +- **Integrated Development Environment (IDE)**: IntelliJ IDEA Community Edition (recommended) or any other IDE +- **Internet Access**: Required for Maven to download necessary dependencies + +== Installation + +Here are the steps to clone the repository and open it in IntelliJ: + +1. Clone the repository with `git clone ` +2. Open IntelliJ IDE and select 'Open' +3. Navigate to the cloned repository and click 'Open' +4. Wait for the IDE to import the project and download the necessary dependencies + +== Running the Project + +[source,mermaid] +.... +sequenceDiagram +autonumber +Client->>+OMS: NewOrderSingle +Note right of Client: via Chronicle Queue, persisted +Note right of OMS: processes event +OMS->>-Client: ExecutionReport +Note right of Client: via Chronicle Queue, persisted +.... + +To run the project, you need to compile the project and run the main classes for different features: + +- `OrderAdderMain`: This class simulates the creation of new orders. +- `OrderViewerMain`: This class simulates the viewing of created orders. +- `OrderBenchmarkMain`: This class benchmarks round trip time of writing a `NewOrderSingle` a queue, have the `OMSImpl` produce an `ExecutionReport` and read that report. +- `OMSImpl`: This class handles the processing of the orders, including cancelling orders. + +You can run these classes directly from your IDE, or you can use `mvn compile exec:exec@id` on the command line to run the compiled classes. + +== Contributing + +We appreciate any contributions to the Order Processor Demo. If you find any bugs or issues, please open an issue in the repository. If you want to contribute to the code, please fork the repository, make your changes, and open a pull request. + +== Acknowledgments + +- Inspired by real-world financial trading systems. +- Uses the FIX protocol for message exchange. +.... + +Lines 46, Blanks 20, Tokens 663 + +== File: src\main\java\town\lost\oms\api\ErrorListener.java +.... +package town.lost.oms.api; + +/** + * This interface provides a method to handle JVM errors. + * Implementations of this interface will define how these errors are handled. + */ +public interface ErrorListener { + + /** + * This method handles JVM errors. + * It takes a String message which provides details about the error. + * + * @param msg a String providing details about the JVM error. + */ + void jvmError(String msg); +} +.... + +Lines 14, Blanks 2, Tokens 93 + +== File: src\main\java\town\lost\oms\api\OMSIn.java +.... +/* + */ + +package town.lost.oms.api; + +import net.openhft.chronicle.bytes.MethodId; +import town.lost.oms.dto.CancelAll; +import town.lost.oms.dto.CancelOrderRequest; +import town.lost.oms.dto.NewOrderSingle; + +/** + * The {@code OMSIn} interface defines the operations that can be performed by an Order Management System (OMS). + *

+ * It includes methods to handle new single orders, cancel order requests, and cancel all orders. + * Each method receives an instance of a data transfer object that represents the details of the operation. + * + * @see NewOrderSingle + * @see CancelOrderRequest + * @see CancelAll + */ +public interface OMSIn { + + /** + * Handles a new single order submission. + * + * @param nos The {@link NewOrderSingle} object representing the details of the new order. + */ + @MethodId(1) + void newOrderSingle(NewOrderSingle nos); + + /** + * Handles a cancel order request. + * + * @param cor The {@link CancelOrderRequest} object representing the details of the cancel order request. + */ + @MethodId(2) + void cancelOrderRequest(CancelOrderRequest cor); + + /** + * Handles a cancel all orders request. + * + * @param cancelAll The {@link CancelAll} object representing the details of the cancel all orders request. + */ + void cancelAll(CancelAll cancelAll); +} +.... + +Lines 39, Blanks 6, Tokens 309 + +== File: src\main\java\town\lost\oms\api\OMSOut.java +.... +/* + */ + +package town.lost.oms.api; + +import net.openhft.chronicle.bytes.MethodId; +import town.lost.oms.dto.ExecutionReport; +import town.lost.oms.dto.OrderCancelReject; + +/** + * The {@code OMSOut} interface defines the output operations that an Order Management System (OMS) can perform. + *

+ * It includes methods to handle execution reports and order cancel rejections. + *

It extends the {@link ErrorListener} interface to handle any JVM errors that may occur during processing. + * Each method receives an instance of a data transfer object that represents the details of the operation. + * + * @see ExecutionReport + * @see OrderCancelReject + * @see ErrorListener + */ +public interface OMSOut extends ErrorListener{ + + /** + * Handles an execution report. + * + * @param er The {@link ExecutionReport} object representing the details of the execution report. + */ + @MethodId(11) + void executionReport(ExecutionReport er); + + /** + * Handles an order cancel reject. + * + * @param ocr The {@link OrderCancelReject} object representing the details of the order cancel reject. + */ + @MethodId(12) + void orderCancelReject(OrderCancelReject ocr); +} +.... + +Lines 33, Blanks 5, Tokens 267 + +== File: src\main\java\town\lost\oms\api\package-info.java +.... +/** + * Provides the API interfaces for interactions with the Order Management System (OMS) in the 'town.lost.oms' project. + * + *

This package includes the following interfaces: + * + *

    + *
  • {@link town.lost.oms.api.OMSIn}, which defines the methods for inbound operations that an OMS can perform, such as handling new single orders, cancel order requests, and cancel all orders.
  • + *
  • {@link town.lost.oms.api.OMSOut}, which defines the methods for outbound operations from the OMS, including handling execution reports and order cancel rejections.
  • + *
+ * + *

Each interface includes methods that correspond to specific actions within the OMS, facilitating communication between clients and the OMS. + * + *

For more details, refer to the documentation for each individual interface. + */ +package town.lost.oms.api; + +.... + +Lines 15, Blanks 1, Tokens 187 + +== File: src\main\java\town\lost\oms\dto\AbstractEvent.java +.... +/* + */ + +package town.lost.oms.dto; + +import net.openhft.chronicle.bytes.BytesIn; +import net.openhft.chronicle.bytes.BytesOut; +import net.openhft.chronicle.core.Jvm; +import net.openhft.chronicle.core.io.InvalidMarshallableException; +import net.openhft.chronicle.core.io.Validatable; +import net.openhft.chronicle.wire.*; +import net.openhft.chronicle.wire.converter.NanoTime; +import net.openhft.chronicle.wire.converter.ShortText; + +/** + * The {@code AbstractEvent} class represents a generic event in the system. + * + *

This class provides common fields such as sender, target, and sendingTime which can be extended + * by more specific events. Fields sender and target are encoded as {@link ShortText} for efficient storage and transmission. + * + *

The class is generic, with the type parameter being a subclass of {@code AbstractEvent}. This allows + * methods in this class to return an instance of the subclass, enabling method chaining in the subclass. + * + *

The encoding of the marshalled bytes can be controlled via system properties "bytesInBinary" and "pregeneratedMarshallable". + */ +public class AbstractEvent> extends SelfDescribingMarshallable implements Validatable { + // Used to control the benchmark + public static final boolean BYTES_IN_BINARY = Jvm.getBoolean("bytesInBinary", true); + + // Used to control the benchmark + public static final boolean PREGENERATED_MARSHALLABLE = Jvm.getBoolean("pregeneratedMarshallable", true); + private static final int MARSHALLABLE_VERSION = 1; + + // The sender of the event + @ShortText + private long sender; + + // The target of the event + @ShortText + private long target; + + // The sending time of the event, in nanoseconds. + @NanoTime + private long sendingTime; + + @Override + public boolean usesSelfDescribingMessage() { + return !BYTES_IN_BINARY; + } + + /** + * Get the sender of the event. + * + * @return The sender's value as a long. + */ + public long sender() { + return sender; + } + + /** + * Set the sender of the event. + * + * @param sender The sender's value to set, as a long. + * @return This AbstractEvent instance, to facilitate method chaining. + */ + @SuppressWarnings("unchecked") + public E sender(long sender) { + this.sender = sender; + return (E) this; + } + + /** + * Get the target of the event. + * + * @return The target's value as a long. + */ + public long target() { + return target; + } + + /** + * Set the target of the event. + * + * @param target The target's value to set, as a long. + * @return This AbstractEvent instance, to facilitate method chaining. + */ + @SuppressWarnings("unchecked") + public E target(long target) { + this.target = target; + return (E) this; + } + + /** + * Get the sending time of the event. + * + * @return The sending time as a long. + */ + public long sendingTime() { + return sendingTime; + } + + /** + * Set the sending time of the event. + * + * @param sendingTime The sending time to set, as a long. + * @return This AbstractEvent instance, to facilitate method chaining. + */ + @SuppressWarnings("unchecked") + public E sendingTime(long sendingTime) { + this.sendingTime = sendingTime; + return (E) this; + } + + @Override + public void writeMarshallable(BytesOut out) { + if (PREGENERATED_MARSHALLABLE) { + out.writeStopBit(MARSHALLABLE_VERSION); + out.writeLong(sender); + out.writeLong(target); + out.writeLong(sendingTime); + } else { + super.writeMarshallable(out); + } + } + + @Override + public void readMarshallable(BytesIn in) { + if (PREGENERATED_MARSHALLABLE) { + int version = (int) in.readStopBit(); + if (version == MARSHALLABLE_VERSION) { + sender = in.readLong(); + target = in.readLong(); + sendingTime = in.readLong(); + } else { + throw new IllegalStateException("Unknown version " + version); + } + } else { + super.readMarshallable(in); + } + } + + @Override + public void writeMarshallable(WireOut out) { + if (PREGENERATED_MARSHALLABLE) { + out.write("sender").writeLong(ShortTextLongConverter.INSTANCE, sender); + out.write("target").writeLong(ShortTextLongConverter.INSTANCE, target); + out.write("sendingTime").writeLong(MicroTimestampLongConverter.INSTANCE, sendingTime); + } else { + super.writeMarshallable(out); + } + } + + @Override + public void readMarshallable(WireIn in) { + if (PREGENERATED_MARSHALLABLE) { + sender = in.read("sender").readLong(ShortTextLongConverter.INSTANCE); + target = in.read("target").readLong(ShortTextLongConverter.INSTANCE); + sendingTime = in.read("sendingTime").readLong(MicroTimestampLongConverter.INSTANCE); + } else { + super.readMarshallable(in); + } + } + + @Override + public void validate() throws InvalidMarshallableException { + if (sender == 0) + throw new InvalidMarshallableException("sender is required"); + if (target == 0) + throw new InvalidMarshallableException("target is required"); + if (sendingTime == 0) + throw new InvalidMarshallableException("sendingTime is required"); + } +} +.... + +Lines 154, Blanks 19, Tokens 1262 + +== File: src\main\java\town\lost\oms\dto\CancelAll.java +.... +/* + */ + +package town.lost.oms.dto; + +import net.openhft.chronicle.bytes.BytesIn; +import net.openhft.chronicle.bytes.BytesOut; +import net.openhft.chronicle.core.io.InvalidMarshallableException; +import net.openhft.chronicle.wire.ShortTextLongConverter; +import net.openhft.chronicle.wire.WireIn; +import net.openhft.chronicle.wire.WireOut; +import net.openhft.chronicle.wire.converter.ShortText; + +/** + * The {@code CancelAll} class represents an event that is used to cancel all active orders for a particular symbol. + * + *

This class extends the {@link AbstractEvent} class with the type parameter {@link CancelAll}, indicating that + * it is processed as a {@code CancelAll} event. + * + *

This class is typically used like this: + * + *

+ * public void cancelAll(CancelAll cancelAll) {
+ *     // Iterate over active orders and cancel those matching the symbol
+ *     for (Order order : activeOrders) {
+ *         if (order.symbol() == cancelAll.symbol()) {
+ *             CancelOrderRequest request = new CancelOrderRequest()
+ *                 .sender(cancelAll.target())
+ *                 .target(cancelAll.sender())
+ *                 .symbol(order.symbol())
+ *                 .clOrdID(order.clOrdID())
+ *                 .sendingTime(cancelAll.sendingTime())
+ *                 .origClOrdID(order.origClOrdID())
+ *                 .side(order.side());
+ *             out.cancelOrderRequest(request);
+ *         }
+ *     }
+ * }
+ * }
+ * + *

Note that the {@code symbol} field is encoded using {@link ShortTextLongConverter} to minimize storage and transmission size. + */ +public class CancelAll extends AbstractEvent { + private static final int MASHALLABLE_VERSION = 1; + // Symbol for which all orders are to be canceled. + @ShortText + private long symbol; + + // Client order ID. + private String clOrdID = ""; + + /** + * Get the symbol for which all orders are to be canceled. + * + * @return The symbol value as a long. + */ + public long symbol() { + return symbol; + } + + /** + * Set the symbol for which all orders are to be canceled. + * + * @param symbol The symbol value to set, as a long. + * @return This CancelAll instance, to facilitate method chaining. + */ + public CancelAll symbol(long symbol) { + this.symbol = symbol; + return this; + } + + /** + * Retrieves the client order ID. + * + * @return The client order ID as a {@code String}. + */ + public String clOrdID() { + return clOrdID; + } + + /** + * Sets the client order ID and returns the {@code CancelAll} object. This method allows for method chaining. + * + * @param clOrdID The client order ID. + * @return The {@code CancelAll} object with the client order ID set. + */ + public CancelAll clOrdID(String clOrdID) { + this.clOrdID = clOrdID; + return this; + } + + @Override + public void writeMarshallable(WireOut out) { + super.writeMarshallable(out); + if (PREGENERATED_MARSHALLABLE) { + out.write("symbol").writeLong(ShortTextLongConverter.INSTANCE, symbol); + out.write("clOrdID").object(String.class, clOrdID); + } + } + + @Override + public void readMarshallable(WireIn in) { + super.readMarshallable(in); + if (PREGENERATED_MARSHALLABLE) { + symbol = in.read("symbol").readLong(ShortTextLongConverter.INSTANCE); + clOrdID = in.read("clOrdID").object(clOrdID, String.class); + } + } + + @Override + public void writeMarshallable(BytesOut out) { + super.writeMarshallable(out); + if (PREGENERATED_MARSHALLABLE) { + out.writeStopBit(MASHALLABLE_VERSION); + out.writeLong(symbol); + out.writeObject(String.class, clOrdID); + } + } + + @Override + public void readMarshallable(BytesIn in) { + super.readMarshallable(in); + if (PREGENERATED_MARSHALLABLE) { + int version = (int) in.readStopBit(); + if (version == MASHALLABLE_VERSION) { + symbol = in.readLong(); + clOrdID = in.readObject(String.class); + } else { + throw new IllegalStateException("Unknown version " + version); + } + } + } + + /** + * Validates the fields of this {@code CancelAll} event. + * + * @throws InvalidMarshallableException if any required field is missing or invalid. + */ + @Override + public void validate() throws InvalidMarshallableException { + super.validate(); + if (symbol == 0) { + throw new InvalidMarshallableException("symbol is required"); + } + if (clOrdID == null || clOrdID.isEmpty()) { + throw new InvalidMarshallableException("clOrdID is required"); + } + } +} +.... + +Lines 136, Blanks 13, Tokens 1121 + +== File: src\main\java\town\lost\oms\dto\CancelOrderRequest.java +.... +/* + */ + +package town.lost.oms.dto; + +import net.openhft.chronicle.bytes.BytesIn; +import net.openhft.chronicle.bytes.BytesOut; +import net.openhft.chronicle.core.io.InvalidMarshallableException; +import net.openhft.chronicle.wire.ShortTextLongConverter; +import net.openhft.chronicle.wire.WireIn; +import net.openhft.chronicle.wire.WireOut; +import net.openhft.chronicle.wire.converter.ShortText; + +/** + * The {@code CancelOrderRequest} class represents a request to cancel an order in a trading system. + * + *

This class extends the {@link AbstractEvent} class, with the type parameter being {@link CancelOrderRequest}. + * This indicates that the event will be processed into a {@link CancelOrderRequest} that represents a request to cancel an order. + * + *

Each {@code CancelOrderRequest} contains the symbol of the financial instrument for the order that is being requested to be cancelled and the client order ID. + * + *

    + *
  • symbol: The symbol of the financial instrument for the order.
  • + *
  • account: The account associated with the order.
  • + *
  • clOrdID: The client order ID, a unique identifier for the order.
  • + *
  • origClOrdID: The original client order ID of the order to be canceled.
  • + *
  • side: The side of the order (e.g., buy or sell).
  • + *
+ * + *

The {@code symbol} and {@code account} fields are encoded using {@link ShortTextLongConverter} + * to save space, while the client order IDs are strings for identification purposes. + */ +public class CancelOrderRequest extends AbstractEvent { + private static final int MASHALLABLE_VERSION = 1; + // Symbol of the financial instrument for the order. + @ShortText + private long symbol; + @ShortText + private long account; + + // Client order ID. + private String clOrdID = ""; + + // Original client order ID. + private String origClOrdID = ""; + + // Side of the order (e.g., buy or sell). + private Side side; + + + /** + * Retrieves the symbol of the financial instrument for the order. + * + * @return The symbol as a {@code long}. + */ + public long symbol() { + return symbol; + } + + /** + * Sets the symbol of the financial instrument for the order and returns the {@code CancelOrderRequest} object. This method allows for method chaining. + * + * @param symbol The symbol of the financial instrument for the order. + * @return The {@code CancelOrderRequest} object with the symbol set. + */ + public CancelOrderRequest symbol(long symbol) { + this.symbol = symbol; + return this; + } + + + /** + * Retrieves the account associated with the order. + * + * @return The account as a {@code long}. + */ + public long account() { + return account; + } + + /** + * Sets the account associated with the order. + * + * @param account The account to set. + * @return This {@code CancelOrderRequest} instance for method chaining. + */ + public CancelOrderRequest account(long account) { + this.account = account; + return this; + } + + /** + * Retrieves the client order ID. + * + * @return The client order ID as a {@code String}. + */ + public String clOrdID() { + return clOrdID; + } + + /** + * Sets the client order ID and returns the {@code CancelOrderRequest} object. This method allows for method chaining. + * + * @param clOrdID The client order ID. + * @return The {@code CancelOrderRequest} object with the client order ID set. + */ + public CancelOrderRequest clOrdID(String clOrdID) { + this.clOrdID = clOrdID; + return this; + } + + /** + * Retrieves the original client order ID. + * + * @return The original client order ID as a {@code String}. + */ + public String origClOrdID() { + return origClOrdID; + } + + /** + * Sets the original client order ID. + * + * @param origClOrdID The original client order ID to set. + * @return This {@code CancelOrderRequest} instance for method chaining. + */ + public CancelOrderRequest origClOrdID(String origClOrdID) { + this.origClOrdID = origClOrdID; + return this; + } + + /** + * Retrieves the side of the order. + * + * @return The side of the order as a {@link Side} enum. + */ + public Side side() { + return side; + } + + /** + * Sets the side of the order. + * + * @param side The side to set (e.g., buy or sell). + * @return This {@code CancelOrderRequest} instance for method chaining. + */ + public CancelOrderRequest side(Side side) { + this.side = side; + return this; + } + + @Override + public void writeMarshallable(WireOut out) { + super.writeMarshallable(out); + if (PREGENERATED_MARSHALLABLE) { + out.write("symbol").writeLong(ShortTextLongConverter.INSTANCE, symbol); + out.write("account").writeLong(ShortTextLongConverter.INSTANCE, account); + out.write("clOrdID").object(String.class, clOrdID); + out.write("origClOrdID").object(String.class, origClOrdID); + out.write("side").object(Side.class, side); + } + } + + @Override + public void readMarshallable(WireIn in) { + super.readMarshallable(in); + if (PREGENERATED_MARSHALLABLE) { + symbol = in.read("symbol").readLong(ShortTextLongConverter.INSTANCE); + account = in.read("account").readLong(ShortTextLongConverter.INSTANCE); + clOrdID = in.read("clOrdID").object(clOrdID, String.class); + origClOrdID = in.read("origClOrdID").object(origClOrdID, String.class); + side = in.read("side").object(Side.class); + } + } + + @Override + public void writeMarshallable(BytesOut out) { + super.writeMarshallable(out); + if (PREGENERATED_MARSHALLABLE) { + out.writeStopBit(MASHALLABLE_VERSION); + out.writeLong(symbol); + out.writeLong(account); + out.writeObject(String.class, clOrdID); + out.writeObject(String.class, origClOrdID); + out.writeObject(Side.class, side); + } + } + + @SuppressWarnings("unchecked") + @Override + public void readMarshallable(BytesIn in) { + super.readMarshallable(in); + if (PREGENERATED_MARSHALLABLE) { + int version = (int) in.readStopBit(); + if (version == MASHALLABLE_VERSION) { + symbol = in.readLong(); + account = in.readLong(); + clOrdID = in.readObject(String.class); + origClOrdID = in.readObject(String.class); + side = in.readObject(Side.class); + } else { + throw new IllegalStateException("Unknown version " + version); + } + } + } + + /** + * Validates the fields of this {@code CancelOrderRequest}. + * + * @throws InvalidMarshallableException if any required field is missing or invalid. + */ + @Override + public void validate() throws InvalidMarshallableException { + if (symbol == 0) { + throw new InvalidMarshallableException("symbol is required"); + } + if (account == 0) { + throw new InvalidMarshallableException("account is required"); + } + if (clOrdID == null || clOrdID.isEmpty()) { + throw new InvalidMarshallableException("clOrdID is required"); + } + if (origClOrdID == null || origClOrdID.isEmpty()) { + throw new InvalidMarshallableException("origClOrdID is required"); + } + if (side == null) { + throw new InvalidMarshallableException("side is required"); + } + } +} +.... + +Lines 207, Blanks 23, Tokens 1812 + +== File: src\main\java\town\lost\oms\dto\Ccy.java +.... +package town.lost.oms.dto; + +/** + * The {@code Ccy} enum represents international currency codes as per the ISO 4217 standard. + * + *

Each constant corresponds to a specific currency, identified by its three-letter code. + * Additional information such as the currency name, country, and symbol can be accessed through + * the provided methods. + * + *

Example usage: + *

{@code
+ * Ccy currency = Ccy.USD;
+ * System.out.println("Currency Code: " + currency.getCode());
+ * System.out.println("Currency Name: " + currency.getCurrencyName());
+ * System.out.println("Country: " + currency.getCountry());
+ * System.out.println("Symbol: " + currency.getSymbol());
+ * }
+ * + *

This would output: + *

+ * Currency Code: USD
+ * Currency Name: United States Dollar
+ * Country: United States
+ * Symbol: $
+ * 
+ */ +public enum Ccy { + /** + * United States Dollar. + */ + USD("United States Dollar", "United States", "$"), + /** + * Euro. + */ + EUR("Euro", "Eurozone", "€"), + /** + * British Pound Sterling. + */ + GBP("Pound Sterling", "United Kingdom", "£"), + /** + * Japanese Yen. + */ + JPY("Japanese Yen", "Japan", "¥"), + /** + * Swiss Franc. + */ + CHF("Swiss Franc", "Switzerland", "CHF"), + /** + * Canadian Dollar. + */ + CAD("Canadian Dollar", "Canada", "C$"), + /** + * Australian Dollar. + */ + AUD("Australian Dollar", "Australia", "A$"), + /** + * New Zealand Dollar. + */ + NZD("New Zealand Dollar", "New Zealand", "NZ$"), + /** + * Chinese Yuan Renminbi. + */ + CNY("Chinese Yuan", "China", "¥"), + /** + * Swedish Krona. + */ + SEK("Swedish Krona", "Sweden", "kr"), + /** + * Norwegian Krone. + */ + NOK("Norwegian Krone", "Norway", "kr"), + /** + * Russian Ruble. + */ + RUB("Russian Ruble", "Russia", "₽"), + /** + * South African Rand. + */ + ZAR("South African Rand", "South Africa", "R"), + /** + * Singapore Dollar. + */ + SGD("Singapore Dollar", "Singapore", "S$"), + /** + * Hong Kong Dollar. + */ + HKD("Hong Kong Dollar", "Hong Kong", "HK$"); + + // Fields + private final String currencyName; + private final String country; + private final String symbol; + + /** + * Constructs a {@code Ccy} enum constant with the specified currency name, country, and symbol. + * + * @param currencyName the full name of the currency + * @param country the country or region where the currency is used + * @param symbol the symbol representing the currency + */ + Ccy(String currencyName, String country, String symbol) { + this.currencyName = currencyName; + this.country = country; + this.symbol = symbol; + } + + /** + * Gets the full name of the currency. + * + * @return the currency name + */ + public String getCurrencyName() { + return currencyName; + } + + /** + * Gets the country or region where the currency is used. + * + * @return the country or region + */ + public String getCountry() { + return country; + } + + /** + * Gets the symbol representing the currency. + * + * @return the currency symbol + */ + public String getSymbol() { + return symbol; + } + + /** + * Gets the three-letter currency code as per the ISO 4217 standard. + * + * @return the currency code + */ + public String getCode() { + return this.name(); + } +} + +.... + +Lines 135, Blanks 8, Tokens 853 + +== File: src\main\java\town\lost\oms\dto\ExecutionReport.java +.... +/* + */ + +package town.lost.oms.dto; + +import net.openhft.chronicle.bytes.BytesIn; +import net.openhft.chronicle.bytes.BytesOut; +import net.openhft.chronicle.core.io.InvalidMarshallableException; +import net.openhft.chronicle.wire.ShortTextLongConverter; +import net.openhft.chronicle.wire.NanoTimestampLongConverter; +import net.openhft.chronicle.wire.WireIn; +import net.openhft.chronicle.wire.WireOut; +import net.openhft.chronicle.wire.converter.NanoTime; +import net.openhft.chronicle.wire.converter.ShortText; + +import static town.lost.oms.dto.ValidateUtil.*; + +/** + * The {@code ExecutionReport} class represents the execution report of an order in a trading system. + * + *

This class extends the {@link AbstractEvent} class, with the type parameter being {@link ExecutionReport}. + * This indicates that the event will be processed into an {@link ExecutionReport} that represents the execution report of an order. + * + *

Each {@code ExecutionReport} contains various pieces of information about the order execution, including: + * + *

    + *
  • symbol: The identifier of the financial instrument.
  • + *
  • transactTime: The transaction time in nanoseconds.
  • + *
  • orderQty: The quantity of the order.
  • + *
  • price: The price of the order.
  • + *
  • orderID: The unique identifier of the order.
  • + *
  • lastPx: The last traded price of the order.
  • + *
  • leavesQty: The remaining quantity of the order.
  • + *
  • cumQty: The accumulated quantity of the order.
  • + *
  • avgPx: The average price of the order.
  • + *
  • side: The side of the order (buy or sell).
  • + *
  • ordType: The type of the order (e.g., market or limit).
  • + *
  • clOrdID: The client order ID.
  • + *
  • text: An optional text message about the order execution.
  • + *
+ * + *

The {@code symbol} field is encoded using {@link ShortTextLongConverter}, and {@code transactTime} and {@code orderID} + * use {@link NanoTimestampLongConverter} to save space. + */ +public class ExecutionReport extends AbstractEvent { + private static final int MASHALLABLE_VERSION = 1; + // Symbol of the financial instrument. + @ShortText + private long symbol; + + // Transaction time in nanoseconds. + @NanoTime + private long transactTime; + + // Quantity of the order. + private double orderQty; + + // Price of the order. + private double price; + + // Order ID in nanoseconds. + @NanoTime + private long orderID; + + // Last traded price of the order. + private double lastPx; + + // Remaining quantity of the order. + private double leavesQty; + + // Accumulated quantity of the order. + private double cumQty; + + // Average price of the order. + private double avgPx; + + // Side of the order (buy or sell). + private Side side; + + // Type of the order (market or limit). + private OrderType ordType; + + // Client order ID. + private String clOrdID = ""; + + // Optional text message about the order execution. + private String text = null; + + /** + * Retrieves the client order ID. + * + * @return the client order ID as a {@code String} + */ + public String clOrdID() { + return clOrdID; + } + + /** + * Sets the client order ID. + * + * @param clOrdID the client order ID to set + * @return this {@code ExecutionReport} instance for method chaining + */ + public ExecutionReport clOrdID(String clOrdID) { + this.clOrdID = clOrdID; + return this; + } + + /** + * Retrieves the symbol of the financial instrument. + * + * @return the symbol as a {@code long} + */ + public long symbol() { + return symbol; + } + + /** + * Sets the symbol of the financial instrument. + * + * @param symbol the symbol to set + * @return this {@code ExecutionReport} instance for method chaining + */ + public ExecutionReport symbol(long symbol) { + this.symbol = symbol; + return this; + } + + /** + * Retrieves the side of the order (buy or sell). + * + * @return the side as a {@link Side} enum value + */ + public Side side() { + return side; + } + + /** + * Sets the side of the order (buy or sell). + * + * @param side the side to set + * @return this {@code ExecutionReport} instance for method chaining + */ + public ExecutionReport side(Side side) { + this.side = side; + return this; + } + + /** + * Retrieves the transaction time in nanoseconds. + * + * @return the transaction time as a {@code long} + */ + public long transactTime() { + return transactTime; + } + + /** + * Sets the transaction time in nanoseconds. + * + * @param transactTime the transaction time to set + * @return this {@code ExecutionReport} instance for method chaining + */ + public ExecutionReport transactTime(long transactTime) { + this.transactTime = transactTime; + return this; + } + + /** + * Retrieves the quantity of the order. + * + * @return the order quantity as a {@code double} + */ + public double orderQty() { + return orderQty; + } + + /** + * Sets the quantity of the order. + * + * @param orderQty the order quantity to set + * @return this {@code ExecutionReport} instance for method chaining + */ + public ExecutionReport orderQty(double orderQty) { + this.orderQty = orderQty; + return this; + } + + /** + * Retrieves the price of the order. + * + * @return the price as a {@code double} + */ + public double price() { + return price; + } + + /** + * Sets the price of the order. + * + * @param price the price to set + * @return this {@code ExecutionReport} instance for method chaining + */ + public ExecutionReport price(double price) { + this.price = price; + return this; + } + + /** + * Retrieves the order ID. + * + * @return the order ID as a {@code long} + */ + public long orderID() { + return orderID; + } + + /** + * Sets the order ID. + * + * @param orderID the order ID to set + * @return this {@code ExecutionReport} instance for method chaining + */ + public ExecutionReport orderID(long orderID) { + this.orderID = orderID; + return this; + } + /** + * Returns the order type. + * + * @return the order type + */ + public OrderType ordType() { + return ordType; + } + /** + * Sets the order type and returns this ExecutionReport instance. + * + * @param ordType the order type to set + * @return this ExecutionReport instance + */ + public ExecutionReport ordType(OrderType ordType) { + this.ordType = ordType; + return this; + } + + /** + * Retrieves the last traded price of the order. + * + * @return the last traded price as a {@code double} + */ + public double lastPx() { + return lastPx; + } + + /** + * Sets the last traded price of the order. + * + * @param lastPx the last price to set + * @return this {@code ExecutionReport} instance for method chaining + */ + public ExecutionReport lastPx(double lastPx) { + this.lastPx = lastPx; + return this; + } + + /** + * Retrieves the remaining quantity of the order. + * + * @return the leaves quantity as a {@code double} + */ + public double leavesQty() { + return leavesQty; + } + + /** + * Sets the remaining quantity of the order. + * + * @param leavesQty the leaves quantity to set + * @return this {@code ExecutionReport} instance for method chaining + */ + public ExecutionReport leavesQty(double leavesQty) { + this.leavesQty = leavesQty; + return this; + } + + /** + * Retrieves the accumulated quantity of the order. + * + * @return the cumulative quantity as a {@code double} + */ + public double cumQty() { + return cumQty; + } + + /** + * Sets the accumulated quantity of the order. + * + * @param cumQty the cumulative quantity to set + * @return this {@code ExecutionReport} instance for method chaining + */ + public ExecutionReport cumQty(double cumQty) { + this.cumQty = cumQty; + return this; + } + + /** + * Retrieves the average price of the order. + * + * @return the average price as a {@code double} + */ + public double avgPx() { + return avgPx; + } + + /** + * Sets the average price and returns this ExecutionReport instance. + * + * @param avgPx the average price to set + * @return this ExecutionReport instance + */ + public ExecutionReport avgPx(double avgPx) { + this.avgPx = avgPx; + return this; + } + + /** + * Retrieves the optional text message about the order execution. + * + * @return the text message as a {@code String} + */ + public String text() { + return text; + } + + /** + * Sets the text of the report and returns this ExecutionReport instance. + * + * @param text the text message to set + * @return this {@code ExecutionReport} instance for method chaining + */ + public ExecutionReport text(String text) { + this.text = text; + return this; + } + + @Override + public void writeMarshallable(BytesOut out) { + super.writeMarshallable(out); + if (PREGENERATED_MARSHALLABLE) { + out.writeStopBit(MASHALLABLE_VERSION); + out.writeLong(symbol); + out.writeLong(transactTime); + out.writeDouble(orderQty); + out.writeDouble(price); + out.writeLong(orderID); + out.writeDouble(lastPx); + out.writeDouble(leavesQty); + out.writeDouble(cumQty); + out.writeDouble(avgPx); + out.writeObject(Side.class, side); + out.writeObject(OrderType.class, ordType); + out.writeObject(String.class, clOrdID); + out.writeObject(String.class, text); + } + } + + @Override + public void readMarshallable(BytesIn in) { + super.readMarshallable(in); + if (PREGENERATED_MARSHALLABLE) { + int version = (int) in.readStopBit(); + if (version == MASHALLABLE_VERSION) { + symbol = in.readLong(); + transactTime = in.readLong(); + orderQty = in.readDouble(); + price = in.readDouble(); + orderID = in.readLong(); + lastPx = in.readDouble(); + leavesQty = in.readDouble(); + cumQty = in.readDouble(); + avgPx = in.readDouble(); + side = in.readObject(Side.class); + ordType = in.readObject(OrderType.class); + clOrdID = in.readObject(String.class); + text = in.readObject(String.class); + } else { + throw new IllegalStateException("Unknown version " + version); + } + } + } + + @Override + public void writeMarshallable(WireOut out) { + super.writeMarshallable(out); + if (PREGENERATED_MARSHALLABLE) { + out.write("symbol").writeLong(ShortTextLongConverter.INSTANCE, symbol); + out.write("transactTime").writeLong(NanoTimestampLongConverter.INSTANCE, transactTime); + out.write("orderQty").writeDouble(orderQty); + out.write("price").writeDouble(price); + out.write("orderID").writeLong(NanoTimestampLongConverter.INSTANCE, orderID); + out.write("lastPx").writeDouble(lastPx); + out.write("leavesQty").writeDouble(leavesQty); + out.write("cumQty").writeDouble(cumQty); + out.write("avgPx").writeDouble(avgPx); + out.write("side").object(Side.class, side); + out.write("ordType").object(OrderType.class, ordType); + out.write("clOrdID").object(String.class, clOrdID); + out.write("text").object(String.class, text); + } + } + + @Override + public void readMarshallable(WireIn in) { + super.readMarshallable(in); + if (PREGENERATED_MARSHALLABLE) { + symbol = in.read("symbol").readLong(ShortTextLongConverter.INSTANCE); + transactTime = in.read("transactTime").readLong(NanoTimestampLongConverter.INSTANCE); + orderQty = in.read("orderQty").readDouble(); + price = in.read("price").readDouble(); + orderID = in.read("orderID").readLong(NanoTimestampLongConverter.INSTANCE); + lastPx = in.read("lastPx").readDouble(); + leavesQty = in.read("leavesQty").readDouble(); + cumQty = in.read("cumQty").readDouble(); + avgPx = in.read("avgPx").readDouble(); + side = in.read("side").object(side, Side.class); + ordType = in.read("ordType").object(ordType, OrderType.class); + clOrdID = in.read("clOrdID").object(clOrdID, String.class); + text = in.read("text").object(text, String.class); + } + } + + /** + * Validates the fields of this {@code ExecutionReport} event. + * + * @throws InvalidMarshallableException if any required field is missing or invalid + */ + @Override + public void validate() throws InvalidMarshallableException { + super.validate(); + if (symbol == 0) + throw new InvalidMarshallableException("symbol is required"); + if (transactTime == 0) + throw new InvalidMarshallableException("transactTime is required"); + if (invalidQuantity(orderQty)) + throw new InvalidMarshallableException("orderQty is invalid"); + if (invalidPrice(price)) + throw new InvalidMarshallableException("price is invalid"); + if (orderID == 0) + throw new InvalidMarshallableException("orderID is required"); + if (lastPx != 0 && invalidPrice(lastPx)) + throw new InvalidMarshallableException("lastPx is invalid"); + if (invalidQuantity(leavesQty)) + throw new InvalidMarshallableException("leavesQty is invalid"); + if (invalidQuantity(cumQty)) + throw new InvalidMarshallableException("cumQty is invalid"); + if (avgPx != 0 && invalidPrice(avgPx)) + throw new InvalidMarshallableException("avgPx is invalid"); + if (side == null) + throw new InvalidMarshallableException("side is required"); + if (ordType == null) + throw new InvalidMarshallableException("ordType is required"); + if (clOrdID == null || clOrdID.isEmpty()) + throw new InvalidMarshallableException("clOrdID is required"); + } +} +.... + +Lines 421, Blanks 45, Tokens 3417 + +== File: src\main\java\town\lost\oms\dto\NewOrderSingle.java +.... +/* + */ + +package town.lost.oms.dto; + +import net.openhft.chronicle.bytes.BytesIn; +import net.openhft.chronicle.bytes.BytesOut; +import net.openhft.chronicle.core.io.InvalidMarshallableException; +import net.openhft.chronicle.wire.*; +import net.openhft.chronicle.wire.converter.NanoTime; +import net.openhft.chronicle.wire.converter.ShortText; + +import static town.lost.oms.dto.ValidateUtil.invalidPrice; +import static town.lost.oms.dto.ValidateUtil.invalidQuantity; + +/** + * The {@code NewOrderSingle} class represents a new single order in a trading system. + * + *

This class extends the {@link AbstractEvent} class, with the type parameter being {@link NewOrderSingle}. + * This indicates that the event will be processed into a {@link NewOrderSingle} that will be sent to the order + * system. + * + *

Each {@code NewOrderSingle} contains various pieces of information about the order, including: + * + *

    + *
  • symbol: The identifier of the financial instrument.
  • + *
  • transactTime: The transaction time in nanoseconds.
  • + *
  • account: The account associated with the order.
  • + *
  • orderQty: The quantity of the order.
  • + *
  • price: The price of the order.
  • + *
  • side: The side of the order (buy or sell).
  • + *
  • clOrdID: The client order ID.
  • + *
  • ordType: The type of the order (e.g., market or limit).
  • + *
  • timeInForce: The time-in-force instruction for the order.
  • + *
  • currency: The currency of the order.
  • + *
+ * + *

The symbol is encoded using {@link ShortText} and the transaction time is in nanoseconds, both to save space. + * The client order ID is a string that identifies the order, the side indicates whether the order is to buy or sell, + * and the order type indicates whether the order is a market order or a limit order. + */ +public class NewOrderSingle extends AbstractEvent { + private static final int MASHALLABLE_VERSION = 1; + // Symbol of the financial instrument. + @ShortText + private long symbol; + // Transaction time in nanoseconds. + @NanoTime + private long transactTime; + @ShortText + private long account; + + // Quantity of the order. + private double orderQty; + + // Price of the order. + private double price; + + // Side of the order (buy or sell). + private Side side; + + // Client order ID. + private String clOrdID = ""; + + // Type of the order (market or limit). + private OrderType ordType; + + // Time-in-force instruction for the order. + private TimeInForce timeInForce; + + // Currency of the order. + private Ccy currency; + + /** + * Get the client order ID. + * + * @return The client order ID as a string. + */ + public String clOrdID() { + return clOrdID; + } + + /** + * Sets the client order ID. + * + * @param clOrdID The client order ID to set, as a string. + * @return This {@code NewOrderSingle} instance, to facilitate method chaining. + */ + public NewOrderSingle clOrdID(String clOrdID) { + this.clOrdID = clOrdID; + return this; + } + + /** + * Get the symbol of the financial instrument. + * + * @return The symbol as a long. + */ + public long symbol() { + return symbol; + } + + /** + * Sets the symbol of the financial instrument. + * + * @param symbol The symbol to set, as a long. + * @return This {@code NewOrderSingle} instance, to facilitate method chaining. + */ + public NewOrderSingle symbol(long symbol) { + this.symbol = symbol; + return this; + } + + /** + * Get the side of the order (buy or sell). + * + * @return The side as a {@link Side} enum value. + */ + public Side side() { + return side; + } + + /** + * Sets the side of the order (buy or sell). + * + * @param side The side to set, as a {@link Side} enum value. + * @return This {@code NewOrderSingle} instance, to facilitate method chaining. + */ + public NewOrderSingle side(Side side) { + this.side = side; + return this; + } + + /** + * Get the transaction time in nanoseconds. + * + * @return The transaction time as a long. + */ + public long transactTime() { + return transactTime; + } + + /** + * Sets the transaction time in nanoseconds. + * + * @param transactTime The transaction time to set, as a long. + * @return This {@code NewOrderSingle} instance, to facilitate method chaining. + */ + public NewOrderSingle transactTime(long transactTime) { + this.transactTime = transactTime; + return this; + } + + /** + * Get the quantity of the order. + * + * @return The order quantity as a double. + */ + public double orderQty() { + return orderQty; + } + + /** + * Sets the quantity of the order. + * + * @param orderQty The order quantity to set, as a double. + * @return This {@code NewOrderSingle} instance, to facilitate method chaining. + */ + public NewOrderSingle orderQty(double orderQty) { + this.orderQty = orderQty; + return this; + } + + /** + * Get the price of the order. + * + * @return The price as a double. + */ + public double price() { + return price; + } + + /** + * Sets the price of the order. + * + * @param price The price to set, as a double. + * @return This {@code NewOrderSingle} instance, to facilitate method chaining. + */ + public NewOrderSingle price(double price) { + this.price = price; + return this; + } + + /** + * Get the type of the order (market or limit). + * + * @return The order type as an {@link OrderType} enum value. + */ + public OrderType ordType() { + return ordType; + } + + /** + * Sets the type of the order (market or limit). + * + * @param ordType The order type to set, as an {@link OrderType} enum value. + * @return This {@code NewOrderSingle} instance, to facilitate method chaining. + */ + public NewOrderSingle ordType(OrderType ordType) { + this.ordType = ordType; + return this; + } + + /** + * Retrieves the time-in-force instruction for the order. + * + * @return The time-in-force as a {@link TimeInForce} enum value. + */ + public TimeInForce timeInForce() { + return timeInForce; + } + + /** + * Sets the time-in-force instruction for the order. + * + * @param timeInForce The time-in-force to set. + * @return This {@code NewOrderSingle} instance for method chaining. + */ + public NewOrderSingle timeInForce(TimeInForce timeInForce) { + this.timeInForce = timeInForce; + return this; + } + + /** + * Retrieves the account associated with the order. + * + * @return The account as a {@code long}. + */ + public long account() { + return account; + } + + /** + * Sets the account associated with the order. + * + * @param account The account to set. + * @return This {@code NewOrderSingle} instance for method chaining. + */ + public NewOrderSingle account(long account) { + this.account = account; + return this; + } + + /** + * Retrieves the currency of the order. + * + * @return The currency as a {@link Ccy} enum value. + */ + public Ccy currency() { + return currency; + } + + /** + * Sets the currency of the order. + * + * @param currency The currency to set. + * @return This {@code NewOrderSingle} instance for method chaining. + */ + public NewOrderSingle currency(Ccy currency) { + this.currency = currency; + return this; + } + + @Override + public void writeMarshallable(BytesOut out) { + super.writeMarshallable(out); + if (PREGENERATED_MARSHALLABLE) { + out.writeStopBit(MASHALLABLE_VERSION); + out.writeLong(symbol); + out.writeLong(transactTime); + out.writeLong(account); + out.writeDouble(orderQty); + out.writeDouble(price); + out.writeObject(Side.class, side); + out.writeObject(OrderType.class, ordType); + out.writeObject(String.class, clOrdID); + out.writeObject(TimeInForce.class, timeInForce); + out.writeObject(Ccy.class, currency); + } + } + + @Override + public void readMarshallable(BytesIn in) { + super.readMarshallable(in); + if (PREGENERATED_MARSHALLABLE) { + int version = (int) in.readStopBit(); + if (version == MASHALLABLE_VERSION) { + symbol = in.readLong(); + transactTime = in.readLong(); + account = in.readLong(); + orderQty = in.readDouble(); + price = in.readDouble(); + side = in.readObject(Side.class); + ordType = in.readObject(OrderType.class); + clOrdID = in.readObject(String.class); + timeInForce = in.readObject(TimeInForce.class); + currency = in.readObject(Ccy.class); + } + } + } + + @Override + public void writeMarshallable(WireOut out) { + super.writeMarshallable(out); + if (PREGENERATED_MARSHALLABLE) { + out.write("symbol").writeLong(ShortTextLongConverter.INSTANCE, symbol); + out.write("transactTime").writeLong(NanoTimestampLongConverter.INSTANCE, transactTime); + out.write("account").writeLong(ShortTextLongConverter.INSTANCE, account); + out.write("orderQty").writeDouble(orderQty); + out.write("price").writeDouble(price); + out.write("side").object(Side.class, side); + out.write("ordType").object(OrderType.class, ordType); + out.write("clOrdID").object(String.class, clOrdID); + out.write("timeInForce").object(TimeInForce.class, timeInForce); + out.write("currency").object(Ccy.class, currency); + } + } + + @Override + public void readMarshallable(WireIn in) { + super.readMarshallable(in); + if (PREGENERATED_MARSHALLABLE) { + symbol = in.read("symbol").readLong(ShortTextLongConverter.INSTANCE); + transactTime = in.read("transactTime").readLong(NanoTimestampLongConverter.INSTANCE); + account = in.read("account").readLong(ShortTextLongConverter.INSTANCE); + orderQty = in.read("orderQty").readDouble(); + price = in.read("price").readDouble(); + side = in.read("side").object(side, Side.class); + ordType = in.read("ordType").object(OrderType.class); + clOrdID = in.read("clOrdID").object(clOrdID, String.class); + timeInForce = in.read("timeInForce").object(TimeInForce.class); + currency = in.read("currency").object(Ccy.class); + } + } + + /** + * Validates the fields of this {@code NewOrderSingle} event. + * + * @throws InvalidMarshallableException if any required field is missing or invalid. + */ + @Override + public void validate() throws InvalidMarshallableException { + super.validate(); + if (symbol == 0) + throw new InvalidMarshallableException("symbol is required"); + if (transactTime == 0) + throw new InvalidMarshallableException("transactTime is required"); + if (account == 0) + throw new InvalidMarshallableException("account is required"); + if (orderQty == 0) + throw new InvalidMarshallableException("orderQty is required"); + if (invalidQuantity(orderQty)) + throw new InvalidMarshallableException("orderQty is invalid"); + if (invalidPrice(price)) + throw new InvalidMarshallableException("price is invalid"); + if (side == null) + throw new InvalidMarshallableException("side is required"); + if (ordType == null) + throw new InvalidMarshallableException("ordType is required"); + if (clOrdID == null || clOrdID.isEmpty()) + throw new InvalidMarshallableException("clOrdID is required"); + if (timeInForce == null) + throw new InvalidMarshallableException("timeInForce is required"); + if (currency == null) + throw new InvalidMarshallableException("currency is required"); + } +} +.... + +Lines 341, Blanks 36, Tokens 2886 + +== File: src\main\java\town\lost\oms\dto\OrderCancelReject.java +.... +/* + */ + +package town.lost.oms.dto; + +import net.openhft.chronicle.bytes.BytesIn; +import net.openhft.chronicle.bytes.BytesOut; +import net.openhft.chronicle.core.io.InvalidMarshallableException; +import net.openhft.chronicle.wire.ShortTextLongConverter; +import net.openhft.chronicle.wire.WireIn; +import net.openhft.chronicle.wire.WireOut; +import net.openhft.chronicle.wire.converter.ShortText; + +/** + * The {@code OrderCancelReject} class represents an event that is used to indicate that a request to cancel an order was rejected. + * + *

This class extends the {@link AbstractEvent} class, with the type parameter being {@link OrderCancelReject}. + * This indicates that the event will be processed into a {@link OrderCancelReject} that will be sent to the order + * system. + * + *

Each {@code OrderCancelReject} event contains: + *

    + *
  • symbol: The identifier for the financial instrument involved in the order.
  • + *
  • clOrdID: The client order ID of the order that was requested to be canceled.
  • + *
  • reason: A string explaining why the cancellation request was rejected.
  • + *
+ * + *

Note that the symbol field is encoded using {@link ShortText} to minimize the space required for storage and transmission. + */ +public class OrderCancelReject extends AbstractEvent { + private static final int MASHALLABLE_VERSION = 1; + // Symbol for which the order cancellation was rejected. + @ShortText + private long symbol; + + // Client order ID of the order that was requested to be canceled. + private String clOrdID = ""; + + // Reason for the rejection of the order cancellation request. + private String reason = ""; + + /** + * Get the client order ID of the order that was requested to be canceled. + * + * @return The client order ID as a string. + */ + public String clOrdID() { + return clOrdID; + } + + /** + * Set the client order ID of the order that was requested to be canceled. + * + * @param clOrdID The client order ID to set, as a string. + * @return This OrderCancelReject instance, to facilitate method chaining. + */ + public OrderCancelReject clOrdID(String clOrdID) { + this.clOrdID = clOrdID; + return this; + } + + /** + * Get the symbol for which the order cancellation was rejected. + * + * @return The symbol value as a {@code long}. + */ + public long symbol() { + return symbol; + } + + /** + * Set the symbol for which the order cancellation was rejected. + * + * @param symbol The symbol value to set, as a long. + * @return This {@code OrderCancelReject} instance, to facilitate method chaining. + */ + public OrderCancelReject symbol(long symbol) { + this.symbol = symbol; + return this; + } + + /** + * Get the reason for the rejection of the order cancellation request. + * + * @return The reason as a string. + */ + public String reason() { + return reason; + } + + /** + * Set the reason for the rejection of the order cancellation request. + * + * @param reason The reason to set, as a string. + * @return This {@code OrderCancelReject} instance, to facilitate method chaining. + */ + public OrderCancelReject reason(String reason) { + this.reason = reason; + return this; + } + + @Override + public void writeMarshallable(WireOut out) { + validate(); + super.writeMarshallable(out); + if (PREGENERATED_MARSHALLABLE) { + out.write("symbol").writeLong(ShortTextLongConverter.INSTANCE, symbol); + out.write("clOrdID").object(String.class, clOrdID); + out.write("reason").object(String.class, reason); + } + } + + @Override + public void readMarshallable(WireIn in) { + super.readMarshallable(in); + if (PREGENERATED_MARSHALLABLE) { + symbol = in.read("symbol").readLong(ShortTextLongConverter.INSTANCE); + clOrdID = in.read("clOrdID").object(clOrdID, String.class); + reason = in.read("reason").object(reason, String.class); + } + } + + @Override + public void writeMarshallable(BytesOut out) { + validate(); + super.writeMarshallable(out); + if (PREGENERATED_MARSHALLABLE) { + out.writeStopBit(MASHALLABLE_VERSION); + out.writeLong(symbol); + out.writeObject(String.class, clOrdID); + out.writeObject(String.class, reason); + } + } + + @Override + public void readMarshallable(BytesIn in) { + super.readMarshallable(in); + if (PREGENERATED_MARSHALLABLE) { + int version = (int) in.readStopBit(); + if (version == MASHALLABLE_VERSION) { + symbol = in.readLong(); + clOrdID = in.readObject(String.class); + reason = in.readObject(String.class); + } else { + throw new IllegalStateException("Unknown version " + version); + } + } + } + + /** + * Validates the fields of this {@code OrderCancelReject} event. + * + * @throws InvalidMarshallableException if any required field is missing or invalid. + */ + @Override + public void validate() throws InvalidMarshallableException { + super.validate(); + if (symbol == 0) + throw new InvalidMarshallableException("symbol is required"); + if (clOrdID == null || clOrdID.isEmpty()) + throw new InvalidMarshallableException("clOrdID is required"); + if (reason == null || reason.isEmpty()) + throw new InvalidMarshallableException("reason is required"); + } +} +.... + +Lines 149, Blanks 16, Tokens 1294 + +== File: src\main\java\town\lost\oms\dto\OrderType.java +.... +/* + */ + +package town.lost.oms.dto; +/** + * Enumeration for the types of orders in a trading system. + * + *

It contains two values: MARKET and LIMIT, which represent the two main types of orders that can be placed + * in a trading system. A MARKET order is an order to buy or sell a security at the current market price, + * whereas a LIMIT order is an order to buy or sell a security at a specific price or better. + * + *

This enumeration can be used when creating trading orders, like so: + * + *

+ * NewOrderSingle nos = new NewOrderSingle()
+ *    .sender(ShortText.parse("client"))
+ *    .target(ShortText.parse("OMS"))
+ *    .clOrdID("clOrdId")
+ *    .orderQty(1e6)
+ *    .price(1.6)
+ *    .symbol(ShortText.parse("AUDUSD"))
+ *    .ordType(OrderType.LIMIT)
+ *    .side(BuySell.BUY);
+ * 
+ * + *

Note that the order type is indicated by the {@link OrderType} used in the 'ordType' field of the order. + */ +public enum OrderType { + /** + * Market order type, which means the order should be executed at the current market price. + */ + MARKET, + + /** + * Limit order type, which means the order should be executed at a specific price or better. + */ + LIMIT, + + /** + * Pegged order type, where the price is pegged to a benchmark price, such as the best bid or ask. + */ + PEGGED, + + /** + * Fill or Kill order type, which must be executed immediately in its entirety or cancelled. + */ + FILL_OR_KILL, + + /** + * Immediate or Cancel order type, which executes all or part immediately and cancels any unfilled portion. + */ + IMMEDIATE_OR_CANCEL, +} + +.... + +Lines 48, Blanks 6, Tokens 409 + +== File: src\main\java\town\lost\oms\dto\package-info.java +.... +/** + * Provides the classes and interfaces necessary to create and manage Order Management System (OMS) data transfer objects (DTOs). + * + *

This package includes the following key components: + * + *

    + *
  • AbstractEvent and its subclasses, which represent different types of events that can occur in an OMS, such as: + *
      + *
    • {@link town.lost.oms.dto.NewOrderSingle}: Represents a new order submission.
    • + *
    • {@link town.lost.oms.dto.CancelOrderRequest}: Represents a request to cancel an existing order.
    • + *
    • {@link town.lost.oms.dto.ExecutionReport}: Represents the execution status of an order.
    • + *
    • {@link town.lost.oms.dto.OrderCancelReject}: Indicates that an order cancellation request was rejected.
    • + *
    • {@link town.lost.oms.dto.CancelAll}: Represents a request to cancel all orders for a specific symbol.
    • + *
    + *
  • + *
  • Enums that define constants for various order attributes: + *
      + *
    • {@link town.lost.oms.dto.Side}: Represents the side of an order (e.g., buy or sell).
    • + *
    • {@link town.lost.oms.dto.OrderType}: Represents the type of an order (e.g., market, limit).
    • + *
    • {@link town.lost.oms.dto.TimeInForce}: Represents the time-in-force instructions for an order.
    • + *
    • {@link town.lost.oms.dto.Ccy}: Represents currency codes as per ISO 4217.
    • + *
    + *
  • + *
  • Utility Classes: + *
      + *
    • {@link town.lost.oms.dto.ValidateUtil}: Provides utility methods for validating order parameters such as price and quantity.
    • + *
    + *
  • + *
+ * + *

Each class is designed to be marshalled and unmarshalled efficiently for high-performance data transfer, leveraging serialization optimizations provided by Chronicle Wire. + * + *

For more details, refer to the documentation of each individual class. + */ +package town.lost.oms.dto; +.... + +Lines 35, Blanks 0, Tokens 493 + +== File: src\main\java\town\lost\oms\dto\Side.java +.... +/* + */ + +package town.lost.oms.dto; + +/** + * Enumeration for the direction of a trade order. + * + *

It contains two values: BUY and SELL, which represents the direction of the order. BUY (+1) means the order is to + * purchase, while SELL (-1) implies the order is to sell. + * + *

This enumeration can be used to create trading orders, like so: + * + *

{@code
+ * NewOrderSingle nos = new NewOrderSingle()
+ *    .sender(toLong("sender"))
+ *    .target(toLong("target"))
+ *    .transactTime(now())
+ *    .sendingTime(now())
+ *    .orderQty(1)
+ *    .ordType(OrderType.MARKET)
+ *    .side(Side.BUY)
+ *    .symbol(toLong("EURUSD"));
+ * }
+ * + *

Note that the direction is indicated by the {@link Side} used in the 'side' field of the order. + */ +public enum Side { + /** + * Buy order direction, represented by an integer value of +1. + * Indicates an order to purchase. + */ + BUY(+1), + + /** + * Sell order direction, represented by an integer value of -1. + * Indicates an order to sell. + */ + SELL(-1); + + /** + * The direction of the order. + */ + public final int direction; + + /** + * Constructs a BuySell enum with the specified direction. + * + * @param direction the direction of the order (+1 for buy, -1 for sell) + */ + Side(int direction) { + this.direction = direction; + } + + /** + * Gets the direction indicator of the order. + * + * @return the direction indicator as an integer + */ + public int direction() { + return direction; + } + } +.... + +Lines 57, Blanks 6, Tokens 392 + +== File: src\main\java\town\lost\oms\dto\TimeInForce.java +.... +package town.lost.oms.dto; + +/** + * The {@code TimeInForce} enum represents the time conditions under which an order will be executed. + * + *

These conditions specify how long an order remains active in the market before it is executed or expires. + * This enum includes commonly used time-in-force instructions in trading systems, conforming to standard trading practices. + * + *

Example usage: + *

{@code
+ * TimeInForce tif = TimeInForce.DAY;
+ * System.out.println("Time in Force: " + tif);
+ * }
+ * + *

This would output: + *

+ * Time in Force: DAY
+ * 
+ */ +public enum TimeInForce { + /** + * Indicates that the order should be valid for the current trading day only. + * If not executed by the end of the trading day, the order expires. + */ + DAY, + + /** + * "Good Till Cancelled" - The order remains active until it is either executed or explicitly cancelled by the trader. + * It may carry over to the next trading day. + */ + GTC, + + /** + * "Immediate or Cancel" - The order attempts to execute all or part immediately. + * Any portion not immediately executed is cancelled. + */ + IOC, + + /** + * "Fill or Kill" - The order must be executed immediately in its entirety, or it is cancelled. + * Partial fills are not allowed. + */ + FOK, + + /** + * "Good Till Date" - The order remains active until a specified date unless it is executed or cancelled before then. + */ + GTD, + + /** + * "Good Till Time" - The order remains active until a specified time on the current trading day. + * If not executed by that time, the order expires. + */ + GTT, + + /** + * "At the Opening" - The order is to be executed at the opening of the market. + * If not executed at the opening, it is cancelled. + */ + OPG, + + /** + * "At the Close" - The order is to be executed at the close of the market. + * If not executed at the close, it is cancelled. + */ + ATC, + + /** + * "Good for Auction" - The order is valid only during the auction period. + */ + GFA, + + /** + * "Good in Session" - The order is valid only during the specified trading session. + */ + GIS, + + /** + * "Good Through Crossing" - The order is valid through the crossing session. + */ + GTX, + + /** + * "Systematic Internaliser Only" - The order is to be executed only on a systematic internaliser. + */ + SIO +} +.... + +Lines 75, Blanks 12, Tokens 586 + +== File: src\main\java\town\lost\oms\dto\ValidateUtil.java +.... +/* + */ +package town.lost.oms.dto; + +/** + * The {@code ValidateUtil} class provides utility methods for validating numerical values such as price and quantity. + * + *

This class is final and cannot be instantiated. It contains static methods that check whether a given price or quantity + * is valid according to predefined business rules. + * + *

Example usage: + * + *

{@code
+ * double price = 100.0;
+ * if (ValidateUtil.invalidPrice(price)) {
+ *     throw new IllegalArgumentException("Invalid price");
+ * }
+ * }
+ */ +public final class ValidateUtil { + + /** + * Private constructor to prevent instantiation. + */ + private ValidateUtil() { + } + + /** + * Checks if the provided price is invalid. + * + *

A price is considered invalid if it is not a finite number or if it is less than or equal to zero. + * + * @param price the price to validate + * @return {@code true} if the price is invalid; {@code false} otherwise + */ + public static boolean invalidPrice(double price) { + return !Double.isFinite(price) || (price <= 0); + } + + /** + * Checks if the provided quantity is invalid. + * + *

A quantity is considered invalid if it is not a finite number or if it is less than zero. + * + * @param quantity the quantity to validate + * @return {@code true} if the quantity is invalid; {@code false} otherwise + */ + public static boolean invalidQuantity(double quantity) { + return !Double.isFinite(quantity) || (quantity < 0); + } +} +.... + +Lines 47, Blanks 4, Tokens 352 + +== File: src\main\java\town\lost\oms\OMSBenchmarkMain.java +.... +/* + */ + +package town.lost.oms; + +import net.openhft.affinity.AffinityLock; +import net.openhft.chronicle.bytes.MethodReader; +import net.openhft.chronicle.core.Jvm; +import net.openhft.chronicle.core.OS; +import net.openhft.chronicle.core.io.BackgroundResourceReleaser; +import net.openhft.chronicle.core.io.IOTools; +import net.openhft.chronicle.jlbh.JLBH; +import net.openhft.chronicle.jlbh.JLBHOptions; +import net.openhft.chronicle.jlbh.JLBHTask; +import net.openhft.chronicle.queue.ChronicleQueue; +import net.openhft.chronicle.threads.DiskSpaceMonitor; +import net.openhft.chronicle.wire.ShortTextLongConverter; +import town.lost.oms.api.OMSIn; +import town.lost.oms.api.OMSOut; +import town.lost.oms.dto.*; + +import static town.lost.oms.OrderAdderMain.now; +// isolcpus=5,6,7 set in grub.cfg +// sudo cpupower frequency-set -g performance -d 4.5g + +// -Xmx64m -XX:+UnlockCommercialFeatures -XX:+FlightRecorder -XX:StartFlightRecording=name=test,filename=test.jfr,dumponexit=true,settings=profile -XX:-UseTLAB +/* Run on a Ryzen 9 5950X, Ubuntu 20.10 +-Xmx1g -DbyteInBinary=true -DpregeneratedMarshallable=true -Dthroughput=100000 -DrunTime=120 -Dpath=/tmp +-------------------------------- SUMMARY (end to end) us ------------------------------------------- +Percentile run1 run2 run3 run4 run5 % Variation +50.0: 2.06 1.99 1.99 1.99 1.99 0.00 +90.0: 2.13 2.04 2.04 2.05 2.05 0.33 +99.0: 3.16 3.09 3.09 3.10 3.10 0.17 +99.7: 3.66 3.57 3.58 3.58 3.57 0.15 +99.9: 4.02 3.92 3.90 3.90 3.90 0.41 +99.97: 4.78 4.66 4.65 4.70 4.65 0.68 +99.99: 5.58 5.29 5.21 5.34 5.14 2.43 +99.997: 8.56 6.01 5.88 6.34 5.90 5.00 +99.999: 9.30 6.81 6.42 9.39 6.47 23.55 +worst: 50.11 74.11 10.58 13.30 11.15 80.02 +---------------------------------------------------------------------------------------------------- + +-Xmx772m -DbyteInBinary=true -DpregeneratedMarshallable=true -Dthroughput=100000 -DrunTime=120 -Dpath=/nvme/tmp +-------------------------------- SUMMARY (end to end) us ------------------------------------------- +Percentile run1 run2 run3 run4 run5 % Variation +50.0: 1.98 1.97 1.97 1.97 1.97 0.00 +90.0: 2.05 2.04 2.04 2.04 2.04 0.00 +99.0: 3.38 3.39 3.36 3.34 3.34 0.95 +99.7: 6.34 6.38 5.80 5.45 5.46 10.20 +99.9: 16.27 16.42 16.48 16.42 16.48 0.26 +99.97: 17.38 17.38 17.38 17.25 17.38 0.49 +99.99: 18.91 18.46 18.66 18.21 18.66 1.61 +99.997: 24.29 22.24 22.82 21.09 25.12 11.31 +99.999: 29.15 27.94 26.40 25.76 402.94 90.71 +worst: 214.27 51.78 537.60 50.24 1255.42 94.12 +---------------------------------------------------------------------------------------------------- + */ +public class OMSBenchmarkMain { + static { + System.setProperty("pauser.minProcessors", "2"); + System.setProperty("disableValidate", "true"); + } + + public static final int THROUGHPUT = Integer.getInteger("throughput", 100_000); + public static final int RUN_TIME = Integer.getInteger("runTime", 10); + public static final ShortTextLongConverter ShortText = ShortTextLongConverter.INSTANCE; + public static final String PATH = System.getProperty("path", OS.TMP); + public static final boolean ACCOUNT_FOR_COORDINATED_OMISSION = Jvm.getBoolean("accountForCoordinatedOmission"); + + @SuppressWarnings("try") + public static void main(String[] args) { + printProperties(); + + String tmpDir = PATH + "/bench-" + System.nanoTime(); + try (ChronicleQueue input = single(tmpDir, "/input"); + ChronicleQueue output = single(tmpDir, "/output")) { + + // processing thread + Thread processor = new Thread(() -> { + try (AffinityLock ignored = AffinityLock.acquireCore()) { + OMSOut out = output.createAppender().methodWriter(OMSOut.class); + OMSImpl oms = new OMSImpl(out); + MethodReader in = input.createTailer("test").methodReader(oms); + while (!Thread.currentThread().isInterrupted()) { + in.readOne(); + } + } + }, "processor"); + processor.start(); + + JLBH jlbh = new JLBH(new JLBHOptions() + .warmUpIterations(300_000) + .pauseAfterWarmupMS(500) + .throughput(THROUGHPUT) + .iterations(THROUGHPUT * RUN_TIME) + .runs(5) + .recordOSJitter(false) + .accountForCoordinatedOmission(ACCOUNT_FOR_COORDINATED_OMISSION) + .acquireLock(AffinityLock::acquireCore) + .jlbhTask(new MyJLBHTask(input))); + + Thread last = new Thread(() -> { + try (AffinityLock ignored = AffinityLock.acquireCore()) { + final MethodReader reader = output.createTailer().methodReader(new OMSOut() { + @Override + public void executionReport(ExecutionReport er) { + jlbh.sampleNanos(System.nanoTime() - er.sendingTime()); + } + + @Override + public void orderCancelReject(OrderCancelReject ocr) { + } + + @Override + public void jvmError(String msg) { + } + }); + while (!Thread.currentThread().isInterrupted()) + reader.readOne(); + + } catch (Throwable t) { + t.printStackTrace(); + } + }, "last"); + last.start(); + + jlbh.start(); + + processor.interrupt(); + last.interrupt(); + } + printProperties(); + Jvm.pause(1000); + IOTools.deleteDirWithFiles(tmpDir); + // for a clean shutdown + BackgroundResourceReleaser.stop(); + DiskSpaceMonitor.INSTANCE.close(); + } + + static ChronicleQueue single(String tmpDir, String x) { + return ChronicleQueue.singleBuilder(tmpDir + x) + .blockSize(OS.isSparseFileSupported() ? 64L << 30 : 64L << 20) + .build(); + } + + private static void printProperties() { + long estimatedMemory = Math.round(Runtime.getRuntime().totalMemory() / 1e6); + System.out.println("-Xmx" + estimatedMemory + "m " + + "-DbyteInBinary=" + AbstractEvent.BYTES_IN_BINARY + " " + + "-DpregeneratedMarshallable=" + AbstractEvent.PREGENERATED_MARSHALLABLE + " " + + "-Dthroughput=" + THROUGHPUT + " " + + "-DrunTime=" + RUN_TIME + " " + + "-Dpath=" + PATH + " " + + "-DaccountForCoordinatedOmission=" + ACCOUNT_FOR_COORDINATED_OMISSION); + } + + private static class MyJLBHTask implements JLBHTask { + private JLBH jlbh; + private NewOrderSingle nos; + private OMSIn in; + + public MyJLBHTask(ChronicleQueue input) { + nos = new NewOrderSingle() + .sender(ShortText.parse("client")) + .target(ShortText.parse("OMS")) + .clOrdID("clOrdId") + .orderQty(1e6) + .price(1.6) + .symbol(ShortText.parse("AUDUSD")) + .ordType(OrderType.LIMIT) + .side(Side.SELL); + in = input.createAppender().methodWriter(OMSIn.class); + } + + @Override + public void init(JLBH jlbh) { + this.jlbh = jlbh; + } + + @Override + public void run(long startTimeNS) { + nos.sendingTime(startTimeNS).transactTime(now()); + in.newOrderSingle(nos); + } + } +} +.... + +Lines 164, Blanks 22, Tokens 2236 + +== File: src\main\java\town\lost\oms\OMSImpl.java +.... +/* + */ + +package town.lost.oms; + +import net.openhft.chronicle.core.time.SystemTimeProvider; +import town.lost.oms.api.OMSIn; +import town.lost.oms.api.OMSOut; +import town.lost.oms.dto.*; + +/** + * The {@code OMSImpl} class is a concrete implementation of the {@link OMSIn} interface, + * acting as an intermediary between client requests and the {@link OMSOut} interface. + * + *

This class processes requests for order creation, order cancellation, and cancelling all orders. + * It generates appropriate responses using {@link ExecutionReport} and {@link OrderCancelReject} + * and sends them through the {@link OMSOut} interface. + * + *

Note: This class is not thread-safe. If multiple threads are expected to use + * the same instance of {@code OMSImpl}, synchronization or separate instances per thread should be used. + * + *

Usage Example:

+ * + *
{@code
+ * OMSOut omsOut = new OMSOutImplementation();
+ * OMSIn oms = new OMSImpl(omsOut);
+ *
+ * NewOrderSingle newOrder = new NewOrderSingle()
+ *     .sender("ClientA")
+ *     .target("OMS")
+ *     .clOrdID("Order123")
+ *     .symbol("AAPL")
+ *     .orderQty(100)
+ *     .price(150.00)
+ *     .side(Side.BUY)
+ *     .ordType(OrderType.MARKET)
+ *     .transactTime(System.currentTimeMillis());
+ *
+ * oms.newOrderSingle(newOrder);
+ * }
+ */ +public class OMSImpl implements OMSIn { + // The outbound interface for sending execution reports and order cancel rejections + private final OMSOut out; + + // Reusable instance of ExecutionReport for creating new orders + private final ExecutionReport er = new ExecutionReport(); + + // Reusable instance of OrderCancelReject for cancelling orders + private final OrderCancelReject ocr = new OrderCancelReject(); + + /** + * Constructs a new {@code OMSImpl} with a given outbound interface. + * + * @param out the outbound interface to be used for sending responses + */ + public OMSImpl(OMSOut out) { + this.out = out; + } + + /** + * Processes a new single order request. The request's details are populated into an execution report and sent out. + * + * @param nos the {@link NewOrderSingle} request to process + */ + @Override + public void newOrderSingle(NewOrderSingle nos) { + // Reset the execution report DTO + er.reset(); + final long orderID = SystemTimeProvider.CLOCK.currentTimeNanos(); // Generate unique order ID + + // Populate the ExecutionReport with request details + er.sender(nos.target()) + .target(nos.sender()) + .symbol(nos.symbol()) + .clOrdID(nos.clOrdID()) + .ordType(nos.ordType()) + .orderQty(nos.orderQty()) + .price(nos.price()) + .side(nos.side()) + .sendingTime(nos.sendingTime()) + .transactTime(nos.transactTime()) + .leavesQty(0) + .cumQty(0) + .avgPx(0) + .orderID(orderID) + .text("Not ready"); + + // Send execution report + out.executionReport(er); + } + + /** + * Processes a cancel order request. The request's details are populated into an order cancel rejection and sent out. + * + * @param cor the {@link CancelOrderRequest} request to process + */ + @Override + public void cancelOrderRequest(CancelOrderRequest cor) { + // Reset the reusable OrderCancelReject instance + ocr.reset(); + // Populate OrderCancelReject with request details + ocr.sender(cor.target()) + .target(cor.sender()) + .symbol(cor.symbol()) + .clOrdID(cor.clOrdID()) + .sendingTime(cor.sendingTime()) + .reason("No such order"); + + // Send order cancellation rejection + out.orderCancelReject(ocr); + } + + /** + * Processes a cancel all orders request. The request's details are populated into an order cancel rejection and sent out. + * + * @param cancelAll the {@link CancelAll} request to process + */ + @Override + public void cancelAll(CancelAll cancelAll) { + // Reset the reusable OrderCancelReject instance + ocr.reset(); + // Populate OrderCancelReject with request details + ocr.sender(cancelAll.target()) + .target(cancelAll.sender()) + .symbol(cancelAll.symbol()) + .clOrdID(cancelAll.clOrdID()) + .sendingTime(cancelAll.sendingTime()) + .reason("No orders to cancel"); + + // Send order cancellation rejection + out.orderCancelReject(ocr); + } +} +.... + +Lines 121, Blanks 13, Tokens 1041 + +== File: src\main\java\town\lost\oms\OrderAdderMain.java +.... +/* + */ + +package town.lost.oms; + +import net.openhft.chronicle.core.pool.ClassAliasPool; +import net.openhft.chronicle.core.time.SystemTimeProvider; +import net.openhft.chronicle.core.util.Mocker; +import net.openhft.chronicle.queue.ChronicleQueue; +import net.openhft.chronicle.queue.ExcerptAppender; +import net.openhft.chronicle.queue.impl.single.SingleChronicleQueueBuilder; +import net.openhft.chronicle.queue.rollcycles.TestRollCycles; +import net.openhft.chronicle.wire.converter.ShortText; +import town.lost.oms.api.OMSIn; +import town.lost.oms.dto.*; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; + +/** + * The {@code OrderAdderMain} class is a utility application that allows users to add new orders to the Order Management System (OMS). + * + *

This class connects to a Chronicle Queue and writes {@link NewOrderSingle} messages to it. + * Users can interactively add orders by hitting the Enter key. The application will prompt the user + * and continue to accept orders until any non-empty input is entered. + */ +public class OrderAdderMain { + + /** + * The entry point of the application. + * + * @param args the input arguments (none expected) + */ + public static void main(String[] args) throws IOException { + // Add NewOrderSingle class to the alias pool + ClassAliasPool.CLASS_ALIASES.addAlias(NewOrderSingle.class); + + // Establish connection with the queue + try (ChronicleQueue q = SingleChronicleQueueBuilder.binary("in") + .rollCycle(TestRollCycles.TEST8_DAILY) + .build(); + ExcerptAppender appender = q.createAppender()) { + + // Acquire the appender and write methods for OMSIn + OMSIn in = appender.methodWriter(OMSIn.class); + + // Create a logging mock for OMSIn + OMSIn in2 = Mocker.logging(OMSIn.class, "in - ", System.out); + + // Create a new order single + NewOrderSingle nos = new NewOrderSingle() + .sender(fromShortText("sender")) + .target(fromShortText("target")) + .transactTime(now()) + .sendingTime(now()) + .account(1) + .timeInForce(TimeInForce.GTC) + .currency(Ccy.USD) + .orderQty(1) + .ordType(OrderType.MARKET) + .side(Side.BUY) + .symbol(fromShortText("EURUSD")); + + // Inform the user to add an order + System.out.println("\nHit blank line to add an order, anything else to exit"); + + // Initialize a BufferedReader to read user input + BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); + int orderCounter = 0; + while ("".equals(br.readLine())) { + // For each blank line read, add a new order + nos.clOrdID(Long.toString(orderCounter++)); + + // Send the new order + in.newOrderSingle(nos); + in2.newOrderSingle(nos); + } + } + // Inform the user that the program has finished running + System.out.println("Finished"); + + // Exit the program + System.exit(0); + } + + /** + * Returns the current system time in nanoseconds. + * + * @return the current time in nanoseconds + */ + static long now() { + return SystemTimeProvider.INSTANCE.currentTimeNanos(); + } + + /** + * Converts a string to a long using base85 encoding. + * + * @param s the string to convert + * @return the long representation of the string + */ + static long fromShortText(String s) { + return ShortText.INSTANCE.parse(s); + } +} +.... + +Lines 90, Blanks 15, Tokens 822 + +== File: src\main\java\town\lost\oms\OrderViewerMain.java +.... +/* + */ + +package town.lost.oms; + +import net.openhft.chronicle.bytes.MethodReader; +import net.openhft.chronicle.core.Jvm; +import net.openhft.chronicle.core.util.Mocker; +import net.openhft.chronicle.queue.ChronicleQueue; +import net.openhft.chronicle.queue.impl.single.SingleChronicleQueueBuilder; +import net.openhft.chronicle.queue.rollcycles.TestRollCycles; +import town.lost.oms.api.OMSIn; + +/** + * The {@code OrderViewerMain} class is a utility application that continuously reads and displays + * orders from a Chronicle Queue. It acts as a simple viewer for incoming orders, logging them to + * the console. + */ +public class OrderViewerMain { + + /** + * The entry point of the application. + * + * @param args the input arguments (none expected) + */ + public static void main(String[] args) { + // Inform the user that the program is waiting for messages + System.out.println("\nWaiting for messages..."); + + // Establish connection with the queue + try (ChronicleQueue q = SingleChronicleQueueBuilder.binary("in") + .rollCycle(TestRollCycles.TEST8_DAILY) + .build()) { + + // Create a logging mock for OMSIn + OMSIn logging = Mocker.logging(OMSIn.class, "read - ", System.out); + + // Create a MethodReader from the tail of the queue + MethodReader reader = q.createTailer().methodReader(logging); + + // Continuously read messages from the queue + while (true) { + // Read one message from the queue; pause if no message was read + if (!reader.readOne()) { + Jvm.pause(50); + } + } + } + } +} +.... + +Lines 42, Blanks 8, Tokens 378 + +== File: src\test\java\town\lost\oms\OMSImplTest.java +.... +/* + */ + +package town.lost.oms; + +import net.openhft.chronicle.core.time.SetTimeProvider; +import net.openhft.chronicle.core.time.SystemTimeProvider; +import org.junit.After; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import town.lost.oms.api.OMSOut; + +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.assertEquals; + +/** + * Test class for OMSImpl. + * The OMSImplTest runs tests for each method in OMSImpl class. + * The test data is read from specified files and the actual output is compared against expected output. + */ +@SuppressWarnings("deprecation") +@RunWith(Parameterized.class) +public class OMSImplTest { + // Defines the paths to the tests to run. + static final List paths = Arrays.asList(new String[]{ + "newOrderSingle", + "newOrderSingleEquity", + "cancelOrderRequest", + "cancelAll" + }); + + // The name of the test, and the tester that will run the test. + final String name; + final net.openhft.chronicle.wire.utils.YamlTester tester; + + // Constructor that sets the name and tester. + public OMSImplTest(String name, net.openhft.chronicle.wire.utils.YamlTester tester) { + this.name = name; + this.tester = tester; + } + + // Defines the parameters for the parameterized test runner. + @Parameterized.Parameters(name = "{0}") + public static List parameters() { + // Returns a list of test parameters to run the tests with. + // Each test will be run with an instance of AccountManagerImpl, + // and will be subjected to various agitations to ensure robustness. + return new net.openhft.chronicle.wire.utils.YamlTesterParametersBuilder<>(out -> new OMSImpl(out), OMSOut.class, paths) + .agitators( + net.openhft.chronicle.wire.utils.YamlAgitator.messageMissing(), + net.openhft.chronicle.wire.utils.YamlAgitator.duplicateMessage(), + net.openhft.chronicle.wire.utils.YamlAgitator.overrideFields("sendingTime: '', symbol: '', side: '', orderQty: NaN, orderQty: -1, price: NaN, price: -1, clOrdId: '', ordType: ''".split(", *")), + net.openhft.chronicle.wire.utils.YamlAgitator.missingFields("sender, target, sendingTime, symbol, transactTime, account, orderQty, price, side, clOrdID, ordType, timeInForce, currency".split(", *"))) + .exceptionHandlerFunction(out -> (log, msg, thrown) -> out.jvmError(thrown == null ? msg : (msg + " " + thrown))) + .exceptionHandlerFunctionAndLog(true) + .get(); + } + + // After each test, this method resets the system time provider. + @After + public void tearDown() { + SystemTimeProvider.CLOCK = SystemTimeProvider.INSTANCE; + } + + // This is the actual test method, which uses the provided tester + // to run the test and then compares the expected output to the actual output. + @Test + public void runTester() { + // Sets the system clock to a specific time for the purpose of testing. + SystemTimeProvider.CLOCK = new SetTimeProvider("2019-12-03T09:54:37.345679") + .autoIncrement(1, TimeUnit.SECONDS); + // Asserts that the expected output matches the actual output. + assertEquals(tester.expected(), tester.actual()); + } +} +.... + +Lines 69, Blanks 10, Tokens 791 + +== File: src\test\resources\cancelAll\in.yaml +.... +--- +# This is a test for the CancelOrderRequest operation. +# The CLIENT1 (sender) is sending a request to the Order Management System (OMS1, the target) +# to cancel an order that was previously placed. The order is identified by its unique ID (clOrdID). + +cancelOrderRequest: { + sender: CLIENT1, + target: OMS1, + sendingTime: 2019-12-03T09:54:37.134475, + symbol: EURUSD, + account: ACC-12345, + clOrdID: dkj4378fwh, + origClOrdID: ABC1234567, + side: BUY +} +... +--- +# This is a test for the CancelAll operation. +# The CLIENT1 (sender) sends a request to the OMS1 (target) to cancel all orders. +cancelAll: { + sender: CLIENT1, + target: OMS1, + sendingTime: 2019-12-03T09:54:37.134475, + symbol: EURUSD, + clOrdID: CANCEL_ALL, +} +... +.... + +Lines 26, Blanks 1, Tokens 229 + +== File: src\test\resources\cancelAll\out.yaml +.... +# This is a test for the CancelOrderRequest operation. +# The CLIENT1 (sender) is sending a request to the Order Management System (OMS1, the target) +# to cancel an order that was previously placed. The order is identified by its unique ID (clOrdID). +--- +orderCancelReject: { + sender: OMS1, + target: CLIENT1, + sendingTime: 2019-12-03T09:54:37.134475, + symbol: EURUSD, + clOrdID: dkj4378fwh, + reason: No such order +} +... +# This is a test for the CancelAll operation. +# The CLIENT1 (sender) sends a request to the OMS1 (target) to cancel all orders. +--- +orderCancelReject: { + sender: OMS1, + target: CLIENT1, + sendingTime: 2019-12-03T09:54:37.134475, + symbol: EURUSD, + clOrdID: CANCEL_ALL, + reason: No orders to cancel +} +... +.... + +Lines 25, Blanks 0, Tokens 220 + +== File: src\test\resources\cancelOrderRequest\in.yaml +.... +--- +# This is a test for the CancelOrderRequest operation. +# The CLIENT1 (sender) is sending a request to the Order Management System (OMS1, the target) +# to cancel an order that was previously placed. The order is identified by its unique ID (clOrdID). + +cancelOrderRequest: { + sender: CLIENT1, + target: OMS1, + sendingTime: 2019-12-03T09:54:37.134475, + symbol: EURUSD, + account: ACC-12345, + clOrdID: dkj4378fwh, + origClOrdID: ABC1234567, + side: BUY +} +... +.... + +Lines 15, Blanks 1, Tokens 142 + +== File: src\test\resources\cancelOrderRequest\out.yaml +.... +# This is a test for the CancelOrderRequest operation. +# The CLIENT1 (sender) is sending a request to the Order Management System (OMS1, the target) +# to cancel an order that was previously placed. The order is identified by its unique ID (clOrdID). +--- +orderCancelReject: { + sender: OMS1, + target: CLIENT1, + sendingTime: 2019-12-03T09:54:37.134475, + symbol: EURUSD, + clOrdID: dkj4378fwh, + reason: No such order +} +... +.... + +Lines 13, Blanks 0, Tokens 124 + +== File: src\test\resources\newOrderSingle\in.yaml +.... +--- +# This is a test for the NewOrderSingle operation. +# The client (sender) is sending a new order to the Order Management System (OMS, the target). +# The order details such as the trading symbol, order quantity, price, and the order type are specified. + +newOrderSingle: { + sender: CLIENT, + target: OMS, + sendingTime: 2019-12-03T09:54:37.134475, + symbol: EURUSD, + transactTime: 2019-12-03T09:54:37.344751, + account: ACC-12345, + orderQty: 10E6, + price: 1.2123, + side: BUY, + clOrdID: dkj4378fwh, + ordType: LIMIT, + timeInForce: GTC, + currency: USD +} +... + +.... + +Lines 20, Blanks 2, Tokens 186 + +== File: src\test\resources\newOrderSingle\out.yaml +.... +# This is a test for the NewOrderSingle operation. +# The client (sender) is sending a new order to the Order Management System (OMS, the target). +# The order details such as the trading symbol, order quantity, price, and the order type are specified. +--- +executionReport: { + sender: OMS, + target: CLIENT, + sendingTime: 2019-12-03T09:54:37.134475, + symbol: EURUSD, + transactTime: 2019-12-03T09:54:37.344751, + orderQty: 10E6, + price: 1.2123, + orderID: 2019-12-03T09:54:37.345679, + lastPx: 0.0, + leavesQty: 0.0, + cumQty: 0.0, + avgPx: 0.0, + side: BUY, + ordType: LIMIT, + clOrdID: dkj4378fwh, + text: Not ready +} +... +.... + +Lines 23, Blanks 0, Tokens 225 + +== File: src\test\resources\newOrderSingleEquity\in.yaml +.... +--- +# Sample `NewOrderSingle` message representing a client's new order sent to the Order Management System (OMS). +newOrderSingle: { + sender: CLIENT1, + target: OMS1, + sendingTime: 2023-10-20T14:25:37.134475Z, + symbol: AAPL, + transactTime: 2023-10-20T14:25:37.134475Z, + account: ACC-12345, + orderQty: 1000, + price: 150.25, + side: buy, + clOrdID: ABC1234567, + ordType: LIMIT, + timeInForce: DAY, + currency: USD +} +... +--- +# Sample `NewOrderSingle` message representing another client's new order sent to the Order Management System (OMS). +newOrderSingle: { + sender: CLIENT2, + target: OMS1, + sendingTime: 2023-10-20T14:27:15.987654Z, + symbol: GOOGL, + transactTime: 2023-10-20T14:27:15.987654Z, + account: ACC-67890, + orderQty: 500, + price: 2750.50, + side: sell, + clOrdID: XYZ7654321, + ordType: MARKET, + timeInForce: DAY, + currency: USD +} +... +.... + +Lines 36, Blanks 0, Tokens 299 + +== File: src\test\resources\newOrderSingleEquity\out.yaml +.... +# Sample `NewOrderSingle` message representing a client's new order sent to the Order Management System (OMS). +--- +executionReport: { + sender: OMS1, + target: CLIENT1, + sendingTime: 2023-10-20T14:25:37.134475, + symbol: AAPL, + transactTime: 2023-10-20T14:25:37.134475, + orderQty: 1E3, + price: 150.25, + orderID: 2019-12-03T09:54:37.345679, + lastPx: 0.0, + leavesQty: 0.0, + cumQty: 0.0, + avgPx: 0.0, + side: BUY, + ordType: LIMIT, + clOrdID: ABC1234567, + text: Not ready +} +... +# Sample `NewOrderSingle` message representing another client's new order sent to the Order Management System (OMS). +--- +executionReport: { + sender: OMS1, + target: CLIENT2, + sendingTime: 2023-10-20T14:27:15.987654, + symbol: GOOGL, + transactTime: 2023-10-20T14:27:15.987654, + orderQty: 500.0, + price: 2750.5, + orderID: 2019-12-03T09:54:38.345679, + lastPx: 0.0, + leavesQty: 0.0, + cumQty: 0.0, + avgPx: 0.0, + side: SELL, + ordType: MARKET, + clOrdID: XYZ7654321, + text: Not ready +} +... +.... + +Lines 42, Blanks 0, Tokens 382 + +== Summary == +Total Lines: 2833 +Total Blanks: 298 +Total Tokens: 25169 +Tokens/Line: 8.9 diff --git a/order-processor/src/main/adoc/oms-api-requirements.adoc b/order-processor/src/main/adoc/oms-api-requirements.adoc new file mode 100644 index 0000000..a2b903c --- /dev/null +++ b/order-processor/src/main/adoc/oms-api-requirements.adoc @@ -0,0 +1,484 @@ += Detailed Functional Requirements for `lost.town.oms.api` and `lost.town.oms.dto` +:toc: +:toclevels: 3 + +This requirements document provides **sufficient detail to recreate** both the `lost.town.oms.api` and `lost.town.oms.dto` packages. It specifies the interfaces, classes, fields, validation logic, threading assumptions, and testing approach. + +== Project Context + +In this system, an **Order Management System (OMS)** receives inbound events (orders, cancellations) and emits outbound events (execution reports, reject messages). This design uses **Chronicle Queue** for persistence and asynchronous event-driven processing, with **Chronicle Wire** for serialization. + +== Assumptions + +1. **Single-Threaded**: This demo runs in a single-threaded context (e.g., one thread polling events from the queue). +2. **Validation**: The *framework* calls each DTO’s `validate()` method before passing it to the `OMSIn` (for inbound) or writing via `OMSOut` (for outbound). +3. **Error Handling**: Any thrown exception is caught by the framework, which then calls `OMSOut.jvmError(...)`. +4. **Chronicle Wire**: The data transfer objects (DTOs) are compatible with Chronicle Wire for efficient marshalling/unmarshalling. In production, you would use **Chronicle Services** for replication and failover. + +== `lost.town.oms.api` Package + +The `lost.town.oms.api` package declares the contracts (interfaces) for interacting with the OMS. It includes: + +* `ErrorListener` +* `OMSIn` +* `OMSOut` +* `package-info.java` (provides overview documentation) + +=== `ErrorListener` + +[source,java] +---- +package lost.town.oms.api; + +public interface ErrorListener { + /** + * Called when any exception or critical error occurs. + * + * @param msg A descriptive message about the JVM error or exception + */ + void jvmError(String msg); +} +---- + +==== Requirements + +1. **Single Method**: `jvmError(String msg)` must be provided to handle system-level errors, exceptions, or critical failures. +2. **Invocation**: The framework (or other system component) calls `jvmError(msg)` whenever an exception bubbles up unhandled. +3. **Implementors**: Typically, classes implementing `OMSOut` also implement `ErrorListener` so they can publish or log these errors if desired. + +=== `OMSIn` + +[source,java] +---- +package lost.town.oms.api; + +import town.lost.oms.dto.CancelAll; +import town.lost.oms.dto.CancelOrderRequest; +import town.lost.oms.dto.NewOrderSingle; + +public interface OMSIn { + /** + * Handles a new order request. + * @param nos the DTO with new order details + */ + void newOrderSingle(NewOrderSingle nos); + + /** + * Handles a cancellation request for a specific order. + * @param cor the DTO with cancellation details + */ + void cancelOrderRequest(CancelOrderRequest cor); + + /** + * Handles a request to cancel all orders for a given symbol or filter. + * @param cancelAll the DTO with cancel-all details + */ + void cancelAll(CancelAll cancelAll); +} +---- + +==== Requirements + +1. **Inbound Contract**: `OMSIn` defines methods for events flowing into the OMS. +2. **Methods**: + - `newOrderSingle(NewOrderSingle nos)` – for creating a new order. + - `cancelOrderRequest(CancelOrderRequest cor)` – for canceling a single, known order. + - `cancelAll(CancelAll cancelAll)` – for canceling all orders matching a symbol or filter. +3. **Validation**: By the time the DTO is passed in, it has already been validated by the framework. +4. **Exception Handling**: Any runtime exception inside these methods is caught by the framework, which then calls `jvmError(...)`. +5. **Implementation**: A typical implementation (e.g., `OMSImpl`) will transform inbound DTOs into outbound messages (`ExecutionReport`, `OrderCancelReject`) via `OMSOut`. + +=== `OMSOut` + +[source,java] +---- +package lost.town.oms.api; + +import town.lost.oms.dto.ExecutionReport; +import town.lost.oms.dto.OrderCancelReject; + +public interface OMSOut extends ErrorListener { + /** + * Called to publish an execution report. + * @param er the DTO containing execution details + */ + void executionReport(ExecutionReport er); + + /** + * Called to publish an order-cancel-reject event. + * @param ocr the DTO containing rejection details + */ + void orderCancelReject(OrderCancelReject ocr); +} +---- + +==== Requirements + +1. **Outbound Contract**: `OMSOut` defines methods for events the OMS emits after processing. +2. **Methods**: + - `executionReport(ExecutionReport er)` – used to acknowledge or confirm orders. + - `orderCancelReject(OrderCancelReject ocr)` – used to reject cancellations or indicate an error in canceling. +3. **ErrorListener**: Inherits `jvmError(String msg)` from `ErrorListener` for system-level exceptions. +4. **Validation**: The framework must validate `er` or `ocr` before they are actually written to the Chronicle Queue. +5. **Implementation**: Typically, this interface is realized by a Chronicle Queue `MethodWriter`, though it can be implemented in other ways if needed. + +=== `package-info.java` + +[source,java] +---- +/** + * This package defines the inbound (OMSIn) and outbound (OMSOut) interfaces + * for an Order Management System, plus an ErrorListener interface for + * handling exceptions at a system level. + * + *

The framework: + *

    + *
  • Validates all DTOs before passing to OMSIn or writing via OMSOut.
  • + *
  • Catches exceptions, calling jvmError() on the relevant ErrorListener implementation.
  • + *
  • Operates in a single-threaded environment for this demo.
  • + *
+ * + *

See also the lost.town.oms.dto package for the message objects used. + */ +package lost.town.oms.api; +---- + +==== Requirements + +1. **Overview**: Must describe the role of each interface in the broader system. +2. **Simplicity**: The package-level docs must clarify that this is a *single-threaded* demonstration and that the framework is responsible for major tasks like validation and error interception. + +== `lost.town.oms.dto` Package + +This package provides **Data Transfer Objects** (DTOs) representing orders and associated messages. + +=== Contents + +* `AbstractEvent` (base class) +* `CancelAll` +* `CancelOrderRequest` +* `Ccy` +* `ExecutionReport` +* `NewOrderSingle` +* `OrderCancelReject` +* `OrderType` +* `Side` +* `TimeInForce` +* `ValidateUtil` +* `package-info.java` + +=== Common Requirements + +1. **Compatibility**: All classes must be **Chronicle Wire**-friendly (e.g., implement or extend classes like `SelfDescribingMarshallable`, use field-level annotations for marshalling if needed). +2. **Validation**: Each class provides a `validate()` method that throws `InvalidMarshallableException` if required fields are missing or invalid. The framework calls this method prior to usage. +3. **Timestamp Fields**: Typically stored in nanoseconds (e.g., `sendingTime`, `transactTime`). +4. **Symbols and IDs**: Often stored as `long` converted via short-text mechanisms (e.g., `ShortTextLongConverter`). +5. **Extended Fields**: Classes can be extended with extra fields if needed, but existing fields must remain backward compatible. + +=== `AbstractEvent` + +[source,java] +---- +package lost.town.oms.dto; + +import net.openhft.chronicle.core.io.InvalidMarshallableException; +import net.openhft.chronicle.wire.SelfDescribingMarshallable; +import net.openhft.chronicle.core.io.Validatable; + +public class AbstractEvent> + extends SelfDescribingMarshallable + implements Validatable { + + protected long sender; + protected long target; + protected long sendingTime; + + @Override + public void validate() throws InvalidMarshallableException { + if (sender == 0) throw new InvalidMarshallableException("sender is required"); + if (target == 0) throw new InvalidMarshallableException("target is required"); + if (sendingTime == 0) throw new InvalidMarshallableException("sendingTime is required"); + } + // ... Setters, getters, etc. +} +---- + +==== Requirements + +1. **Generic Parameter**: `>` used for fluent-style method chaining in subclasses. +2. **Shared Fields**: + - `sender`: long ID of who initiated the event + - `target`: long ID of recipient or destination + - `sendingTime`: nanosecond timestamp +3. **`validate()`**: Must check these fields are non-zero. +4. **Usage**: Subclassed by all other event objects. + +=== `NewOrderSingle` + +[source,java] +---- +package lost.town.oms.dto; + +public class NewOrderSingle extends AbstractEvent { + private long symbol; + private long transactTime; + private long account; + private double orderQty; + private double price; + private Side side; + private String clOrdID = ""; + private OrderType ordType; + private TimeInForce timeInForce; + private Ccy currency; + + @Override + public void validate() throws InvalidMarshallableException { + super.validate(); + if (symbol == 0) throw new InvalidMarshallableException("symbol is required"); + if (transactTime == 0) throw new InvalidMarshallableException("transactTime is required"); + if (account == 0) throw new InvalidMarshallableException("account is required"); + if (orderQty <= 0) throw new InvalidMarshallableException("orderQty must be > 0"); + if (!Double.isFinite(price) || price <= 0) throw new InvalidMarshallableException("invalid price"); + if (side == null) throw new InvalidMarshallableException("side is required"); + if (ordType == null) throw new InvalidMarshallableException("ordType is required"); + if (clOrdID == null || clOrdID.isEmpty()) throw new InvalidMarshallableException("clOrdID is required"); + if (timeInForce == null) throw new InvalidMarshallableException("timeInForce is required"); + if (currency == null) throw new InvalidMarshallableException("currency is required"); + } + // ... Setters, getters, etc. +} +---- + +==== Requirements + +1. **Represents** an inbound order message. +2. **Required Fields**: + - `symbol`, `transactTime`, `account`, `orderQty`, `price`, `side`, `clOrdID`, `ordType`, `timeInForce`, `currency`. +3. **Validation**: Must confirm each field is non-null, non-zero, and appropriately ranged. + +=== `CancelOrderRequest` + +[source,java] +---- +public class CancelOrderRequest extends AbstractEvent { + private long symbol; + private long account; + private String clOrdID = ""; + private String origClOrdID = ""; + private Side side; + // ... + @Override + public void validate() throws InvalidMarshallableException { + super.validate(); + if (symbol == 0) throw new InvalidMarshallableException("symbol is required"); + if (account == 0) throw new InvalidMarshallableException("account is required"); + if (clOrdID == null || clOrdID.isEmpty()) throw new InvalidMarshallableException("clOrdID required"); + if (origClOrdID == null || origClOrdID.isEmpty()) throw new InvalidMarshallableException("origClOrdID required"); + if (side == null) throw new InvalidMarshallableException("side required"); + } +} +---- + +==== Requirements + +1. **Represents** a request to cancel an existing order. +2. **Key Fields**: `symbol`, `account`, `clOrdID` (the new cancel request ID), `origClOrdID` (the order to cancel), and `side`. + +=== `CancelAll` + +[source,java] +---- +public class CancelAll extends AbstractEvent { + private long symbol; + private String clOrdID = ""; + + @Override + public void validate() throws InvalidMarshallableException { + super.validate(); + if (symbol == 0) throw new InvalidMarshallableException("symbol required"); + if (clOrdID == null || clOrdID.isEmpty()) throw new InvalidMarshallableException("clOrdID required"); + } +} +---- + +==== Requirements + +1. **Represents** a mass-cancel request for a `symbol`. +2. **Return Type**: Notice the class is declared as `` for potential chaining in some frameworks (though not used in basic code). +3. **Validation**: Must ensure `symbol != 0` and `clOrdID` is non-empty. + +=== `ExecutionReport` + +[source,java] +---- +public class ExecutionReport extends AbstractEvent { + private long symbol; + private long transactTime; + private double orderQty; + private double price; + private long orderID; + private double lastPx; + private double leavesQty; + private double cumQty; + private double avgPx; + private Side side; + private OrderType ordType; + private String clOrdID = ""; + private String text; + // ... + @Override + public void validate() throws InvalidMarshallableException { + super.validate(); + if (symbol == 0) throw new InvalidMarshallableException("symbol required"); + if (transactTime == 0) throw new InvalidMarshallableException("transactTime required"); + if (orderQty <= 0) throw new InvalidMarshallableException("orderQty must be > 0"); + if (price <= 0) throw new InvalidMarshallableException("price must be > 0"); + if (orderID == 0) throw new InvalidMarshallableException("orderID required"); + if (side == null) throw new InvalidMarshallableException("side required"); + if (ordType == null) throw new InvalidMarshallableException("ordType required"); + if (clOrdID == null || clOrdID.isEmpty()) throw new InvalidMarshallableException("clOrdID required"); + // lastPx, leavesQty, cumQty, avgPx can be zero or positive + } +} +---- + +==== Requirements + +1. **Outbound** message describing an order’s status (e.g., partial fill, full fill, acceptance). +2. **Key Fields**: `orderID` assigned by the OMS, plus standard fields like `symbol`, `side`, `ordType`. +3. **Optional Fields**: `lastPx`, `leavesQty`, `cumQty`, `text` can be set to reflect current execution status. Zero or missing is allowed for partial data. + +=== `OrderCancelReject` + +[source,java] +---- +public class OrderCancelReject extends AbstractEvent { + private long symbol; + private String clOrdID = ""; + private String reason = ""; + // ... + @Override + public void validate() throws InvalidMarshallableException { + super.validate(); + if (symbol == 0) throw new InvalidMarshallableException("symbol required"); + if (clOrdID == null || clOrdID.isEmpty()) throw new InvalidMarshallableException("clOrdID required"); + if (reason == null || reason.isEmpty()) throw new InvalidMarshallableException("reason required"); + } +} +---- + +==== Requirements + +1. **Represents** a rejected cancel request (or some other cancel-related error). +2. **Key Fields**: `symbol`, `clOrdID` (the ID of the request that’s being rejected), and a human-readable `reason`. + +=== Supporting Enumerations + +. `Side` +:: + [source,java] + ---- + public enum Side { + BUY(+1), SELL(-1); + public final int direction; + Side(int direction) { this.direction = direction; } + } + ---- + - Represents a **buy** or **sell** order. + +. `OrderType` +:: + [source,java] + ---- + public enum OrderType { + MARKET, LIMIT, PEGGED, FILL_OR_KILL, IMMEDIATE_OR_CANCEL + } + ---- + - Defines the **type** of the order (market, limit, etc.). + +. `TimeInForce` +:: + [source,java] + ---- + public enum TimeInForce { + DAY, GTC, IOC, FOK, GTD, GTT, OPG, ATC, GFA, GIS, GTX, SIO + } + ---- + - Controls **how long** an order remains active or under what conditions it is executed/canceled. + +. `Ccy` +:: + [source,java] + ---- + public enum Ccy { + USD(...), EUR(...), GBP(...), ... + // Each enum holds currency name, country, symbol, etc. + } + ---- + - Enumerates currencies by ISO 4217 code, with metadata. + +=== `ValidateUtil` + +[source,java] +---- +public final class ValidateUtil { + private ValidateUtil() {} + + public static boolean invalidPrice(double price) { + return !Double.isFinite(price) || price <= 0; + } + + public static boolean invalidQuantity(double quantity) { + return !Double.isFinite(quantity) || quantity < 0; + } +} +---- + +==== Requirements + +1. **Static Helpers**: Provides commonly used checks (`invalidPrice`, `invalidQuantity`). +2. **Integration**: Classes like `NewOrderSingle` might call these helpers in `validate()`. +3. **No Instantiation**: The class is `final` with a private constructor. + +=== `package-info.java` + +[source,java] +---- +/** + * The DTOs for the OMS. Each class extends {@link AbstractEvent} or is an enum of possible values. + * + *

All DTOs require a call to {@code validate()} to ensure fields are properly set. + * The framework is responsible for calling validate() before using them. + * + *

Includes: + *

    + *
  • {@link NewOrderSingle}, {@link CancelOrderRequest}, {@link CancelAll} (inbound requests)
  • + *
  • {@link ExecutionReport}, {@link OrderCancelReject} (outbound events)
  • + *
  • {@link Side}, {@link OrderType}, {@link TimeInForce}, {@link Ccy} (enums)
  • + *
  • {@link ValidateUtil} (static validation helpers)
  • + *
+ */ +package lost.town.oms.dto; +---- + +==== Requirements + +1. **Documentation**: Summarize how each DTO or enum is used. +2. **Highlight** that the framework ensures validation. + +== Testing Approach + +1. **YAML-Based BDD**: The project uses directories containing `_setup.yaml`, `in.yaml`, and `out.yaml` to define test scenarios. +2. **Single Thread**: The test harness processes events in a single-thread loop, verifying that for each inbound event (`in.yaml`), the system produces the correct outbound event (`out.yaml`). +3. **Adding Tests**: Add a new directory with appropriate YAML files and reference it in `OMSImplTest` to run new scenarios. + +== Conclusion + +By following this detailed specification: + +* You can **reconstruct** all interfaces (`OMSIn`, `OMSOut`, `ErrorListener`) and **implement** the data models (`NewOrderSingle`, `CancelOrderRequest`, etc.). +* **Validate** each DTO to ensure correctness before passing to the OMS or publishing to the queue. +* Maintain a **single-threaded** model for simplicity, with the framework capturing exceptions via `jvmError(...)`. +* Leverage **YAML-based BDD tests** to verify end-to-end behavior. + +These packages form the **foundation** for an event-driven Order Management System using Chronicle technologies. diff --git a/order-processor/src/main/adoc/oms-requirements.adoc b/order-processor/src/main/adoc/oms-requirements.adoc new file mode 100644 index 0000000..b444fac --- /dev/null +++ b/order-processor/src/main/adoc/oms-requirements.adoc @@ -0,0 +1,181 @@ += OMSImpl Functional Requirements (Updated) +:toc: +:toclevels: 3 + +== Introduction + +The `OMSImpl` class is the core implementation of the `OMSIn` interface, responsible for handling inbound order-related messages and producing appropriate outbound responses. It serves as the "business logic" portion of the Order Management System (OMS), transforming inbound messages (`NewOrderSingle`, `CancelOrderRequest`, `CancelAll`) into outbound messages (`ExecutionReport`, `OrderCancelReject`). + +This component is designed to operate within an **event-driven framework**. Each inbound event is read from a Chronicle Queue, and each outbound event is written to another Chronicle Queue. This approach provides *traceability*, as all inbound and outbound events are persisted. + +== Purpose and Scope + +The primary purposes of `OMSImpl` are: + +1. **Receive and Validate** incoming messages (via the framework, which calls `validate()` on DTOs prior to invocation). +2. **Process** inbound messages (e.g., new orders, cancellation requests). +3. **Generate** outbound messages (e.g., execution reports, cancellation rejects). +4. **Publish** outbound events to a Chronicle Queue, after validation via `validate()`. + +The scope of `OMSImpl` includes creating new orders, rejecting cancellations, and responding to mass-cancel requests with a single rejection message in this simple example. State management (e.g., actual order book) is not within the scope of this demonstration. + +== Event-Driven Interaction Model + +- **Input Events**: Arrive through a Chronicle Queue and are read by a matching method on `OMSImpl` (e.g., `newOrderSingle(...)`). +- **Output Events**: Published to another Chronicle Queue, which downstream consumers or loggers can subscribe to. +- **Traceability**: Because both inbound and outbound events are fully persisted, the system can be audited or replayed. + +== Functional Overview + +`OMSImpl` implements the following methods from `OMSIn`: + +1. `newOrderSingle(NewOrderSingle nos)` +2. `cancelOrderRequest(CancelOrderRequest cor)` +3. `cancelAll(CancelAll cancelAll)` + +=== Validation + +All inbound DTOs are validated by the framework before calling `OMSImpl`. +Similarly, `OMSImpl` constructs outbound DTOs (e.g., `ExecutionReport`, `OrderCancelReject`), and these are also validated before being written to the Chronicle Queue. Any validation failure triggers a path where `jvmError` is called or a rejection message is published. + +=== newOrderSingle + +* **Input**: `NewOrderSingle nos` +- Contains fields such as `symbol`, `orderQty`, `price`, `side`, `clOrdID`, `ordType`, etc. +- Includes metadata (e.g., `sender()`, `target()`, `transactTime()`, `sendingTime()`). + +* **Process**: +- A unique `orderID` is generated (in the reference implementation, a nanosecond timestamp). +- An `ExecutionReport` DTO is populated with the relevant data and sent out. +- If the inbound `NewOrderSingle` fails validation (missing fields, invalid values), the system *must* create and send an `OrderCancelReject` instead of an `ExecutionReport`. + +* **Output**: `ExecutionReport` (or `OrderCancelReject` if invalid) + +=== cancelOrderRequest + +* **Input**: `CancelOrderRequest cor` +- Contains fields such as `symbol`, `account`, `origClOrdID`, `clOrdID`, `side`, etc. + +* **Process**: +- In this simple reference implementation, no existing orders are tracked, so the request is rejected with `OrderCancelReject` containing the reason `"No such order"`. + +* **Output**: `OrderCancelReject` + +=== cancelAll + +* **Input**: `CancelAll cancelAll` +- Contains fields such as `symbol` and `clOrdID`, representing which orders to cancel. + +* **Process**: +- Similar to `cancelOrderRequest`, no orders are tracked. The system publishes an `OrderCancelReject` with `"No orders to cancel"`. + +* **Output**: `OrderCancelReject` + +== Error Handling + +When an **exception is thrown** during processing (e.g., a validation or runtime error), the event-driven framework captures the exception and calls `OMSOut.jvmError(msg)`. This ensures system-level errors are tracked in the same queue-based workflow. + +== Performance Requirements + +- For this simple example, the component **must** be capable of consistently handling **100k messages/second**. +- **99% latency** should remain **low jitter** or better under these throughput conditions, indicating that the system processes most messages quickly and consistently. + +== Class Responsibilities + +1. **Order Creation**: Creates and populates an `ExecutionReport` from a `NewOrderSingle`. +2. **Order Rejection**: Returns an `OrderCancelReject` for both single-order and mass-cancel requests, or if validation fails. +3. **DTO Mapping**: Maps inbound DTO fields to outbound DTO fields (`sender ↔ target`, `symbol`, `clOrdID`, etc.). +4. **Time/ID Generation**: Uses `SystemTimeProvider.CLOCK.currentTimeNanos()` to assign `orderID`. +5. **Validation Flow**: Relies on the framework to call `validate()` on input and output DTOs. If invalid, the framework or `OMSImpl` triggers appropriate rejection/error handling. + +== Sample Implementation + +[source,java] +---- +public class OMSImpl implements OMSIn { + private final OMSOut out; + private final ExecutionReport er = new ExecutionReport(); + private final OrderCancelReject ocr = new OrderCancelReject(); + + public OMSImpl(OMSOut out) { + this.out = out; + } + + @Override + public void newOrderSingle(NewOrderSingle nos) { + // If validated already, proceed; otherwise framework calls jvmError() or triggers rejection + er.reset(); + long orderID = SystemTimeProvider.CLOCK.currentTimeNanos(); + + er.sender(nos.target()) + .target(nos.sender()) + .symbol(nos.symbol()) + .clOrdID(nos.clOrdID()) + .ordType(nos.ordType()) + .orderQty(nos.orderQty()) + .price(nos.price()) + .side(nos.side()) + .sendingTime(nos.sendingTime()) + .transactTime(nos.transactTime()) + .leavesQty(0) + .cumQty(0) + .avgPx(0) + .orderID(orderID) + .text("Not ready"); + + out.executionReport(er); + } + + @Override + public void cancelOrderRequest(CancelOrderRequest cor) { + ocr.reset(); + ocr.sender(cor.target()) + .target(cor.sender()) + .symbol(cor.symbol()) + .clOrdID(cor.clOrdID()) + .sendingTime(cor.sendingTime()) + .reason("No such order"); + + out.orderCancelReject(ocr); + } + + @Override + public void cancelAll(CancelAll cancelAll) { + ocr.reset(); + ocr.sender(cancelAll.target()) + .target(cancelAll.sender()) + .symbol(cancelAll.symbol()) + .clOrdID(cancelAll.clOrdID()) + .sendingTime(cancelAll.sendingTime()) + .reason("No orders to cancel"); + + out.orderCancelReject(ocr); + } +} +---- + +== Execution and Testing + +To compile and run a demonstration that uses `OMSImpl` (and other classes in this system), follow these steps: + +1. **Clone the source code** for the Order Processor Demo (the project containing `pom.xml` and these source files). +2. **Build the project**: ++ +---- +mvn clean install +---- +3. **Run the relevant main classes**: ++ +- `OrderAdderMain` to send `NewOrderSingle` messages. +- `OrderViewerMain` to observe messages in the queue. +- `OMSBenchmarkMain` to measure latency and throughput. + +4. **Observe the results**: +- Execution Reports and Order Cancel Rejects appear in the output Chronicle Queue and/or console. + +5. **Error Handling**: +- Any exceptions thrown are collected and published by calling `jvmError(msg)` on `OMSOut`. + +== Conclusion + +The `OMSImpl` class is a simple yet illustrative component in an event-driven OMS framework. It showcases how to ingest inbound FIX-like messages from a Chronicle Queue, validate them, process the logic (create an `ExecutionReport` or `OrderCancelReject`), and publish results back to a Chronicle Queue. It must handle 100k msgs/s with low jitter at or better than the 99% latency target. This design ensures robust traceability, minimal coupling, and a clean separation of inbound versus outbound workflows. From 3847538e7120f30795c9b4cde97896789ecb2583 Mon Sep 17 00:00:00 2001 From: Peter Lawrey Date: Wed, 1 Jan 2025 08:34:08 +0000 Subject: [PATCH 11/31] don't include context.asciidoc --- order-processor/context.asciidoc | 3199 ------------------------------ 1 file changed, 3199 deletions(-) delete mode 100644 order-processor/context.asciidoc diff --git a/order-processor/context.asciidoc b/order-processor/context.asciidoc deleted file mode 100644 index aedf202..0000000 --- a/order-processor/context.asciidoc +++ /dev/null @@ -1,3199 +0,0 @@ -= Directory Content - -== File: pom.xml -.... - - - 4.0.0 - - - run.chronicle - chronicle-queue-demo - 1.0-SNAPSHOT - ../pom.xml - - - order-processor - OpenHFT/${project.parent.artifactId}/${project.artifactId} - - - - net.openhft - jlbh - - - - - - - org.codehaus.mojo - exec-maven-plugin - - - bench - - exec - - - ${java.home}/bin/java - ${jvm.requiredArgs} -classpath %classpath town.lost.oms.OMSBenchmarkMain - - - - adder - - exec - - - ${java.home}/bin/java - ${jvm.requiredArgs} -classpath %classpath town.lost.oms.OrderAdderMain - - - - viewer - - exec - - - ${java.home}/bin/java - ${jvm.requiredArgs} -classpath %classpath town.lost.oms.OrderViewerMain - - - - dump - - exec - - - ${java.home}/bin/java - ${jvm.requiredArgs} -classpath %classpath net.openhft.chronicle.queue.DumpQueueMain in - - - - - - - -.... - -Lines 70, Blanks 4, Tokens 645 - -== File: README.adoc -.... -= Order Processor Demo -Peter Lawrey - -The Order Processor Demo showcases the implementation of services using messages based on the Financial Information Exchange (FIX) protocol. This project simulates an order processing system, including features such as creating new orders, cancelling specific orders, and cancelling all orders. The project is designed to mimic real-world financial trading systems, demonstrating how such a system can be built using Java. - -== Table of Contents - -* <> -* <> -* <> -* <> -* <> -* <> - -== Getting Started - -To get started with the Order Processor Demo, you need to clone the repository to your local machine. Once you have cloned the repository, you can open it in your IDE of choice (we recommend IntelliJ CE for this project). - -== Prerequisites - -Before you begin, ensure you have the following installed: - -- **Java Development Kit (JDK)**: Java 8 Update 202+, Java 11, Java 17, or Java 21 -- **Apache Maven**: Version 3.6.5 or higher -- **Integrated Development Environment (IDE)**: IntelliJ IDEA Community Edition (recommended) or any other IDE -- **Internet Access**: Required for Maven to download necessary dependencies - -== Installation - -Here are the steps to clone the repository and open it in IntelliJ: - -1. Clone the repository with `git clone ` -2. Open IntelliJ IDE and select 'Open' -3. Navigate to the cloned repository and click 'Open' -4. Wait for the IDE to import the project and download the necessary dependencies - -== Running the Project - -[source,mermaid] -.... -sequenceDiagram -autonumber -Client->>+OMS: NewOrderSingle -Note right of Client: via Chronicle Queue, persisted -Note right of OMS: processes event -OMS->>-Client: ExecutionReport -Note right of Client: via Chronicle Queue, persisted -.... - -To run the project, you need to compile the project and run the main classes for different features: - -- `OrderAdderMain`: This class simulates the creation of new orders. -- `OrderViewerMain`: This class simulates the viewing of created orders. -- `OrderBenchmarkMain`: This class benchmarks round trip time of writing a `NewOrderSingle` a queue, have the `OMSImpl` produce an `ExecutionReport` and read that report. -- `OMSImpl`: This class handles the processing of the orders, including cancelling orders. - -You can run these classes directly from your IDE, or you can use `mvn compile exec:exec@id` on the command line to run the compiled classes. - -== Contributing - -We appreciate any contributions to the Order Processor Demo. If you find any bugs or issues, please open an issue in the repository. If you want to contribute to the code, please fork the repository, make your changes, and open a pull request. - -== Acknowledgments - -- Inspired by real-world financial trading systems. -- Uses the FIX protocol for message exchange. -.... - -Lines 46, Blanks 20, Tokens 663 - -== File: src\main\java\town\lost\oms\api\ErrorListener.java -.... -package town.lost.oms.api; - -/** - * This interface provides a method to handle JVM errors. - * Implementations of this interface will define how these errors are handled. - */ -public interface ErrorListener { - - /** - * This method handles JVM errors. - * It takes a String message which provides details about the error. - * - * @param msg a String providing details about the JVM error. - */ - void jvmError(String msg); -} -.... - -Lines 14, Blanks 2, Tokens 93 - -== File: src\main\java\town\lost\oms\api\OMSIn.java -.... -/* - */ - -package town.lost.oms.api; - -import net.openhft.chronicle.bytes.MethodId; -import town.lost.oms.dto.CancelAll; -import town.lost.oms.dto.CancelOrderRequest; -import town.lost.oms.dto.NewOrderSingle; - -/** - * The {@code OMSIn} interface defines the operations that can be performed by an Order Management System (OMS). - *

- * It includes methods to handle new single orders, cancel order requests, and cancel all orders. - * Each method receives an instance of a data transfer object that represents the details of the operation. - * - * @see NewOrderSingle - * @see CancelOrderRequest - * @see CancelAll - */ -public interface OMSIn { - - /** - * Handles a new single order submission. - * - * @param nos The {@link NewOrderSingle} object representing the details of the new order. - */ - @MethodId(1) - void newOrderSingle(NewOrderSingle nos); - - /** - * Handles a cancel order request. - * - * @param cor The {@link CancelOrderRequest} object representing the details of the cancel order request. - */ - @MethodId(2) - void cancelOrderRequest(CancelOrderRequest cor); - - /** - * Handles a cancel all orders request. - * - * @param cancelAll The {@link CancelAll} object representing the details of the cancel all orders request. - */ - void cancelAll(CancelAll cancelAll); -} -.... - -Lines 39, Blanks 6, Tokens 309 - -== File: src\main\java\town\lost\oms\api\OMSOut.java -.... -/* - */ - -package town.lost.oms.api; - -import net.openhft.chronicle.bytes.MethodId; -import town.lost.oms.dto.ExecutionReport; -import town.lost.oms.dto.OrderCancelReject; - -/** - * The {@code OMSOut} interface defines the output operations that an Order Management System (OMS) can perform. - *

- * It includes methods to handle execution reports and order cancel rejections. - *

It extends the {@link ErrorListener} interface to handle any JVM errors that may occur during processing. - * Each method receives an instance of a data transfer object that represents the details of the operation. - * - * @see ExecutionReport - * @see OrderCancelReject - * @see ErrorListener - */ -public interface OMSOut extends ErrorListener{ - - /** - * Handles an execution report. - * - * @param er The {@link ExecutionReport} object representing the details of the execution report. - */ - @MethodId(11) - void executionReport(ExecutionReport er); - - /** - * Handles an order cancel reject. - * - * @param ocr The {@link OrderCancelReject} object representing the details of the order cancel reject. - */ - @MethodId(12) - void orderCancelReject(OrderCancelReject ocr); -} -.... - -Lines 33, Blanks 5, Tokens 267 - -== File: src\main\java\town\lost\oms\api\package-info.java -.... -/** - * Provides the API interfaces for interactions with the Order Management System (OMS) in the 'town.lost.oms' project. - * - *

This package includes the following interfaces: - * - *

    - *
  • {@link town.lost.oms.api.OMSIn}, which defines the methods for inbound operations that an OMS can perform, such as handling new single orders, cancel order requests, and cancel all orders.
  • - *
  • {@link town.lost.oms.api.OMSOut}, which defines the methods for outbound operations from the OMS, including handling execution reports and order cancel rejections.
  • - *
- * - *

Each interface includes methods that correspond to specific actions within the OMS, facilitating communication between clients and the OMS. - * - *

For more details, refer to the documentation for each individual interface. - */ -package town.lost.oms.api; - -.... - -Lines 15, Blanks 1, Tokens 187 - -== File: src\main\java\town\lost\oms\dto\AbstractEvent.java -.... -/* - */ - -package town.lost.oms.dto; - -import net.openhft.chronicle.bytes.BytesIn; -import net.openhft.chronicle.bytes.BytesOut; -import net.openhft.chronicle.core.Jvm; -import net.openhft.chronicle.core.io.InvalidMarshallableException; -import net.openhft.chronicle.core.io.Validatable; -import net.openhft.chronicle.wire.*; -import net.openhft.chronicle.wire.converter.NanoTime; -import net.openhft.chronicle.wire.converter.ShortText; - -/** - * The {@code AbstractEvent} class represents a generic event in the system. - * - *

This class provides common fields such as sender, target, and sendingTime which can be extended - * by more specific events. Fields sender and target are encoded as {@link ShortText} for efficient storage and transmission. - * - *

The class is generic, with the type parameter being a subclass of {@code AbstractEvent}. This allows - * methods in this class to return an instance of the subclass, enabling method chaining in the subclass. - * - *

The encoding of the marshalled bytes can be controlled via system properties "bytesInBinary" and "pregeneratedMarshallable". - */ -public class AbstractEvent> extends SelfDescribingMarshallable implements Validatable { - // Used to control the benchmark - public static final boolean BYTES_IN_BINARY = Jvm.getBoolean("bytesInBinary", true); - - // Used to control the benchmark - public static final boolean PREGENERATED_MARSHALLABLE = Jvm.getBoolean("pregeneratedMarshallable", true); - private static final int MARSHALLABLE_VERSION = 1; - - // The sender of the event - @ShortText - private long sender; - - // The target of the event - @ShortText - private long target; - - // The sending time of the event, in nanoseconds. - @NanoTime - private long sendingTime; - - @Override - public boolean usesSelfDescribingMessage() { - return !BYTES_IN_BINARY; - } - - /** - * Get the sender of the event. - * - * @return The sender's value as a long. - */ - public long sender() { - return sender; - } - - /** - * Set the sender of the event. - * - * @param sender The sender's value to set, as a long. - * @return This AbstractEvent instance, to facilitate method chaining. - */ - @SuppressWarnings("unchecked") - public E sender(long sender) { - this.sender = sender; - return (E) this; - } - - /** - * Get the target of the event. - * - * @return The target's value as a long. - */ - public long target() { - return target; - } - - /** - * Set the target of the event. - * - * @param target The target's value to set, as a long. - * @return This AbstractEvent instance, to facilitate method chaining. - */ - @SuppressWarnings("unchecked") - public E target(long target) { - this.target = target; - return (E) this; - } - - /** - * Get the sending time of the event. - * - * @return The sending time as a long. - */ - public long sendingTime() { - return sendingTime; - } - - /** - * Set the sending time of the event. - * - * @param sendingTime The sending time to set, as a long. - * @return This AbstractEvent instance, to facilitate method chaining. - */ - @SuppressWarnings("unchecked") - public E sendingTime(long sendingTime) { - this.sendingTime = sendingTime; - return (E) this; - } - - @Override - public void writeMarshallable(BytesOut out) { - if (PREGENERATED_MARSHALLABLE) { - out.writeStopBit(MARSHALLABLE_VERSION); - out.writeLong(sender); - out.writeLong(target); - out.writeLong(sendingTime); - } else { - super.writeMarshallable(out); - } - } - - @Override - public void readMarshallable(BytesIn in) { - if (PREGENERATED_MARSHALLABLE) { - int version = (int) in.readStopBit(); - if (version == MARSHALLABLE_VERSION) { - sender = in.readLong(); - target = in.readLong(); - sendingTime = in.readLong(); - } else { - throw new IllegalStateException("Unknown version " + version); - } - } else { - super.readMarshallable(in); - } - } - - @Override - public void writeMarshallable(WireOut out) { - if (PREGENERATED_MARSHALLABLE) { - out.write("sender").writeLong(ShortTextLongConverter.INSTANCE, sender); - out.write("target").writeLong(ShortTextLongConverter.INSTANCE, target); - out.write("sendingTime").writeLong(MicroTimestampLongConverter.INSTANCE, sendingTime); - } else { - super.writeMarshallable(out); - } - } - - @Override - public void readMarshallable(WireIn in) { - if (PREGENERATED_MARSHALLABLE) { - sender = in.read("sender").readLong(ShortTextLongConverter.INSTANCE); - target = in.read("target").readLong(ShortTextLongConverter.INSTANCE); - sendingTime = in.read("sendingTime").readLong(MicroTimestampLongConverter.INSTANCE); - } else { - super.readMarshallable(in); - } - } - - @Override - public void validate() throws InvalidMarshallableException { - if (sender == 0) - throw new InvalidMarshallableException("sender is required"); - if (target == 0) - throw new InvalidMarshallableException("target is required"); - if (sendingTime == 0) - throw new InvalidMarshallableException("sendingTime is required"); - } -} -.... - -Lines 154, Blanks 19, Tokens 1262 - -== File: src\main\java\town\lost\oms\dto\CancelAll.java -.... -/* - */ - -package town.lost.oms.dto; - -import net.openhft.chronicle.bytes.BytesIn; -import net.openhft.chronicle.bytes.BytesOut; -import net.openhft.chronicle.core.io.InvalidMarshallableException; -import net.openhft.chronicle.wire.ShortTextLongConverter; -import net.openhft.chronicle.wire.WireIn; -import net.openhft.chronicle.wire.WireOut; -import net.openhft.chronicle.wire.converter.ShortText; - -/** - * The {@code CancelAll} class represents an event that is used to cancel all active orders for a particular symbol. - * - *

This class extends the {@link AbstractEvent} class with the type parameter {@link CancelAll}, indicating that - * it is processed as a {@code CancelAll} event. - * - *

This class is typically used like this: - * - *

- * public void cancelAll(CancelAll cancelAll) {
- *     // Iterate over active orders and cancel those matching the symbol
- *     for (Order order : activeOrders) {
- *         if (order.symbol() == cancelAll.symbol()) {
- *             CancelOrderRequest request = new CancelOrderRequest()
- *                 .sender(cancelAll.target())
- *                 .target(cancelAll.sender())
- *                 .symbol(order.symbol())
- *                 .clOrdID(order.clOrdID())
- *                 .sendingTime(cancelAll.sendingTime())
- *                 .origClOrdID(order.origClOrdID())
- *                 .side(order.side());
- *             out.cancelOrderRequest(request);
- *         }
- *     }
- * }
- * }
- * - *

Note that the {@code symbol} field is encoded using {@link ShortTextLongConverter} to minimize storage and transmission size. - */ -public class CancelAll extends AbstractEvent { - private static final int MASHALLABLE_VERSION = 1; - // Symbol for which all orders are to be canceled. - @ShortText - private long symbol; - - // Client order ID. - private String clOrdID = ""; - - /** - * Get the symbol for which all orders are to be canceled. - * - * @return The symbol value as a long. - */ - public long symbol() { - return symbol; - } - - /** - * Set the symbol for which all orders are to be canceled. - * - * @param symbol The symbol value to set, as a long. - * @return This CancelAll instance, to facilitate method chaining. - */ - public CancelAll symbol(long symbol) { - this.symbol = symbol; - return this; - } - - /** - * Retrieves the client order ID. - * - * @return The client order ID as a {@code String}. - */ - public String clOrdID() { - return clOrdID; - } - - /** - * Sets the client order ID and returns the {@code CancelAll} object. This method allows for method chaining. - * - * @param clOrdID The client order ID. - * @return The {@code CancelAll} object with the client order ID set. - */ - public CancelAll clOrdID(String clOrdID) { - this.clOrdID = clOrdID; - return this; - } - - @Override - public void writeMarshallable(WireOut out) { - super.writeMarshallable(out); - if (PREGENERATED_MARSHALLABLE) { - out.write("symbol").writeLong(ShortTextLongConverter.INSTANCE, symbol); - out.write("clOrdID").object(String.class, clOrdID); - } - } - - @Override - public void readMarshallable(WireIn in) { - super.readMarshallable(in); - if (PREGENERATED_MARSHALLABLE) { - symbol = in.read("symbol").readLong(ShortTextLongConverter.INSTANCE); - clOrdID = in.read("clOrdID").object(clOrdID, String.class); - } - } - - @Override - public void writeMarshallable(BytesOut out) { - super.writeMarshallable(out); - if (PREGENERATED_MARSHALLABLE) { - out.writeStopBit(MASHALLABLE_VERSION); - out.writeLong(symbol); - out.writeObject(String.class, clOrdID); - } - } - - @Override - public void readMarshallable(BytesIn in) { - super.readMarshallable(in); - if (PREGENERATED_MARSHALLABLE) { - int version = (int) in.readStopBit(); - if (version == MASHALLABLE_VERSION) { - symbol = in.readLong(); - clOrdID = in.readObject(String.class); - } else { - throw new IllegalStateException("Unknown version " + version); - } - } - } - - /** - * Validates the fields of this {@code CancelAll} event. - * - * @throws InvalidMarshallableException if any required field is missing or invalid. - */ - @Override - public void validate() throws InvalidMarshallableException { - super.validate(); - if (symbol == 0) { - throw new InvalidMarshallableException("symbol is required"); - } - if (clOrdID == null || clOrdID.isEmpty()) { - throw new InvalidMarshallableException("clOrdID is required"); - } - } -} -.... - -Lines 136, Blanks 13, Tokens 1121 - -== File: src\main\java\town\lost\oms\dto\CancelOrderRequest.java -.... -/* - */ - -package town.lost.oms.dto; - -import net.openhft.chronicle.bytes.BytesIn; -import net.openhft.chronicle.bytes.BytesOut; -import net.openhft.chronicle.core.io.InvalidMarshallableException; -import net.openhft.chronicle.wire.ShortTextLongConverter; -import net.openhft.chronicle.wire.WireIn; -import net.openhft.chronicle.wire.WireOut; -import net.openhft.chronicle.wire.converter.ShortText; - -/** - * The {@code CancelOrderRequest} class represents a request to cancel an order in a trading system. - * - *

This class extends the {@link AbstractEvent} class, with the type parameter being {@link CancelOrderRequest}. - * This indicates that the event will be processed into a {@link CancelOrderRequest} that represents a request to cancel an order. - * - *

Each {@code CancelOrderRequest} contains the symbol of the financial instrument for the order that is being requested to be cancelled and the client order ID. - * - *

    - *
  • symbol: The symbol of the financial instrument for the order.
  • - *
  • account: The account associated with the order.
  • - *
  • clOrdID: The client order ID, a unique identifier for the order.
  • - *
  • origClOrdID: The original client order ID of the order to be canceled.
  • - *
  • side: The side of the order (e.g., buy or sell).
  • - *
- * - *

The {@code symbol} and {@code account} fields are encoded using {@link ShortTextLongConverter} - * to save space, while the client order IDs are strings for identification purposes. - */ -public class CancelOrderRequest extends AbstractEvent { - private static final int MASHALLABLE_VERSION = 1; - // Symbol of the financial instrument for the order. - @ShortText - private long symbol; - @ShortText - private long account; - - // Client order ID. - private String clOrdID = ""; - - // Original client order ID. - private String origClOrdID = ""; - - // Side of the order (e.g., buy or sell). - private Side side; - - - /** - * Retrieves the symbol of the financial instrument for the order. - * - * @return The symbol as a {@code long}. - */ - public long symbol() { - return symbol; - } - - /** - * Sets the symbol of the financial instrument for the order and returns the {@code CancelOrderRequest} object. This method allows for method chaining. - * - * @param symbol The symbol of the financial instrument for the order. - * @return The {@code CancelOrderRequest} object with the symbol set. - */ - public CancelOrderRequest symbol(long symbol) { - this.symbol = symbol; - return this; - } - - - /** - * Retrieves the account associated with the order. - * - * @return The account as a {@code long}. - */ - public long account() { - return account; - } - - /** - * Sets the account associated with the order. - * - * @param account The account to set. - * @return This {@code CancelOrderRequest} instance for method chaining. - */ - public CancelOrderRequest account(long account) { - this.account = account; - return this; - } - - /** - * Retrieves the client order ID. - * - * @return The client order ID as a {@code String}. - */ - public String clOrdID() { - return clOrdID; - } - - /** - * Sets the client order ID and returns the {@code CancelOrderRequest} object. This method allows for method chaining. - * - * @param clOrdID The client order ID. - * @return The {@code CancelOrderRequest} object with the client order ID set. - */ - public CancelOrderRequest clOrdID(String clOrdID) { - this.clOrdID = clOrdID; - return this; - } - - /** - * Retrieves the original client order ID. - * - * @return The original client order ID as a {@code String}. - */ - public String origClOrdID() { - return origClOrdID; - } - - /** - * Sets the original client order ID. - * - * @param origClOrdID The original client order ID to set. - * @return This {@code CancelOrderRequest} instance for method chaining. - */ - public CancelOrderRequest origClOrdID(String origClOrdID) { - this.origClOrdID = origClOrdID; - return this; - } - - /** - * Retrieves the side of the order. - * - * @return The side of the order as a {@link Side} enum. - */ - public Side side() { - return side; - } - - /** - * Sets the side of the order. - * - * @param side The side to set (e.g., buy or sell). - * @return This {@code CancelOrderRequest} instance for method chaining. - */ - public CancelOrderRequest side(Side side) { - this.side = side; - return this; - } - - @Override - public void writeMarshallable(WireOut out) { - super.writeMarshallable(out); - if (PREGENERATED_MARSHALLABLE) { - out.write("symbol").writeLong(ShortTextLongConverter.INSTANCE, symbol); - out.write("account").writeLong(ShortTextLongConverter.INSTANCE, account); - out.write("clOrdID").object(String.class, clOrdID); - out.write("origClOrdID").object(String.class, origClOrdID); - out.write("side").object(Side.class, side); - } - } - - @Override - public void readMarshallable(WireIn in) { - super.readMarshallable(in); - if (PREGENERATED_MARSHALLABLE) { - symbol = in.read("symbol").readLong(ShortTextLongConverter.INSTANCE); - account = in.read("account").readLong(ShortTextLongConverter.INSTANCE); - clOrdID = in.read("clOrdID").object(clOrdID, String.class); - origClOrdID = in.read("origClOrdID").object(origClOrdID, String.class); - side = in.read("side").object(Side.class); - } - } - - @Override - public void writeMarshallable(BytesOut out) { - super.writeMarshallable(out); - if (PREGENERATED_MARSHALLABLE) { - out.writeStopBit(MASHALLABLE_VERSION); - out.writeLong(symbol); - out.writeLong(account); - out.writeObject(String.class, clOrdID); - out.writeObject(String.class, origClOrdID); - out.writeObject(Side.class, side); - } - } - - @SuppressWarnings("unchecked") - @Override - public void readMarshallable(BytesIn in) { - super.readMarshallable(in); - if (PREGENERATED_MARSHALLABLE) { - int version = (int) in.readStopBit(); - if (version == MASHALLABLE_VERSION) { - symbol = in.readLong(); - account = in.readLong(); - clOrdID = in.readObject(String.class); - origClOrdID = in.readObject(String.class); - side = in.readObject(Side.class); - } else { - throw new IllegalStateException("Unknown version " + version); - } - } - } - - /** - * Validates the fields of this {@code CancelOrderRequest}. - * - * @throws InvalidMarshallableException if any required field is missing or invalid. - */ - @Override - public void validate() throws InvalidMarshallableException { - if (symbol == 0) { - throw new InvalidMarshallableException("symbol is required"); - } - if (account == 0) { - throw new InvalidMarshallableException("account is required"); - } - if (clOrdID == null || clOrdID.isEmpty()) { - throw new InvalidMarshallableException("clOrdID is required"); - } - if (origClOrdID == null || origClOrdID.isEmpty()) { - throw new InvalidMarshallableException("origClOrdID is required"); - } - if (side == null) { - throw new InvalidMarshallableException("side is required"); - } - } -} -.... - -Lines 207, Blanks 23, Tokens 1812 - -== File: src\main\java\town\lost\oms\dto\Ccy.java -.... -package town.lost.oms.dto; - -/** - * The {@code Ccy} enum represents international currency codes as per the ISO 4217 standard. - * - *

Each constant corresponds to a specific currency, identified by its three-letter code. - * Additional information such as the currency name, country, and symbol can be accessed through - * the provided methods. - * - *

Example usage: - *

{@code
- * Ccy currency = Ccy.USD;
- * System.out.println("Currency Code: " + currency.getCode());
- * System.out.println("Currency Name: " + currency.getCurrencyName());
- * System.out.println("Country: " + currency.getCountry());
- * System.out.println("Symbol: " + currency.getSymbol());
- * }
- * - *

This would output: - *

- * Currency Code: USD
- * Currency Name: United States Dollar
- * Country: United States
- * Symbol: $
- * 
- */ -public enum Ccy { - /** - * United States Dollar. - */ - USD("United States Dollar", "United States", "$"), - /** - * Euro. - */ - EUR("Euro", "Eurozone", "€"), - /** - * British Pound Sterling. - */ - GBP("Pound Sterling", "United Kingdom", "£"), - /** - * Japanese Yen. - */ - JPY("Japanese Yen", "Japan", "¥"), - /** - * Swiss Franc. - */ - CHF("Swiss Franc", "Switzerland", "CHF"), - /** - * Canadian Dollar. - */ - CAD("Canadian Dollar", "Canada", "C$"), - /** - * Australian Dollar. - */ - AUD("Australian Dollar", "Australia", "A$"), - /** - * New Zealand Dollar. - */ - NZD("New Zealand Dollar", "New Zealand", "NZ$"), - /** - * Chinese Yuan Renminbi. - */ - CNY("Chinese Yuan", "China", "¥"), - /** - * Swedish Krona. - */ - SEK("Swedish Krona", "Sweden", "kr"), - /** - * Norwegian Krone. - */ - NOK("Norwegian Krone", "Norway", "kr"), - /** - * Russian Ruble. - */ - RUB("Russian Ruble", "Russia", "₽"), - /** - * South African Rand. - */ - ZAR("South African Rand", "South Africa", "R"), - /** - * Singapore Dollar. - */ - SGD("Singapore Dollar", "Singapore", "S$"), - /** - * Hong Kong Dollar. - */ - HKD("Hong Kong Dollar", "Hong Kong", "HK$"); - - // Fields - private final String currencyName; - private final String country; - private final String symbol; - - /** - * Constructs a {@code Ccy} enum constant with the specified currency name, country, and symbol. - * - * @param currencyName the full name of the currency - * @param country the country or region where the currency is used - * @param symbol the symbol representing the currency - */ - Ccy(String currencyName, String country, String symbol) { - this.currencyName = currencyName; - this.country = country; - this.symbol = symbol; - } - - /** - * Gets the full name of the currency. - * - * @return the currency name - */ - public String getCurrencyName() { - return currencyName; - } - - /** - * Gets the country or region where the currency is used. - * - * @return the country or region - */ - public String getCountry() { - return country; - } - - /** - * Gets the symbol representing the currency. - * - * @return the currency symbol - */ - public String getSymbol() { - return symbol; - } - - /** - * Gets the three-letter currency code as per the ISO 4217 standard. - * - * @return the currency code - */ - public String getCode() { - return this.name(); - } -} - -.... - -Lines 135, Blanks 8, Tokens 853 - -== File: src\main\java\town\lost\oms\dto\ExecutionReport.java -.... -/* - */ - -package town.lost.oms.dto; - -import net.openhft.chronicle.bytes.BytesIn; -import net.openhft.chronicle.bytes.BytesOut; -import net.openhft.chronicle.core.io.InvalidMarshallableException; -import net.openhft.chronicle.wire.ShortTextLongConverter; -import net.openhft.chronicle.wire.NanoTimestampLongConverter; -import net.openhft.chronicle.wire.WireIn; -import net.openhft.chronicle.wire.WireOut; -import net.openhft.chronicle.wire.converter.NanoTime; -import net.openhft.chronicle.wire.converter.ShortText; - -import static town.lost.oms.dto.ValidateUtil.*; - -/** - * The {@code ExecutionReport} class represents the execution report of an order in a trading system. - * - *

This class extends the {@link AbstractEvent} class, with the type parameter being {@link ExecutionReport}. - * This indicates that the event will be processed into an {@link ExecutionReport} that represents the execution report of an order. - * - *

Each {@code ExecutionReport} contains various pieces of information about the order execution, including: - * - *

    - *
  • symbol: The identifier of the financial instrument.
  • - *
  • transactTime: The transaction time in nanoseconds.
  • - *
  • orderQty: The quantity of the order.
  • - *
  • price: The price of the order.
  • - *
  • orderID: The unique identifier of the order.
  • - *
  • lastPx: The last traded price of the order.
  • - *
  • leavesQty: The remaining quantity of the order.
  • - *
  • cumQty: The accumulated quantity of the order.
  • - *
  • avgPx: The average price of the order.
  • - *
  • side: The side of the order (buy or sell).
  • - *
  • ordType: The type of the order (e.g., market or limit).
  • - *
  • clOrdID: The client order ID.
  • - *
  • text: An optional text message about the order execution.
  • - *
- * - *

The {@code symbol} field is encoded using {@link ShortTextLongConverter}, and {@code transactTime} and {@code orderID} - * use {@link NanoTimestampLongConverter} to save space. - */ -public class ExecutionReport extends AbstractEvent { - private static final int MASHALLABLE_VERSION = 1; - // Symbol of the financial instrument. - @ShortText - private long symbol; - - // Transaction time in nanoseconds. - @NanoTime - private long transactTime; - - // Quantity of the order. - private double orderQty; - - // Price of the order. - private double price; - - // Order ID in nanoseconds. - @NanoTime - private long orderID; - - // Last traded price of the order. - private double lastPx; - - // Remaining quantity of the order. - private double leavesQty; - - // Accumulated quantity of the order. - private double cumQty; - - // Average price of the order. - private double avgPx; - - // Side of the order (buy or sell). - private Side side; - - // Type of the order (market or limit). - private OrderType ordType; - - // Client order ID. - private String clOrdID = ""; - - // Optional text message about the order execution. - private String text = null; - - /** - * Retrieves the client order ID. - * - * @return the client order ID as a {@code String} - */ - public String clOrdID() { - return clOrdID; - } - - /** - * Sets the client order ID. - * - * @param clOrdID the client order ID to set - * @return this {@code ExecutionReport} instance for method chaining - */ - public ExecutionReport clOrdID(String clOrdID) { - this.clOrdID = clOrdID; - return this; - } - - /** - * Retrieves the symbol of the financial instrument. - * - * @return the symbol as a {@code long} - */ - public long symbol() { - return symbol; - } - - /** - * Sets the symbol of the financial instrument. - * - * @param symbol the symbol to set - * @return this {@code ExecutionReport} instance for method chaining - */ - public ExecutionReport symbol(long symbol) { - this.symbol = symbol; - return this; - } - - /** - * Retrieves the side of the order (buy or sell). - * - * @return the side as a {@link Side} enum value - */ - public Side side() { - return side; - } - - /** - * Sets the side of the order (buy or sell). - * - * @param side the side to set - * @return this {@code ExecutionReport} instance for method chaining - */ - public ExecutionReport side(Side side) { - this.side = side; - return this; - } - - /** - * Retrieves the transaction time in nanoseconds. - * - * @return the transaction time as a {@code long} - */ - public long transactTime() { - return transactTime; - } - - /** - * Sets the transaction time in nanoseconds. - * - * @param transactTime the transaction time to set - * @return this {@code ExecutionReport} instance for method chaining - */ - public ExecutionReport transactTime(long transactTime) { - this.transactTime = transactTime; - return this; - } - - /** - * Retrieves the quantity of the order. - * - * @return the order quantity as a {@code double} - */ - public double orderQty() { - return orderQty; - } - - /** - * Sets the quantity of the order. - * - * @param orderQty the order quantity to set - * @return this {@code ExecutionReport} instance for method chaining - */ - public ExecutionReport orderQty(double orderQty) { - this.orderQty = orderQty; - return this; - } - - /** - * Retrieves the price of the order. - * - * @return the price as a {@code double} - */ - public double price() { - return price; - } - - /** - * Sets the price of the order. - * - * @param price the price to set - * @return this {@code ExecutionReport} instance for method chaining - */ - public ExecutionReport price(double price) { - this.price = price; - return this; - } - - /** - * Retrieves the order ID. - * - * @return the order ID as a {@code long} - */ - public long orderID() { - return orderID; - } - - /** - * Sets the order ID. - * - * @param orderID the order ID to set - * @return this {@code ExecutionReport} instance for method chaining - */ - public ExecutionReport orderID(long orderID) { - this.orderID = orderID; - return this; - } - /** - * Returns the order type. - * - * @return the order type - */ - public OrderType ordType() { - return ordType; - } - /** - * Sets the order type and returns this ExecutionReport instance. - * - * @param ordType the order type to set - * @return this ExecutionReport instance - */ - public ExecutionReport ordType(OrderType ordType) { - this.ordType = ordType; - return this; - } - - /** - * Retrieves the last traded price of the order. - * - * @return the last traded price as a {@code double} - */ - public double lastPx() { - return lastPx; - } - - /** - * Sets the last traded price of the order. - * - * @param lastPx the last price to set - * @return this {@code ExecutionReport} instance for method chaining - */ - public ExecutionReport lastPx(double lastPx) { - this.lastPx = lastPx; - return this; - } - - /** - * Retrieves the remaining quantity of the order. - * - * @return the leaves quantity as a {@code double} - */ - public double leavesQty() { - return leavesQty; - } - - /** - * Sets the remaining quantity of the order. - * - * @param leavesQty the leaves quantity to set - * @return this {@code ExecutionReport} instance for method chaining - */ - public ExecutionReport leavesQty(double leavesQty) { - this.leavesQty = leavesQty; - return this; - } - - /** - * Retrieves the accumulated quantity of the order. - * - * @return the cumulative quantity as a {@code double} - */ - public double cumQty() { - return cumQty; - } - - /** - * Sets the accumulated quantity of the order. - * - * @param cumQty the cumulative quantity to set - * @return this {@code ExecutionReport} instance for method chaining - */ - public ExecutionReport cumQty(double cumQty) { - this.cumQty = cumQty; - return this; - } - - /** - * Retrieves the average price of the order. - * - * @return the average price as a {@code double} - */ - public double avgPx() { - return avgPx; - } - - /** - * Sets the average price and returns this ExecutionReport instance. - * - * @param avgPx the average price to set - * @return this ExecutionReport instance - */ - public ExecutionReport avgPx(double avgPx) { - this.avgPx = avgPx; - return this; - } - - /** - * Retrieves the optional text message about the order execution. - * - * @return the text message as a {@code String} - */ - public String text() { - return text; - } - - /** - * Sets the text of the report and returns this ExecutionReport instance. - * - * @param text the text message to set - * @return this {@code ExecutionReport} instance for method chaining - */ - public ExecutionReport text(String text) { - this.text = text; - return this; - } - - @Override - public void writeMarshallable(BytesOut out) { - super.writeMarshallable(out); - if (PREGENERATED_MARSHALLABLE) { - out.writeStopBit(MASHALLABLE_VERSION); - out.writeLong(symbol); - out.writeLong(transactTime); - out.writeDouble(orderQty); - out.writeDouble(price); - out.writeLong(orderID); - out.writeDouble(lastPx); - out.writeDouble(leavesQty); - out.writeDouble(cumQty); - out.writeDouble(avgPx); - out.writeObject(Side.class, side); - out.writeObject(OrderType.class, ordType); - out.writeObject(String.class, clOrdID); - out.writeObject(String.class, text); - } - } - - @Override - public void readMarshallable(BytesIn in) { - super.readMarshallable(in); - if (PREGENERATED_MARSHALLABLE) { - int version = (int) in.readStopBit(); - if (version == MASHALLABLE_VERSION) { - symbol = in.readLong(); - transactTime = in.readLong(); - orderQty = in.readDouble(); - price = in.readDouble(); - orderID = in.readLong(); - lastPx = in.readDouble(); - leavesQty = in.readDouble(); - cumQty = in.readDouble(); - avgPx = in.readDouble(); - side = in.readObject(Side.class); - ordType = in.readObject(OrderType.class); - clOrdID = in.readObject(String.class); - text = in.readObject(String.class); - } else { - throw new IllegalStateException("Unknown version " + version); - } - } - } - - @Override - public void writeMarshallable(WireOut out) { - super.writeMarshallable(out); - if (PREGENERATED_MARSHALLABLE) { - out.write("symbol").writeLong(ShortTextLongConverter.INSTANCE, symbol); - out.write("transactTime").writeLong(NanoTimestampLongConverter.INSTANCE, transactTime); - out.write("orderQty").writeDouble(orderQty); - out.write("price").writeDouble(price); - out.write("orderID").writeLong(NanoTimestampLongConverter.INSTANCE, orderID); - out.write("lastPx").writeDouble(lastPx); - out.write("leavesQty").writeDouble(leavesQty); - out.write("cumQty").writeDouble(cumQty); - out.write("avgPx").writeDouble(avgPx); - out.write("side").object(Side.class, side); - out.write("ordType").object(OrderType.class, ordType); - out.write("clOrdID").object(String.class, clOrdID); - out.write("text").object(String.class, text); - } - } - - @Override - public void readMarshallable(WireIn in) { - super.readMarshallable(in); - if (PREGENERATED_MARSHALLABLE) { - symbol = in.read("symbol").readLong(ShortTextLongConverter.INSTANCE); - transactTime = in.read("transactTime").readLong(NanoTimestampLongConverter.INSTANCE); - orderQty = in.read("orderQty").readDouble(); - price = in.read("price").readDouble(); - orderID = in.read("orderID").readLong(NanoTimestampLongConverter.INSTANCE); - lastPx = in.read("lastPx").readDouble(); - leavesQty = in.read("leavesQty").readDouble(); - cumQty = in.read("cumQty").readDouble(); - avgPx = in.read("avgPx").readDouble(); - side = in.read("side").object(side, Side.class); - ordType = in.read("ordType").object(ordType, OrderType.class); - clOrdID = in.read("clOrdID").object(clOrdID, String.class); - text = in.read("text").object(text, String.class); - } - } - - /** - * Validates the fields of this {@code ExecutionReport} event. - * - * @throws InvalidMarshallableException if any required field is missing or invalid - */ - @Override - public void validate() throws InvalidMarshallableException { - super.validate(); - if (symbol == 0) - throw new InvalidMarshallableException("symbol is required"); - if (transactTime == 0) - throw new InvalidMarshallableException("transactTime is required"); - if (invalidQuantity(orderQty)) - throw new InvalidMarshallableException("orderQty is invalid"); - if (invalidPrice(price)) - throw new InvalidMarshallableException("price is invalid"); - if (orderID == 0) - throw new InvalidMarshallableException("orderID is required"); - if (lastPx != 0 && invalidPrice(lastPx)) - throw new InvalidMarshallableException("lastPx is invalid"); - if (invalidQuantity(leavesQty)) - throw new InvalidMarshallableException("leavesQty is invalid"); - if (invalidQuantity(cumQty)) - throw new InvalidMarshallableException("cumQty is invalid"); - if (avgPx != 0 && invalidPrice(avgPx)) - throw new InvalidMarshallableException("avgPx is invalid"); - if (side == null) - throw new InvalidMarshallableException("side is required"); - if (ordType == null) - throw new InvalidMarshallableException("ordType is required"); - if (clOrdID == null || clOrdID.isEmpty()) - throw new InvalidMarshallableException("clOrdID is required"); - } -} -.... - -Lines 421, Blanks 45, Tokens 3417 - -== File: src\main\java\town\lost\oms\dto\NewOrderSingle.java -.... -/* - */ - -package town.lost.oms.dto; - -import net.openhft.chronicle.bytes.BytesIn; -import net.openhft.chronicle.bytes.BytesOut; -import net.openhft.chronicle.core.io.InvalidMarshallableException; -import net.openhft.chronicle.wire.*; -import net.openhft.chronicle.wire.converter.NanoTime; -import net.openhft.chronicle.wire.converter.ShortText; - -import static town.lost.oms.dto.ValidateUtil.invalidPrice; -import static town.lost.oms.dto.ValidateUtil.invalidQuantity; - -/** - * The {@code NewOrderSingle} class represents a new single order in a trading system. - * - *

This class extends the {@link AbstractEvent} class, with the type parameter being {@link NewOrderSingle}. - * This indicates that the event will be processed into a {@link NewOrderSingle} that will be sent to the order - * system. - * - *

Each {@code NewOrderSingle} contains various pieces of information about the order, including: - * - *

    - *
  • symbol: The identifier of the financial instrument.
  • - *
  • transactTime: The transaction time in nanoseconds.
  • - *
  • account: The account associated with the order.
  • - *
  • orderQty: The quantity of the order.
  • - *
  • price: The price of the order.
  • - *
  • side: The side of the order (buy or sell).
  • - *
  • clOrdID: The client order ID.
  • - *
  • ordType: The type of the order (e.g., market or limit).
  • - *
  • timeInForce: The time-in-force instruction for the order.
  • - *
  • currency: The currency of the order.
  • - *
- * - *

The symbol is encoded using {@link ShortText} and the transaction time is in nanoseconds, both to save space. - * The client order ID is a string that identifies the order, the side indicates whether the order is to buy or sell, - * and the order type indicates whether the order is a market order or a limit order. - */ -public class NewOrderSingle extends AbstractEvent { - private static final int MASHALLABLE_VERSION = 1; - // Symbol of the financial instrument. - @ShortText - private long symbol; - // Transaction time in nanoseconds. - @NanoTime - private long transactTime; - @ShortText - private long account; - - // Quantity of the order. - private double orderQty; - - // Price of the order. - private double price; - - // Side of the order (buy or sell). - private Side side; - - // Client order ID. - private String clOrdID = ""; - - // Type of the order (market or limit). - private OrderType ordType; - - // Time-in-force instruction for the order. - private TimeInForce timeInForce; - - // Currency of the order. - private Ccy currency; - - /** - * Get the client order ID. - * - * @return The client order ID as a string. - */ - public String clOrdID() { - return clOrdID; - } - - /** - * Sets the client order ID. - * - * @param clOrdID The client order ID to set, as a string. - * @return This {@code NewOrderSingle} instance, to facilitate method chaining. - */ - public NewOrderSingle clOrdID(String clOrdID) { - this.clOrdID = clOrdID; - return this; - } - - /** - * Get the symbol of the financial instrument. - * - * @return The symbol as a long. - */ - public long symbol() { - return symbol; - } - - /** - * Sets the symbol of the financial instrument. - * - * @param symbol The symbol to set, as a long. - * @return This {@code NewOrderSingle} instance, to facilitate method chaining. - */ - public NewOrderSingle symbol(long symbol) { - this.symbol = symbol; - return this; - } - - /** - * Get the side of the order (buy or sell). - * - * @return The side as a {@link Side} enum value. - */ - public Side side() { - return side; - } - - /** - * Sets the side of the order (buy or sell). - * - * @param side The side to set, as a {@link Side} enum value. - * @return This {@code NewOrderSingle} instance, to facilitate method chaining. - */ - public NewOrderSingle side(Side side) { - this.side = side; - return this; - } - - /** - * Get the transaction time in nanoseconds. - * - * @return The transaction time as a long. - */ - public long transactTime() { - return transactTime; - } - - /** - * Sets the transaction time in nanoseconds. - * - * @param transactTime The transaction time to set, as a long. - * @return This {@code NewOrderSingle} instance, to facilitate method chaining. - */ - public NewOrderSingle transactTime(long transactTime) { - this.transactTime = transactTime; - return this; - } - - /** - * Get the quantity of the order. - * - * @return The order quantity as a double. - */ - public double orderQty() { - return orderQty; - } - - /** - * Sets the quantity of the order. - * - * @param orderQty The order quantity to set, as a double. - * @return This {@code NewOrderSingle} instance, to facilitate method chaining. - */ - public NewOrderSingle orderQty(double orderQty) { - this.orderQty = orderQty; - return this; - } - - /** - * Get the price of the order. - * - * @return The price as a double. - */ - public double price() { - return price; - } - - /** - * Sets the price of the order. - * - * @param price The price to set, as a double. - * @return This {@code NewOrderSingle} instance, to facilitate method chaining. - */ - public NewOrderSingle price(double price) { - this.price = price; - return this; - } - - /** - * Get the type of the order (market or limit). - * - * @return The order type as an {@link OrderType} enum value. - */ - public OrderType ordType() { - return ordType; - } - - /** - * Sets the type of the order (market or limit). - * - * @param ordType The order type to set, as an {@link OrderType} enum value. - * @return This {@code NewOrderSingle} instance, to facilitate method chaining. - */ - public NewOrderSingle ordType(OrderType ordType) { - this.ordType = ordType; - return this; - } - - /** - * Retrieves the time-in-force instruction for the order. - * - * @return The time-in-force as a {@link TimeInForce} enum value. - */ - public TimeInForce timeInForce() { - return timeInForce; - } - - /** - * Sets the time-in-force instruction for the order. - * - * @param timeInForce The time-in-force to set. - * @return This {@code NewOrderSingle} instance for method chaining. - */ - public NewOrderSingle timeInForce(TimeInForce timeInForce) { - this.timeInForce = timeInForce; - return this; - } - - /** - * Retrieves the account associated with the order. - * - * @return The account as a {@code long}. - */ - public long account() { - return account; - } - - /** - * Sets the account associated with the order. - * - * @param account The account to set. - * @return This {@code NewOrderSingle} instance for method chaining. - */ - public NewOrderSingle account(long account) { - this.account = account; - return this; - } - - /** - * Retrieves the currency of the order. - * - * @return The currency as a {@link Ccy} enum value. - */ - public Ccy currency() { - return currency; - } - - /** - * Sets the currency of the order. - * - * @param currency The currency to set. - * @return This {@code NewOrderSingle} instance for method chaining. - */ - public NewOrderSingle currency(Ccy currency) { - this.currency = currency; - return this; - } - - @Override - public void writeMarshallable(BytesOut out) { - super.writeMarshallable(out); - if (PREGENERATED_MARSHALLABLE) { - out.writeStopBit(MASHALLABLE_VERSION); - out.writeLong(symbol); - out.writeLong(transactTime); - out.writeLong(account); - out.writeDouble(orderQty); - out.writeDouble(price); - out.writeObject(Side.class, side); - out.writeObject(OrderType.class, ordType); - out.writeObject(String.class, clOrdID); - out.writeObject(TimeInForce.class, timeInForce); - out.writeObject(Ccy.class, currency); - } - } - - @Override - public void readMarshallable(BytesIn in) { - super.readMarshallable(in); - if (PREGENERATED_MARSHALLABLE) { - int version = (int) in.readStopBit(); - if (version == MASHALLABLE_VERSION) { - symbol = in.readLong(); - transactTime = in.readLong(); - account = in.readLong(); - orderQty = in.readDouble(); - price = in.readDouble(); - side = in.readObject(Side.class); - ordType = in.readObject(OrderType.class); - clOrdID = in.readObject(String.class); - timeInForce = in.readObject(TimeInForce.class); - currency = in.readObject(Ccy.class); - } - } - } - - @Override - public void writeMarshallable(WireOut out) { - super.writeMarshallable(out); - if (PREGENERATED_MARSHALLABLE) { - out.write("symbol").writeLong(ShortTextLongConverter.INSTANCE, symbol); - out.write("transactTime").writeLong(NanoTimestampLongConverter.INSTANCE, transactTime); - out.write("account").writeLong(ShortTextLongConverter.INSTANCE, account); - out.write("orderQty").writeDouble(orderQty); - out.write("price").writeDouble(price); - out.write("side").object(Side.class, side); - out.write("ordType").object(OrderType.class, ordType); - out.write("clOrdID").object(String.class, clOrdID); - out.write("timeInForce").object(TimeInForce.class, timeInForce); - out.write("currency").object(Ccy.class, currency); - } - } - - @Override - public void readMarshallable(WireIn in) { - super.readMarshallable(in); - if (PREGENERATED_MARSHALLABLE) { - symbol = in.read("symbol").readLong(ShortTextLongConverter.INSTANCE); - transactTime = in.read("transactTime").readLong(NanoTimestampLongConverter.INSTANCE); - account = in.read("account").readLong(ShortTextLongConverter.INSTANCE); - orderQty = in.read("orderQty").readDouble(); - price = in.read("price").readDouble(); - side = in.read("side").object(side, Side.class); - ordType = in.read("ordType").object(OrderType.class); - clOrdID = in.read("clOrdID").object(clOrdID, String.class); - timeInForce = in.read("timeInForce").object(TimeInForce.class); - currency = in.read("currency").object(Ccy.class); - } - } - - /** - * Validates the fields of this {@code NewOrderSingle} event. - * - * @throws InvalidMarshallableException if any required field is missing or invalid. - */ - @Override - public void validate() throws InvalidMarshallableException { - super.validate(); - if (symbol == 0) - throw new InvalidMarshallableException("symbol is required"); - if (transactTime == 0) - throw new InvalidMarshallableException("transactTime is required"); - if (account == 0) - throw new InvalidMarshallableException("account is required"); - if (orderQty == 0) - throw new InvalidMarshallableException("orderQty is required"); - if (invalidQuantity(orderQty)) - throw new InvalidMarshallableException("orderQty is invalid"); - if (invalidPrice(price)) - throw new InvalidMarshallableException("price is invalid"); - if (side == null) - throw new InvalidMarshallableException("side is required"); - if (ordType == null) - throw new InvalidMarshallableException("ordType is required"); - if (clOrdID == null || clOrdID.isEmpty()) - throw new InvalidMarshallableException("clOrdID is required"); - if (timeInForce == null) - throw new InvalidMarshallableException("timeInForce is required"); - if (currency == null) - throw new InvalidMarshallableException("currency is required"); - } -} -.... - -Lines 341, Blanks 36, Tokens 2886 - -== File: src\main\java\town\lost\oms\dto\OrderCancelReject.java -.... -/* - */ - -package town.lost.oms.dto; - -import net.openhft.chronicle.bytes.BytesIn; -import net.openhft.chronicle.bytes.BytesOut; -import net.openhft.chronicle.core.io.InvalidMarshallableException; -import net.openhft.chronicle.wire.ShortTextLongConverter; -import net.openhft.chronicle.wire.WireIn; -import net.openhft.chronicle.wire.WireOut; -import net.openhft.chronicle.wire.converter.ShortText; - -/** - * The {@code OrderCancelReject} class represents an event that is used to indicate that a request to cancel an order was rejected. - * - *

This class extends the {@link AbstractEvent} class, with the type parameter being {@link OrderCancelReject}. - * This indicates that the event will be processed into a {@link OrderCancelReject} that will be sent to the order - * system. - * - *

Each {@code OrderCancelReject} event contains: - *

    - *
  • symbol: The identifier for the financial instrument involved in the order.
  • - *
  • clOrdID: The client order ID of the order that was requested to be canceled.
  • - *
  • reason: A string explaining why the cancellation request was rejected.
  • - *
- * - *

Note that the symbol field is encoded using {@link ShortText} to minimize the space required for storage and transmission. - */ -public class OrderCancelReject extends AbstractEvent { - private static final int MASHALLABLE_VERSION = 1; - // Symbol for which the order cancellation was rejected. - @ShortText - private long symbol; - - // Client order ID of the order that was requested to be canceled. - private String clOrdID = ""; - - // Reason for the rejection of the order cancellation request. - private String reason = ""; - - /** - * Get the client order ID of the order that was requested to be canceled. - * - * @return The client order ID as a string. - */ - public String clOrdID() { - return clOrdID; - } - - /** - * Set the client order ID of the order that was requested to be canceled. - * - * @param clOrdID The client order ID to set, as a string. - * @return This OrderCancelReject instance, to facilitate method chaining. - */ - public OrderCancelReject clOrdID(String clOrdID) { - this.clOrdID = clOrdID; - return this; - } - - /** - * Get the symbol for which the order cancellation was rejected. - * - * @return The symbol value as a {@code long}. - */ - public long symbol() { - return symbol; - } - - /** - * Set the symbol for which the order cancellation was rejected. - * - * @param symbol The symbol value to set, as a long. - * @return This {@code OrderCancelReject} instance, to facilitate method chaining. - */ - public OrderCancelReject symbol(long symbol) { - this.symbol = symbol; - return this; - } - - /** - * Get the reason for the rejection of the order cancellation request. - * - * @return The reason as a string. - */ - public String reason() { - return reason; - } - - /** - * Set the reason for the rejection of the order cancellation request. - * - * @param reason The reason to set, as a string. - * @return This {@code OrderCancelReject} instance, to facilitate method chaining. - */ - public OrderCancelReject reason(String reason) { - this.reason = reason; - return this; - } - - @Override - public void writeMarshallable(WireOut out) { - validate(); - super.writeMarshallable(out); - if (PREGENERATED_MARSHALLABLE) { - out.write("symbol").writeLong(ShortTextLongConverter.INSTANCE, symbol); - out.write("clOrdID").object(String.class, clOrdID); - out.write("reason").object(String.class, reason); - } - } - - @Override - public void readMarshallable(WireIn in) { - super.readMarshallable(in); - if (PREGENERATED_MARSHALLABLE) { - symbol = in.read("symbol").readLong(ShortTextLongConverter.INSTANCE); - clOrdID = in.read("clOrdID").object(clOrdID, String.class); - reason = in.read("reason").object(reason, String.class); - } - } - - @Override - public void writeMarshallable(BytesOut out) { - validate(); - super.writeMarshallable(out); - if (PREGENERATED_MARSHALLABLE) { - out.writeStopBit(MASHALLABLE_VERSION); - out.writeLong(symbol); - out.writeObject(String.class, clOrdID); - out.writeObject(String.class, reason); - } - } - - @Override - public void readMarshallable(BytesIn in) { - super.readMarshallable(in); - if (PREGENERATED_MARSHALLABLE) { - int version = (int) in.readStopBit(); - if (version == MASHALLABLE_VERSION) { - symbol = in.readLong(); - clOrdID = in.readObject(String.class); - reason = in.readObject(String.class); - } else { - throw new IllegalStateException("Unknown version " + version); - } - } - } - - /** - * Validates the fields of this {@code OrderCancelReject} event. - * - * @throws InvalidMarshallableException if any required field is missing or invalid. - */ - @Override - public void validate() throws InvalidMarshallableException { - super.validate(); - if (symbol == 0) - throw new InvalidMarshallableException("symbol is required"); - if (clOrdID == null || clOrdID.isEmpty()) - throw new InvalidMarshallableException("clOrdID is required"); - if (reason == null || reason.isEmpty()) - throw new InvalidMarshallableException("reason is required"); - } -} -.... - -Lines 149, Blanks 16, Tokens 1294 - -== File: src\main\java\town\lost\oms\dto\OrderType.java -.... -/* - */ - -package town.lost.oms.dto; -/** - * Enumeration for the types of orders in a trading system. - * - *

It contains two values: MARKET and LIMIT, which represent the two main types of orders that can be placed - * in a trading system. A MARKET order is an order to buy or sell a security at the current market price, - * whereas a LIMIT order is an order to buy or sell a security at a specific price or better. - * - *

This enumeration can be used when creating trading orders, like so: - * - *

- * NewOrderSingle nos = new NewOrderSingle()
- *    .sender(ShortText.parse("client"))
- *    .target(ShortText.parse("OMS"))
- *    .clOrdID("clOrdId")
- *    .orderQty(1e6)
- *    .price(1.6)
- *    .symbol(ShortText.parse("AUDUSD"))
- *    .ordType(OrderType.LIMIT)
- *    .side(BuySell.BUY);
- * 
- * - *

Note that the order type is indicated by the {@link OrderType} used in the 'ordType' field of the order. - */ -public enum OrderType { - /** - * Market order type, which means the order should be executed at the current market price. - */ - MARKET, - - /** - * Limit order type, which means the order should be executed at a specific price or better. - */ - LIMIT, - - /** - * Pegged order type, where the price is pegged to a benchmark price, such as the best bid or ask. - */ - PEGGED, - - /** - * Fill or Kill order type, which must be executed immediately in its entirety or cancelled. - */ - FILL_OR_KILL, - - /** - * Immediate or Cancel order type, which executes all or part immediately and cancels any unfilled portion. - */ - IMMEDIATE_OR_CANCEL, -} - -.... - -Lines 48, Blanks 6, Tokens 409 - -== File: src\main\java\town\lost\oms\dto\package-info.java -.... -/** - * Provides the classes and interfaces necessary to create and manage Order Management System (OMS) data transfer objects (DTOs). - * - *

This package includes the following key components: - * - *

    - *
  • AbstractEvent and its subclasses, which represent different types of events that can occur in an OMS, such as: - *
      - *
    • {@link town.lost.oms.dto.NewOrderSingle}: Represents a new order submission.
    • - *
    • {@link town.lost.oms.dto.CancelOrderRequest}: Represents a request to cancel an existing order.
    • - *
    • {@link town.lost.oms.dto.ExecutionReport}: Represents the execution status of an order.
    • - *
    • {@link town.lost.oms.dto.OrderCancelReject}: Indicates that an order cancellation request was rejected.
    • - *
    • {@link town.lost.oms.dto.CancelAll}: Represents a request to cancel all orders for a specific symbol.
    • - *
    - *
  • - *
  • Enums that define constants for various order attributes: - *
      - *
    • {@link town.lost.oms.dto.Side}: Represents the side of an order (e.g., buy or sell).
    • - *
    • {@link town.lost.oms.dto.OrderType}: Represents the type of an order (e.g., market, limit).
    • - *
    • {@link town.lost.oms.dto.TimeInForce}: Represents the time-in-force instructions for an order.
    • - *
    • {@link town.lost.oms.dto.Ccy}: Represents currency codes as per ISO 4217.
    • - *
    - *
  • - *
  • Utility Classes: - *
      - *
    • {@link town.lost.oms.dto.ValidateUtil}: Provides utility methods for validating order parameters such as price and quantity.
    • - *
    - *
  • - *
- * - *

Each class is designed to be marshalled and unmarshalled efficiently for high-performance data transfer, leveraging serialization optimizations provided by Chronicle Wire. - * - *

For more details, refer to the documentation of each individual class. - */ -package town.lost.oms.dto; -.... - -Lines 35, Blanks 0, Tokens 493 - -== File: src\main\java\town\lost\oms\dto\Side.java -.... -/* - */ - -package town.lost.oms.dto; - -/** - * Enumeration for the direction of a trade order. - * - *

It contains two values: BUY and SELL, which represents the direction of the order. BUY (+1) means the order is to - * purchase, while SELL (-1) implies the order is to sell. - * - *

This enumeration can be used to create trading orders, like so: - * - *

{@code
- * NewOrderSingle nos = new NewOrderSingle()
- *    .sender(toLong("sender"))
- *    .target(toLong("target"))
- *    .transactTime(now())
- *    .sendingTime(now())
- *    .orderQty(1)
- *    .ordType(OrderType.MARKET)
- *    .side(Side.BUY)
- *    .symbol(toLong("EURUSD"));
- * }
- * - *

Note that the direction is indicated by the {@link Side} used in the 'side' field of the order. - */ -public enum Side { - /** - * Buy order direction, represented by an integer value of +1. - * Indicates an order to purchase. - */ - BUY(+1), - - /** - * Sell order direction, represented by an integer value of -1. - * Indicates an order to sell. - */ - SELL(-1); - - /** - * The direction of the order. - */ - public final int direction; - - /** - * Constructs a BuySell enum with the specified direction. - * - * @param direction the direction of the order (+1 for buy, -1 for sell) - */ - Side(int direction) { - this.direction = direction; - } - - /** - * Gets the direction indicator of the order. - * - * @return the direction indicator as an integer - */ - public int direction() { - return direction; - } - } -.... - -Lines 57, Blanks 6, Tokens 392 - -== File: src\main\java\town\lost\oms\dto\TimeInForce.java -.... -package town.lost.oms.dto; - -/** - * The {@code TimeInForce} enum represents the time conditions under which an order will be executed. - * - *

These conditions specify how long an order remains active in the market before it is executed or expires. - * This enum includes commonly used time-in-force instructions in trading systems, conforming to standard trading practices. - * - *

Example usage: - *

{@code
- * TimeInForce tif = TimeInForce.DAY;
- * System.out.println("Time in Force: " + tif);
- * }
- * - *

This would output: - *

- * Time in Force: DAY
- * 
- */ -public enum TimeInForce { - /** - * Indicates that the order should be valid for the current trading day only. - * If not executed by the end of the trading day, the order expires. - */ - DAY, - - /** - * "Good Till Cancelled" - The order remains active until it is either executed or explicitly cancelled by the trader. - * It may carry over to the next trading day. - */ - GTC, - - /** - * "Immediate or Cancel" - The order attempts to execute all or part immediately. - * Any portion not immediately executed is cancelled. - */ - IOC, - - /** - * "Fill or Kill" - The order must be executed immediately in its entirety, or it is cancelled. - * Partial fills are not allowed. - */ - FOK, - - /** - * "Good Till Date" - The order remains active until a specified date unless it is executed or cancelled before then. - */ - GTD, - - /** - * "Good Till Time" - The order remains active until a specified time on the current trading day. - * If not executed by that time, the order expires. - */ - GTT, - - /** - * "At the Opening" - The order is to be executed at the opening of the market. - * If not executed at the opening, it is cancelled. - */ - OPG, - - /** - * "At the Close" - The order is to be executed at the close of the market. - * If not executed at the close, it is cancelled. - */ - ATC, - - /** - * "Good for Auction" - The order is valid only during the auction period. - */ - GFA, - - /** - * "Good in Session" - The order is valid only during the specified trading session. - */ - GIS, - - /** - * "Good Through Crossing" - The order is valid through the crossing session. - */ - GTX, - - /** - * "Systematic Internaliser Only" - The order is to be executed only on a systematic internaliser. - */ - SIO -} -.... - -Lines 75, Blanks 12, Tokens 586 - -== File: src\main\java\town\lost\oms\dto\ValidateUtil.java -.... -/* - */ -package town.lost.oms.dto; - -/** - * The {@code ValidateUtil} class provides utility methods for validating numerical values such as price and quantity. - * - *

This class is final and cannot be instantiated. It contains static methods that check whether a given price or quantity - * is valid according to predefined business rules. - * - *

Example usage: - * - *

{@code
- * double price = 100.0;
- * if (ValidateUtil.invalidPrice(price)) {
- *     throw new IllegalArgumentException("Invalid price");
- * }
- * }
- */ -public final class ValidateUtil { - - /** - * Private constructor to prevent instantiation. - */ - private ValidateUtil() { - } - - /** - * Checks if the provided price is invalid. - * - *

A price is considered invalid if it is not a finite number or if it is less than or equal to zero. - * - * @param price the price to validate - * @return {@code true} if the price is invalid; {@code false} otherwise - */ - public static boolean invalidPrice(double price) { - return !Double.isFinite(price) || (price <= 0); - } - - /** - * Checks if the provided quantity is invalid. - * - *

A quantity is considered invalid if it is not a finite number or if it is less than zero. - * - * @param quantity the quantity to validate - * @return {@code true} if the quantity is invalid; {@code false} otherwise - */ - public static boolean invalidQuantity(double quantity) { - return !Double.isFinite(quantity) || (quantity < 0); - } -} -.... - -Lines 47, Blanks 4, Tokens 352 - -== File: src\main\java\town\lost\oms\OMSBenchmarkMain.java -.... -/* - */ - -package town.lost.oms; - -import net.openhft.affinity.AffinityLock; -import net.openhft.chronicle.bytes.MethodReader; -import net.openhft.chronicle.core.Jvm; -import net.openhft.chronicle.core.OS; -import net.openhft.chronicle.core.io.BackgroundResourceReleaser; -import net.openhft.chronicle.core.io.IOTools; -import net.openhft.chronicle.jlbh.JLBH; -import net.openhft.chronicle.jlbh.JLBHOptions; -import net.openhft.chronicle.jlbh.JLBHTask; -import net.openhft.chronicle.queue.ChronicleQueue; -import net.openhft.chronicle.threads.DiskSpaceMonitor; -import net.openhft.chronicle.wire.ShortTextLongConverter; -import town.lost.oms.api.OMSIn; -import town.lost.oms.api.OMSOut; -import town.lost.oms.dto.*; - -import static town.lost.oms.OrderAdderMain.now; -// isolcpus=5,6,7 set in grub.cfg -// sudo cpupower frequency-set -g performance -d 4.5g - -// -Xmx64m -XX:+UnlockCommercialFeatures -XX:+FlightRecorder -XX:StartFlightRecording=name=test,filename=test.jfr,dumponexit=true,settings=profile -XX:-UseTLAB -/* Run on a Ryzen 9 5950X, Ubuntu 20.10 --Xmx1g -DbyteInBinary=true -DpregeneratedMarshallable=true -Dthroughput=100000 -DrunTime=120 -Dpath=/tmp --------------------------------- SUMMARY (end to end) us ------------------------------------------- -Percentile run1 run2 run3 run4 run5 % Variation -50.0: 2.06 1.99 1.99 1.99 1.99 0.00 -90.0: 2.13 2.04 2.04 2.05 2.05 0.33 -99.0: 3.16 3.09 3.09 3.10 3.10 0.17 -99.7: 3.66 3.57 3.58 3.58 3.57 0.15 -99.9: 4.02 3.92 3.90 3.90 3.90 0.41 -99.97: 4.78 4.66 4.65 4.70 4.65 0.68 -99.99: 5.58 5.29 5.21 5.34 5.14 2.43 -99.997: 8.56 6.01 5.88 6.34 5.90 5.00 -99.999: 9.30 6.81 6.42 9.39 6.47 23.55 -worst: 50.11 74.11 10.58 13.30 11.15 80.02 ----------------------------------------------------------------------------------------------------- - --Xmx772m -DbyteInBinary=true -DpregeneratedMarshallable=true -Dthroughput=100000 -DrunTime=120 -Dpath=/nvme/tmp --------------------------------- SUMMARY (end to end) us ------------------------------------------- -Percentile run1 run2 run3 run4 run5 % Variation -50.0: 1.98 1.97 1.97 1.97 1.97 0.00 -90.0: 2.05 2.04 2.04 2.04 2.04 0.00 -99.0: 3.38 3.39 3.36 3.34 3.34 0.95 -99.7: 6.34 6.38 5.80 5.45 5.46 10.20 -99.9: 16.27 16.42 16.48 16.42 16.48 0.26 -99.97: 17.38 17.38 17.38 17.25 17.38 0.49 -99.99: 18.91 18.46 18.66 18.21 18.66 1.61 -99.997: 24.29 22.24 22.82 21.09 25.12 11.31 -99.999: 29.15 27.94 26.40 25.76 402.94 90.71 -worst: 214.27 51.78 537.60 50.24 1255.42 94.12 ----------------------------------------------------------------------------------------------------- - */ -public class OMSBenchmarkMain { - static { - System.setProperty("pauser.minProcessors", "2"); - System.setProperty("disableValidate", "true"); - } - - public static final int THROUGHPUT = Integer.getInteger("throughput", 100_000); - public static final int RUN_TIME = Integer.getInteger("runTime", 10); - public static final ShortTextLongConverter ShortText = ShortTextLongConverter.INSTANCE; - public static final String PATH = System.getProperty("path", OS.TMP); - public static final boolean ACCOUNT_FOR_COORDINATED_OMISSION = Jvm.getBoolean("accountForCoordinatedOmission"); - - @SuppressWarnings("try") - public static void main(String[] args) { - printProperties(); - - String tmpDir = PATH + "/bench-" + System.nanoTime(); - try (ChronicleQueue input = single(tmpDir, "/input"); - ChronicleQueue output = single(tmpDir, "/output")) { - - // processing thread - Thread processor = new Thread(() -> { - try (AffinityLock ignored = AffinityLock.acquireCore()) { - OMSOut out = output.createAppender().methodWriter(OMSOut.class); - OMSImpl oms = new OMSImpl(out); - MethodReader in = input.createTailer("test").methodReader(oms); - while (!Thread.currentThread().isInterrupted()) { - in.readOne(); - } - } - }, "processor"); - processor.start(); - - JLBH jlbh = new JLBH(new JLBHOptions() - .warmUpIterations(300_000) - .pauseAfterWarmupMS(500) - .throughput(THROUGHPUT) - .iterations(THROUGHPUT * RUN_TIME) - .runs(5) - .recordOSJitter(false) - .accountForCoordinatedOmission(ACCOUNT_FOR_COORDINATED_OMISSION) - .acquireLock(AffinityLock::acquireCore) - .jlbhTask(new MyJLBHTask(input))); - - Thread last = new Thread(() -> { - try (AffinityLock ignored = AffinityLock.acquireCore()) { - final MethodReader reader = output.createTailer().methodReader(new OMSOut() { - @Override - public void executionReport(ExecutionReport er) { - jlbh.sampleNanos(System.nanoTime() - er.sendingTime()); - } - - @Override - public void orderCancelReject(OrderCancelReject ocr) { - } - - @Override - public void jvmError(String msg) { - } - }); - while (!Thread.currentThread().isInterrupted()) - reader.readOne(); - - } catch (Throwable t) { - t.printStackTrace(); - } - }, "last"); - last.start(); - - jlbh.start(); - - processor.interrupt(); - last.interrupt(); - } - printProperties(); - Jvm.pause(1000); - IOTools.deleteDirWithFiles(tmpDir); - // for a clean shutdown - BackgroundResourceReleaser.stop(); - DiskSpaceMonitor.INSTANCE.close(); - } - - static ChronicleQueue single(String tmpDir, String x) { - return ChronicleQueue.singleBuilder(tmpDir + x) - .blockSize(OS.isSparseFileSupported() ? 64L << 30 : 64L << 20) - .build(); - } - - private static void printProperties() { - long estimatedMemory = Math.round(Runtime.getRuntime().totalMemory() / 1e6); - System.out.println("-Xmx" + estimatedMemory + "m " + - "-DbyteInBinary=" + AbstractEvent.BYTES_IN_BINARY + " " + - "-DpregeneratedMarshallable=" + AbstractEvent.PREGENERATED_MARSHALLABLE + " " + - "-Dthroughput=" + THROUGHPUT + " " + - "-DrunTime=" + RUN_TIME + " " + - "-Dpath=" + PATH + " " + - "-DaccountForCoordinatedOmission=" + ACCOUNT_FOR_COORDINATED_OMISSION); - } - - private static class MyJLBHTask implements JLBHTask { - private JLBH jlbh; - private NewOrderSingle nos; - private OMSIn in; - - public MyJLBHTask(ChronicleQueue input) { - nos = new NewOrderSingle() - .sender(ShortText.parse("client")) - .target(ShortText.parse("OMS")) - .clOrdID("clOrdId") - .orderQty(1e6) - .price(1.6) - .symbol(ShortText.parse("AUDUSD")) - .ordType(OrderType.LIMIT) - .side(Side.SELL); - in = input.createAppender().methodWriter(OMSIn.class); - } - - @Override - public void init(JLBH jlbh) { - this.jlbh = jlbh; - } - - @Override - public void run(long startTimeNS) { - nos.sendingTime(startTimeNS).transactTime(now()); - in.newOrderSingle(nos); - } - } -} -.... - -Lines 164, Blanks 22, Tokens 2236 - -== File: src\main\java\town\lost\oms\OMSImpl.java -.... -/* - */ - -package town.lost.oms; - -import net.openhft.chronicle.core.time.SystemTimeProvider; -import town.lost.oms.api.OMSIn; -import town.lost.oms.api.OMSOut; -import town.lost.oms.dto.*; - -/** - * The {@code OMSImpl} class is a concrete implementation of the {@link OMSIn} interface, - * acting as an intermediary between client requests and the {@link OMSOut} interface. - * - *

This class processes requests for order creation, order cancellation, and cancelling all orders. - * It generates appropriate responses using {@link ExecutionReport} and {@link OrderCancelReject} - * and sends them through the {@link OMSOut} interface. - * - *

Note: This class is not thread-safe. If multiple threads are expected to use - * the same instance of {@code OMSImpl}, synchronization or separate instances per thread should be used. - * - *

Usage Example:

- * - *
{@code
- * OMSOut omsOut = new OMSOutImplementation();
- * OMSIn oms = new OMSImpl(omsOut);
- *
- * NewOrderSingle newOrder = new NewOrderSingle()
- *     .sender("ClientA")
- *     .target("OMS")
- *     .clOrdID("Order123")
- *     .symbol("AAPL")
- *     .orderQty(100)
- *     .price(150.00)
- *     .side(Side.BUY)
- *     .ordType(OrderType.MARKET)
- *     .transactTime(System.currentTimeMillis());
- *
- * oms.newOrderSingle(newOrder);
- * }
- */ -public class OMSImpl implements OMSIn { - // The outbound interface for sending execution reports and order cancel rejections - private final OMSOut out; - - // Reusable instance of ExecutionReport for creating new orders - private final ExecutionReport er = new ExecutionReport(); - - // Reusable instance of OrderCancelReject for cancelling orders - private final OrderCancelReject ocr = new OrderCancelReject(); - - /** - * Constructs a new {@code OMSImpl} with a given outbound interface. - * - * @param out the outbound interface to be used for sending responses - */ - public OMSImpl(OMSOut out) { - this.out = out; - } - - /** - * Processes a new single order request. The request's details are populated into an execution report and sent out. - * - * @param nos the {@link NewOrderSingle} request to process - */ - @Override - public void newOrderSingle(NewOrderSingle nos) { - // Reset the execution report DTO - er.reset(); - final long orderID = SystemTimeProvider.CLOCK.currentTimeNanos(); // Generate unique order ID - - // Populate the ExecutionReport with request details - er.sender(nos.target()) - .target(nos.sender()) - .symbol(nos.symbol()) - .clOrdID(nos.clOrdID()) - .ordType(nos.ordType()) - .orderQty(nos.orderQty()) - .price(nos.price()) - .side(nos.side()) - .sendingTime(nos.sendingTime()) - .transactTime(nos.transactTime()) - .leavesQty(0) - .cumQty(0) - .avgPx(0) - .orderID(orderID) - .text("Not ready"); - - // Send execution report - out.executionReport(er); - } - - /** - * Processes a cancel order request. The request's details are populated into an order cancel rejection and sent out. - * - * @param cor the {@link CancelOrderRequest} request to process - */ - @Override - public void cancelOrderRequest(CancelOrderRequest cor) { - // Reset the reusable OrderCancelReject instance - ocr.reset(); - // Populate OrderCancelReject with request details - ocr.sender(cor.target()) - .target(cor.sender()) - .symbol(cor.symbol()) - .clOrdID(cor.clOrdID()) - .sendingTime(cor.sendingTime()) - .reason("No such order"); - - // Send order cancellation rejection - out.orderCancelReject(ocr); - } - - /** - * Processes a cancel all orders request. The request's details are populated into an order cancel rejection and sent out. - * - * @param cancelAll the {@link CancelAll} request to process - */ - @Override - public void cancelAll(CancelAll cancelAll) { - // Reset the reusable OrderCancelReject instance - ocr.reset(); - // Populate OrderCancelReject with request details - ocr.sender(cancelAll.target()) - .target(cancelAll.sender()) - .symbol(cancelAll.symbol()) - .clOrdID(cancelAll.clOrdID()) - .sendingTime(cancelAll.sendingTime()) - .reason("No orders to cancel"); - - // Send order cancellation rejection - out.orderCancelReject(ocr); - } -} -.... - -Lines 121, Blanks 13, Tokens 1041 - -== File: src\main\java\town\lost\oms\OrderAdderMain.java -.... -/* - */ - -package town.lost.oms; - -import net.openhft.chronicle.core.pool.ClassAliasPool; -import net.openhft.chronicle.core.time.SystemTimeProvider; -import net.openhft.chronicle.core.util.Mocker; -import net.openhft.chronicle.queue.ChronicleQueue; -import net.openhft.chronicle.queue.ExcerptAppender; -import net.openhft.chronicle.queue.impl.single.SingleChronicleQueueBuilder; -import net.openhft.chronicle.queue.rollcycles.TestRollCycles; -import net.openhft.chronicle.wire.converter.ShortText; -import town.lost.oms.api.OMSIn; -import town.lost.oms.dto.*; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; - -/** - * The {@code OrderAdderMain} class is a utility application that allows users to add new orders to the Order Management System (OMS). - * - *

This class connects to a Chronicle Queue and writes {@link NewOrderSingle} messages to it. - * Users can interactively add orders by hitting the Enter key. The application will prompt the user - * and continue to accept orders until any non-empty input is entered. - */ -public class OrderAdderMain { - - /** - * The entry point of the application. - * - * @param args the input arguments (none expected) - */ - public static void main(String[] args) throws IOException { - // Add NewOrderSingle class to the alias pool - ClassAliasPool.CLASS_ALIASES.addAlias(NewOrderSingle.class); - - // Establish connection with the queue - try (ChronicleQueue q = SingleChronicleQueueBuilder.binary("in") - .rollCycle(TestRollCycles.TEST8_DAILY) - .build(); - ExcerptAppender appender = q.createAppender()) { - - // Acquire the appender and write methods for OMSIn - OMSIn in = appender.methodWriter(OMSIn.class); - - // Create a logging mock for OMSIn - OMSIn in2 = Mocker.logging(OMSIn.class, "in - ", System.out); - - // Create a new order single - NewOrderSingle nos = new NewOrderSingle() - .sender(fromShortText("sender")) - .target(fromShortText("target")) - .transactTime(now()) - .sendingTime(now()) - .account(1) - .timeInForce(TimeInForce.GTC) - .currency(Ccy.USD) - .orderQty(1) - .ordType(OrderType.MARKET) - .side(Side.BUY) - .symbol(fromShortText("EURUSD")); - - // Inform the user to add an order - System.out.println("\nHit blank line to add an order, anything else to exit"); - - // Initialize a BufferedReader to read user input - BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); - int orderCounter = 0; - while ("".equals(br.readLine())) { - // For each blank line read, add a new order - nos.clOrdID(Long.toString(orderCounter++)); - - // Send the new order - in.newOrderSingle(nos); - in2.newOrderSingle(nos); - } - } - // Inform the user that the program has finished running - System.out.println("Finished"); - - // Exit the program - System.exit(0); - } - - /** - * Returns the current system time in nanoseconds. - * - * @return the current time in nanoseconds - */ - static long now() { - return SystemTimeProvider.INSTANCE.currentTimeNanos(); - } - - /** - * Converts a string to a long using base85 encoding. - * - * @param s the string to convert - * @return the long representation of the string - */ - static long fromShortText(String s) { - return ShortText.INSTANCE.parse(s); - } -} -.... - -Lines 90, Blanks 15, Tokens 822 - -== File: src\main\java\town\lost\oms\OrderViewerMain.java -.... -/* - */ - -package town.lost.oms; - -import net.openhft.chronicle.bytes.MethodReader; -import net.openhft.chronicle.core.Jvm; -import net.openhft.chronicle.core.util.Mocker; -import net.openhft.chronicle.queue.ChronicleQueue; -import net.openhft.chronicle.queue.impl.single.SingleChronicleQueueBuilder; -import net.openhft.chronicle.queue.rollcycles.TestRollCycles; -import town.lost.oms.api.OMSIn; - -/** - * The {@code OrderViewerMain} class is a utility application that continuously reads and displays - * orders from a Chronicle Queue. It acts as a simple viewer for incoming orders, logging them to - * the console. - */ -public class OrderViewerMain { - - /** - * The entry point of the application. - * - * @param args the input arguments (none expected) - */ - public static void main(String[] args) { - // Inform the user that the program is waiting for messages - System.out.println("\nWaiting for messages..."); - - // Establish connection with the queue - try (ChronicleQueue q = SingleChronicleQueueBuilder.binary("in") - .rollCycle(TestRollCycles.TEST8_DAILY) - .build()) { - - // Create a logging mock for OMSIn - OMSIn logging = Mocker.logging(OMSIn.class, "read - ", System.out); - - // Create a MethodReader from the tail of the queue - MethodReader reader = q.createTailer().methodReader(logging); - - // Continuously read messages from the queue - while (true) { - // Read one message from the queue; pause if no message was read - if (!reader.readOne()) { - Jvm.pause(50); - } - } - } - } -} -.... - -Lines 42, Blanks 8, Tokens 378 - -== File: src\test\java\town\lost\oms\OMSImplTest.java -.... -/* - */ - -package town.lost.oms; - -import net.openhft.chronicle.core.time.SetTimeProvider; -import net.openhft.chronicle.core.time.SystemTimeProvider; -import org.junit.After; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; -import town.lost.oms.api.OMSOut; - -import java.util.Arrays; -import java.util.List; -import java.util.concurrent.TimeUnit; - -import static org.junit.Assert.assertEquals; - -/** - * Test class for OMSImpl. - * The OMSImplTest runs tests for each method in OMSImpl class. - * The test data is read from specified files and the actual output is compared against expected output. - */ -@SuppressWarnings("deprecation") -@RunWith(Parameterized.class) -public class OMSImplTest { - // Defines the paths to the tests to run. - static final List paths = Arrays.asList(new String[]{ - "newOrderSingle", - "newOrderSingleEquity", - "cancelOrderRequest", - "cancelAll" - }); - - // The name of the test, and the tester that will run the test. - final String name; - final net.openhft.chronicle.wire.utils.YamlTester tester; - - // Constructor that sets the name and tester. - public OMSImplTest(String name, net.openhft.chronicle.wire.utils.YamlTester tester) { - this.name = name; - this.tester = tester; - } - - // Defines the parameters for the parameterized test runner. - @Parameterized.Parameters(name = "{0}") - public static List parameters() { - // Returns a list of test parameters to run the tests with. - // Each test will be run with an instance of AccountManagerImpl, - // and will be subjected to various agitations to ensure robustness. - return new net.openhft.chronicle.wire.utils.YamlTesterParametersBuilder<>(out -> new OMSImpl(out), OMSOut.class, paths) - .agitators( - net.openhft.chronicle.wire.utils.YamlAgitator.messageMissing(), - net.openhft.chronicle.wire.utils.YamlAgitator.duplicateMessage(), - net.openhft.chronicle.wire.utils.YamlAgitator.overrideFields("sendingTime: '', symbol: '', side: '', orderQty: NaN, orderQty: -1, price: NaN, price: -1, clOrdId: '', ordType: ''".split(", *")), - net.openhft.chronicle.wire.utils.YamlAgitator.missingFields("sender, target, sendingTime, symbol, transactTime, account, orderQty, price, side, clOrdID, ordType, timeInForce, currency".split(", *"))) - .exceptionHandlerFunction(out -> (log, msg, thrown) -> out.jvmError(thrown == null ? msg : (msg + " " + thrown))) - .exceptionHandlerFunctionAndLog(true) - .get(); - } - - // After each test, this method resets the system time provider. - @After - public void tearDown() { - SystemTimeProvider.CLOCK = SystemTimeProvider.INSTANCE; - } - - // This is the actual test method, which uses the provided tester - // to run the test and then compares the expected output to the actual output. - @Test - public void runTester() { - // Sets the system clock to a specific time for the purpose of testing. - SystemTimeProvider.CLOCK = new SetTimeProvider("2019-12-03T09:54:37.345679") - .autoIncrement(1, TimeUnit.SECONDS); - // Asserts that the expected output matches the actual output. - assertEquals(tester.expected(), tester.actual()); - } -} -.... - -Lines 69, Blanks 10, Tokens 791 - -== File: src\test\resources\cancelAll\in.yaml -.... ---- -# This is a test for the CancelOrderRequest operation. -# The CLIENT1 (sender) is sending a request to the Order Management System (OMS1, the target) -# to cancel an order that was previously placed. The order is identified by its unique ID (clOrdID). - -cancelOrderRequest: { - sender: CLIENT1, - target: OMS1, - sendingTime: 2019-12-03T09:54:37.134475, - symbol: EURUSD, - account: ACC-12345, - clOrdID: dkj4378fwh, - origClOrdID: ABC1234567, - side: BUY -} -... ---- -# This is a test for the CancelAll operation. -# The CLIENT1 (sender) sends a request to the OMS1 (target) to cancel all orders. -cancelAll: { - sender: CLIENT1, - target: OMS1, - sendingTime: 2019-12-03T09:54:37.134475, - symbol: EURUSD, - clOrdID: CANCEL_ALL, -} -... -.... - -Lines 26, Blanks 1, Tokens 229 - -== File: src\test\resources\cancelAll\out.yaml -.... -# This is a test for the CancelOrderRequest operation. -# The CLIENT1 (sender) is sending a request to the Order Management System (OMS1, the target) -# to cancel an order that was previously placed. The order is identified by its unique ID (clOrdID). ---- -orderCancelReject: { - sender: OMS1, - target: CLIENT1, - sendingTime: 2019-12-03T09:54:37.134475, - symbol: EURUSD, - clOrdID: dkj4378fwh, - reason: No such order -} -... -# This is a test for the CancelAll operation. -# The CLIENT1 (sender) sends a request to the OMS1 (target) to cancel all orders. ---- -orderCancelReject: { - sender: OMS1, - target: CLIENT1, - sendingTime: 2019-12-03T09:54:37.134475, - symbol: EURUSD, - clOrdID: CANCEL_ALL, - reason: No orders to cancel -} -... -.... - -Lines 25, Blanks 0, Tokens 220 - -== File: src\test\resources\cancelOrderRequest\in.yaml -.... ---- -# This is a test for the CancelOrderRequest operation. -# The CLIENT1 (sender) is sending a request to the Order Management System (OMS1, the target) -# to cancel an order that was previously placed. The order is identified by its unique ID (clOrdID). - -cancelOrderRequest: { - sender: CLIENT1, - target: OMS1, - sendingTime: 2019-12-03T09:54:37.134475, - symbol: EURUSD, - account: ACC-12345, - clOrdID: dkj4378fwh, - origClOrdID: ABC1234567, - side: BUY -} -... -.... - -Lines 15, Blanks 1, Tokens 142 - -== File: src\test\resources\cancelOrderRequest\out.yaml -.... -# This is a test for the CancelOrderRequest operation. -# The CLIENT1 (sender) is sending a request to the Order Management System (OMS1, the target) -# to cancel an order that was previously placed. The order is identified by its unique ID (clOrdID). ---- -orderCancelReject: { - sender: OMS1, - target: CLIENT1, - sendingTime: 2019-12-03T09:54:37.134475, - symbol: EURUSD, - clOrdID: dkj4378fwh, - reason: No such order -} -... -.... - -Lines 13, Blanks 0, Tokens 124 - -== File: src\test\resources\newOrderSingle\in.yaml -.... ---- -# This is a test for the NewOrderSingle operation. -# The client (sender) is sending a new order to the Order Management System (OMS, the target). -# The order details such as the trading symbol, order quantity, price, and the order type are specified. - -newOrderSingle: { - sender: CLIENT, - target: OMS, - sendingTime: 2019-12-03T09:54:37.134475, - symbol: EURUSD, - transactTime: 2019-12-03T09:54:37.344751, - account: ACC-12345, - orderQty: 10E6, - price: 1.2123, - side: BUY, - clOrdID: dkj4378fwh, - ordType: LIMIT, - timeInForce: GTC, - currency: USD -} -... - -.... - -Lines 20, Blanks 2, Tokens 186 - -== File: src\test\resources\newOrderSingle\out.yaml -.... -# This is a test for the NewOrderSingle operation. -# The client (sender) is sending a new order to the Order Management System (OMS, the target). -# The order details such as the trading symbol, order quantity, price, and the order type are specified. ---- -executionReport: { - sender: OMS, - target: CLIENT, - sendingTime: 2019-12-03T09:54:37.134475, - symbol: EURUSD, - transactTime: 2019-12-03T09:54:37.344751, - orderQty: 10E6, - price: 1.2123, - orderID: 2019-12-03T09:54:37.345679, - lastPx: 0.0, - leavesQty: 0.0, - cumQty: 0.0, - avgPx: 0.0, - side: BUY, - ordType: LIMIT, - clOrdID: dkj4378fwh, - text: Not ready -} -... -.... - -Lines 23, Blanks 0, Tokens 225 - -== File: src\test\resources\newOrderSingleEquity\in.yaml -.... ---- -# Sample `NewOrderSingle` message representing a client's new order sent to the Order Management System (OMS). -newOrderSingle: { - sender: CLIENT1, - target: OMS1, - sendingTime: 2023-10-20T14:25:37.134475Z, - symbol: AAPL, - transactTime: 2023-10-20T14:25:37.134475Z, - account: ACC-12345, - orderQty: 1000, - price: 150.25, - side: buy, - clOrdID: ABC1234567, - ordType: LIMIT, - timeInForce: DAY, - currency: USD -} -... ---- -# Sample `NewOrderSingle` message representing another client's new order sent to the Order Management System (OMS). -newOrderSingle: { - sender: CLIENT2, - target: OMS1, - sendingTime: 2023-10-20T14:27:15.987654Z, - symbol: GOOGL, - transactTime: 2023-10-20T14:27:15.987654Z, - account: ACC-67890, - orderQty: 500, - price: 2750.50, - side: sell, - clOrdID: XYZ7654321, - ordType: MARKET, - timeInForce: DAY, - currency: USD -} -... -.... - -Lines 36, Blanks 0, Tokens 299 - -== File: src\test\resources\newOrderSingleEquity\out.yaml -.... -# Sample `NewOrderSingle` message representing a client's new order sent to the Order Management System (OMS). ---- -executionReport: { - sender: OMS1, - target: CLIENT1, - sendingTime: 2023-10-20T14:25:37.134475, - symbol: AAPL, - transactTime: 2023-10-20T14:25:37.134475, - orderQty: 1E3, - price: 150.25, - orderID: 2019-12-03T09:54:37.345679, - lastPx: 0.0, - leavesQty: 0.0, - cumQty: 0.0, - avgPx: 0.0, - side: BUY, - ordType: LIMIT, - clOrdID: ABC1234567, - text: Not ready -} -... -# Sample `NewOrderSingle` message representing another client's new order sent to the Order Management System (OMS). ---- -executionReport: { - sender: OMS1, - target: CLIENT2, - sendingTime: 2023-10-20T14:27:15.987654, - symbol: GOOGL, - transactTime: 2023-10-20T14:27:15.987654, - orderQty: 500.0, - price: 2750.5, - orderID: 2019-12-03T09:54:38.345679, - lastPx: 0.0, - leavesQty: 0.0, - cumQty: 0.0, - avgPx: 0.0, - side: SELL, - ordType: MARKET, - clOrdID: XYZ7654321, - text: Not ready -} -... -.... - -Lines 42, Blanks 0, Tokens 382 - -== Summary == -Total Lines: 2833 -Total Blanks: 298 -Total Tokens: 25169 -Tokens/Line: 8.9 From fc3640bf65c08f02bba140813841480e351ee3d1 Mon Sep 17 00:00:00 2001 From: Peter Lawrey Date: Wed, 1 Jan 2025 09:53:03 +0000 Subject: [PATCH 12/31] Second Phase - Adding tests --- order-processor/aide/notes-on-using-aide.adoc | 19 +- order-processor/pom.xml | 6 + .../java/town/lost/oms/OMSBenchmarkMain.java | 2 +- .../java/town/lost/oms/dto/CancelAll.java | 2 +- .../java/town/lost/oms/dto/OrderType.java | 2 +- .../town/lost/oms/OMSImplEdgeCaseTest.java | 228 ++++++++++++++++++ .../test/java/town/lost/oms/OMSImplTest.java | 2 +- .../src/test/resources/cancelAll/in.yaml | 3 +- .../test/resources/cancelOrderRequest/in.yaml | 1 + .../src/test/resources/newOrderSingle/in.yaml | 2 +- .../resources/newOrderSingleEquity/in.yaml | 14 +- .../out-missing-field-side.yaml | 4 +- .../out-set-field-side=__.yaml | 4 +- 13 files changed, 267 insertions(+), 22 deletions(-) create mode 100644 order-processor/src/test/java/town/lost/oms/OMSImplEdgeCaseTest.java diff --git a/order-processor/aide/notes-on-using-aide.adoc b/order-processor/aide/notes-on-using-aide.adoc index b6fe6e0..448ae35 100644 --- a/order-processor/aide/notes-on-using-aide.adoc +++ b/order-processor/aide/notes-on-using-aide.adoc @@ -10,7 +10,8 @@ Keeping notes on how the project is progressing and what changes you have made i NOTE: This document can be a template for recording notes on your own project and for helping the AI suggest improvements, as it is also a source of information between phases. -== First Phase +== First Phase - Requirements + Create a skeleton for the project and see what the AI can generate for you. These are more a source of inspiration than a source of truth. NOTE: Using `o1` is usally better than `4o` as it is more likely to be understood by the AI and produce a better result. `4o` works better for converting images, or web pages to asciidoc. @@ -46,12 +47,18 @@ More YAML based tests can be added by including a new directory in the list of p Output written as `src/main/adoc/oms-api-requirements.adoc` -== Second Phase -Generate requirements docs for each events and the API +== Second Phase - Unit Tests + +Reran `AppDocumentApp` to create a new context.asciidoc file. + +- `suggest improvements to the YAML based tests` +* Consistent case for `sell`, `ordType`, and `clOrdID` in the YAML files. +* Consistent absense of timezone ie removed `Z` from timestamps. +* `Following suggestion 2.1, add a Scenario headings to all the YAML based in.yaml files` +** (Reran `AppDocumentApp` to create a new increment.asciidoc file so it has the changes I just made) +** Merged using `Compare with Clipboard` +- `generate a unit test for OMSImpl that covers edge cases` -- Update the requirements from the code and tests -- Update the tests from the requirements and code -- Update the code from the requirements and tests == Third Phase Review the project diff --git a/order-processor/pom.xml b/order-processor/pom.xml index 986ec8b..6354ca6 100644 --- a/order-processor/pom.xml +++ b/order-processor/pom.xml @@ -19,6 +19,12 @@ net.openhft jlbh + + + org.mockito + mockito-core + test + diff --git a/order-processor/src/main/java/town/lost/oms/OMSBenchmarkMain.java b/order-processor/src/main/java/town/lost/oms/OMSBenchmarkMain.java index c181cce..164ab8b 100644 --- a/order-processor/src/main/java/town/lost/oms/OMSBenchmarkMain.java +++ b/order-processor/src/main/java/town/lost/oms/OMSBenchmarkMain.java @@ -164,7 +164,7 @@ public MyJLBHTask(ChronicleQueue input) { nos = new NewOrderSingle() .sender(ShortText.parse("client")) .target(ShortText.parse("OMS")) - .clOrdID("clOrdId") + .clOrdID("clOrdID") .orderQty(1e6) .price(1.6) .symbol(ShortText.parse("AUDUSD")) diff --git a/order-processor/src/main/java/town/lost/oms/dto/CancelAll.java b/order-processor/src/main/java/town/lost/oms/dto/CancelAll.java index 46db993..99ef069 100755 --- a/order-processor/src/main/java/town/lost/oms/dto/CancelAll.java +++ b/order-processor/src/main/java/town/lost/oms/dto/CancelAll.java @@ -41,7 +41,7 @@ * *

Note that the {@code symbol} field is encoded using {@link ShortTextLongConverter} to minimize storage and transmission size. */ -public class CancelAll extends AbstractEvent { +public class CancelAll extends AbstractEvent { private static final int MASHALLABLE_VERSION = 1; // Symbol for which all orders are to be canceled. @ShortText diff --git a/order-processor/src/main/java/town/lost/oms/dto/OrderType.java b/order-processor/src/main/java/town/lost/oms/dto/OrderType.java index 2079a81..da9baf8 100644 --- a/order-processor/src/main/java/town/lost/oms/dto/OrderType.java +++ b/order-processor/src/main/java/town/lost/oms/dto/OrderType.java @@ -16,7 +16,7 @@ * NewOrderSingle nos = new NewOrderSingle() * .sender(ShortText.parse("client")) * .target(ShortText.parse("OMS")) - * .clOrdID("clOrdId") + * .clOrdID("clOrdID") * .orderQty(1e6) * .price(1.6) * .symbol(ShortText.parse("AUDUSD")) diff --git a/order-processor/src/test/java/town/lost/oms/OMSImplEdgeCaseTest.java b/order-processor/src/test/java/town/lost/oms/OMSImplEdgeCaseTest.java new file mode 100644 index 0000000..83de430 --- /dev/null +++ b/order-processor/src/test/java/town/lost/oms/OMSImplEdgeCaseTest.java @@ -0,0 +1,228 @@ +package town.lost.oms; + +import net.openhft.chronicle.core.time.SetTimeProvider; +import net.openhft.chronicle.core.time.SystemTimeProvider; +import net.openhft.chronicle.core.time.TimeProvider; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import town.lost.oms.api.OMSOut; +import town.lost.oms.dto.*; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +/** + * A set of unit tests covering potential edge cases for the OMSImpl class. + * Although OMSImpl typically relies on a framework to validate inputs, + * these tests demonstrate how to handle unexpected or invalid DTO values + * in the context of an event-driven system. + */ +public class OMSImplEdgeCaseTest { + + private OMSOut mockOut; + private OMSImpl oms; + + @Before + public void setUp() { + // Prepare a mock OMSOut so we can verify OMSImpl’s behavior. + mockOut = mock(OMSOut.class); + + // Create an instance of OMSImpl using the mock OMSOut. + oms = new OMSImpl(mockOut); + + // Optionally fix the system time so that orderID generation or timestamps are predictable. + TimeProvider fixedTimeProvider = new SetTimeProvider(1234567890L); // Some fixed nanosecond time + SystemTimeProvider.CLOCK = fixedTimeProvider; + } + + @After + public void tearDown() { + // Reset the system time provider to the default. + SystemTimeProvider.CLOCK = SystemTimeProvider.INSTANCE; + } + + /** + * Test how OMSImpl behaves if a NewOrderSingle has a negative quantity. + * In a real system, the framework might reject it before calling OMSImpl, + * but here we simulate a pass-through for edge-case testing. + */ + @Test + public void testNewOrderSingleWithNegativeQuantity() { + // Arrange: create an invalid DTO (negative orderQty). + NewOrderSingle nos = new NewOrderSingle() + .sender(1001L) + .target(2002L) + .symbol(3003L) + .orderQty(-100) // Negative quantity + .price(1.25) + .side(Side.BUY) + .clOrdID("NEGQTY") + .ordType(OrderType.LIMIT) + .sendingTime(1234567000L) + .transactTime(1234567001L); + + // Act: call OMSImpl + oms.newOrderSingle(nos); + + // Assert: we expect an ExecutionReport to be generated anyway, + // since OMSImpl doesn’t strictly do its own validation by default. + // But if you want to simulate a "reject" scenario, you'd adapt the code + // in OMSImpl or do something like this: + ArgumentCaptor erCaptor = ArgumentCaptor.forClass(ExecutionReport.class); + verify(mockOut).executionReport(erCaptor.capture()); + verifyNoMoreInteractions(mockOut); + + ExecutionReport er = erCaptor.getValue(); + assertEquals("NEGQTY", er.clOrdID()); + assertEquals(3003L, er.symbol()); + // Leaves/cumQty are set to 0 by default in OMSImpl + assertEquals(0.0, er.leavesQty(), 0.0001); + assertEquals(0.0, er.cumQty(), 0.0001); + // etc., check any relevant fields + } + + /** + * Test behavior if the price is zero or less, which is typically invalid. + * In a real production environment, you'd likely expect an OrderCancelReject. + */ + @Test + public void testNewOrderSingleWithZeroPrice() { + NewOrderSingle nos = new NewOrderSingle() + .sender(111L) + .target(222L) + .symbol(333L) + .orderQty(100) + .price(0.0) // Zero price + .side(Side.SELL) + .clOrdID("ZEROPRICE") + .ordType(OrderType.MARKET) + .sendingTime(999999L) + .transactTime(1000000L); + + oms.newOrderSingle(nos); + + // Verify an ExecutionReport was sent + ArgumentCaptor erCaptor = ArgumentCaptor.forClass(ExecutionReport.class); + verify(mockOut).executionReport(erCaptor.capture()); + verifyNoMoreInteractions(mockOut); + + ExecutionReport er = erCaptor.getValue(); + assertEquals("ZEROPRICE", er.clOrdID()); + assertEquals(0.0, er.price(), 0.0001); + } + + /** + * Test behavior if the mandatory 'side' field is null. + * Currently, OMSImpl does not do explicit checks—this test + * reveals that no rejection occurs unless you code it in. + */ + @Test + public void testNewOrderSingleWithMissingSide() { + NewOrderSingle nos = new NewOrderSingle() + .sender(123L) + .target(456L) + .symbol(789L) + .orderQty(100) + .price(1.23) + .clOrdID("NOSIDE") + .ordType(OrderType.LIMIT) + .sendingTime(5000L) + .transactTime(5001L); + // side is omitted (null) + + oms.newOrderSingle(nos); + + // By default, OMSImpl does not reject it: + ArgumentCaptor erCaptor = ArgumentCaptor.forClass(ExecutionReport.class); + verify(mockOut).executionReport(erCaptor.capture()); + } + + /** + * Test if cancelOrderRequest is called with a non-existent order ID. + * The reference OMSImpl simply responds with "No such order." + */ + @Test + public void testCancelOrderRequestForUnknownOrder() { + CancelOrderRequest cor = new CancelOrderRequest() + .sender(999L) + .target(888L) + .symbol(777L) + .account(12345L) + .clOrdID("NON_EXISTENT") + .origClOrdID("OLD_ID") + .side(Side.BUY) + .sendingTime(1234000L); + + oms.cancelOrderRequest(cor); + + // We expect an orderCancelReject + ArgumentCaptor ocrCaptor = ArgumentCaptor.forClass(OrderCancelReject.class); + verify(mockOut).orderCancelReject(ocrCaptor.capture()); + verifyNoMoreInteractions(mockOut); + + OrderCancelReject ocr = ocrCaptor.getValue(); + assertEquals("NON_EXISTENT", ocr.clOrdID()); + assertEquals("No such order", ocr.reason()); + } + + /** + * Test cancelAll when symbol is 0 or clOrdID is empty. + * By default, OMSImpl again rejects with "No orders to cancel." + */ + @Test + public void testCancelAllWithEmptySymbol() { + CancelAll ca = new CancelAll() + .sender(100L) + .target(200L) + .symbol(0) // Invalid symbol + .clOrdID("") // Empty client order ID + .sendingTime(5000L); + + oms.cancelAll(ca); + + // Expect "No orders to cancel" + ArgumentCaptor ocrCaptor = ArgumentCaptor.forClass(OrderCancelReject.class); + verify(mockOut).orderCancelReject(ocrCaptor.capture()); + verifyNoMoreInteractions(mockOut); + + OrderCancelReject ocr = ocrCaptor.getValue(); + assertEquals("", ocr.clOrdID()); + assertEquals("No orders to cancel", ocr.reason()); + } + + /** + * Demonstrates how an exception might be handled, even though OMSImpl + * does not currently throw any by default. + */ + @Test + public void testExceptionHandling() { + // Mock OMSOut to do something unexpected + doThrow(new RuntimeException("Simulated Crash")) + .when(mockOut) + .executionReport(any(ExecutionReport.class)); + + NewOrderSingle nos = new NewOrderSingle() + .sender(1) + .target(2) + .symbol(3) + .orderQty(100) + .price(123.45) + .side(Side.BUY) + .clOrdID("EXCEPTION") + .ordType(OrderType.MARKET) + .sendingTime(10) + .transactTime(11); + + // Try-catch block to demonstrate how we'd detect an exception inside the test + try { + oms.newOrderSingle(nos); + } catch (RuntimeException e) { + // We can verify jvmError was called, if OMSImpl had that logic directly. + // Or handle it here if we want. For now, just confirm the message. + assertEquals("Simulated Crash", e.getMessage()); + } + } +} diff --git a/order-processor/src/test/java/town/lost/oms/OMSImplTest.java b/order-processor/src/test/java/town/lost/oms/OMSImplTest.java index f6f69be..10d41f8 100644 --- a/order-processor/src/test/java/town/lost/oms/OMSImplTest.java +++ b/order-processor/src/test/java/town/lost/oms/OMSImplTest.java @@ -54,7 +54,7 @@ public static List parameters() { .agitators( net.openhft.chronicle.wire.utils.YamlAgitator.messageMissing(), net.openhft.chronicle.wire.utils.YamlAgitator.duplicateMessage(), - net.openhft.chronicle.wire.utils.YamlAgitator.overrideFields("sendingTime: '', symbol: '', side: '', orderQty: NaN, orderQty: -1, price: NaN, price: -1, clOrdId: '', ordType: ''".split(", *")), + net.openhft.chronicle.wire.utils.YamlAgitator.overrideFields("sendingTime: '', symbol: '', side: '', orderQty: NaN, orderQty: -1, price: NaN, price: -1, clOrdID: '', ordType: ''".split(", *")), net.openhft.chronicle.wire.utils.YamlAgitator.missingFields("sender, target, sendingTime, symbol, transactTime, account, orderQty, price, side, clOrdID, ordType, timeInForce, currency".split(", *"))) .exceptionHandlerFunction(out -> (log, msg, thrown) -> out.jvmError(thrown == null ? msg : (msg + " " + thrown))) .exceptionHandlerFunctionAndLog(true) diff --git a/order-processor/src/test/resources/cancelAll/in.yaml b/order-processor/src/test/resources/cancelAll/in.yaml index c650c9e..3a73299 100755 --- a/order-processor/src/test/resources/cancelAll/in.yaml +++ b/order-processor/src/test/resources/cancelAll/in.yaml @@ -1,4 +1,5 @@ --- +# Scenario: Cancel a single existing order # This is a test for the CancelOrderRequest operation. # The CLIENT1 (sender) is sending a request to the Order Management System (OMS1, the target) # to cancel an order that was previously placed. The order is identified by its unique ID (clOrdID). @@ -15,7 +16,7 @@ cancelOrderRequest: { } ... --- -# This is a test for the CancelAll operation. +# Scenario: Cancel all orders for EURUSD # The CLIENT1 (sender) sends a request to the OMS1 (target) to cancel all orders. cancelAll: { sender: CLIENT1, diff --git a/order-processor/src/test/resources/cancelOrderRequest/in.yaml b/order-processor/src/test/resources/cancelOrderRequest/in.yaml index 638b5c6..d9e987e 100644 --- a/order-processor/src/test/resources/cancelOrderRequest/in.yaml +++ b/order-processor/src/test/resources/cancelOrderRequest/in.yaml @@ -1,4 +1,5 @@ --- +# Scenario: Cancel a single existing order # This is a test for the CancelOrderRequest operation. # The CLIENT1 (sender) is sending a request to the Order Management System (OMS1, the target) # to cancel an order that was previously placed. The order is identified by its unique ID (clOrdID). diff --git a/order-processor/src/test/resources/newOrderSingle/in.yaml b/order-processor/src/test/resources/newOrderSingle/in.yaml index 7a6e3c0..39be933 100644 --- a/order-processor/src/test/resources/newOrderSingle/in.yaml +++ b/order-processor/src/test/resources/newOrderSingle/in.yaml @@ -1,4 +1,5 @@ --- +# Scenario: Basic NewOrderSingle for EURUSD # This is a test for the NewOrderSingle operation. # The client (sender) is sending a new order to the Order Management System (OMS, the target). # The order details such as the trading symbol, order quantity, price, and the order type are specified. @@ -19,4 +20,3 @@ newOrderSingle: { currency: USD } ... - diff --git a/order-processor/src/test/resources/newOrderSingleEquity/in.yaml b/order-processor/src/test/resources/newOrderSingleEquity/in.yaml index 0361aa4..bf47e82 100644 --- a/order-processor/src/test/resources/newOrderSingleEquity/in.yaml +++ b/order-processor/src/test/resources/newOrderSingleEquity/in.yaml @@ -1,15 +1,16 @@ --- +# Scenario: Submit a new AAPL equity order # Sample `NewOrderSingle` message representing a client's new order sent to the Order Management System (OMS). newOrderSingle: { sender: CLIENT1, target: OMS1, - sendingTime: 2023-10-20T14:25:37.134475Z, + sendingTime: 2023-10-20T14:25:37.134475, symbol: AAPL, - transactTime: 2023-10-20T14:25:37.134475Z, + transactTime: 2023-10-20T14:25:37.134475, account: ACC-12345, orderQty: 1000, price: 150.25, - side: buy, + side: BUY, clOrdID: ABC1234567, ordType: LIMIT, timeInForce: DAY, @@ -17,17 +18,18 @@ newOrderSingle: { } ... --- +# Scenario: Submit a new GOOGL equity order # Sample `NewOrderSingle` message representing another client's new order sent to the Order Management System (OMS). newOrderSingle: { sender: CLIENT2, target: OMS1, - sendingTime: 2023-10-20T14:27:15.987654Z, + sendingTime: 2023-10-20T14:27:15.987654, symbol: GOOGL, - transactTime: 2023-10-20T14:27:15.987654Z, + transactTime: 2023-10-20T14:27:15.987654, account: ACC-67890, orderQty: 500, price: 2750.50, - side: sell, + side: SELL, clOrdID: XYZ7654321, ordType: MARKET, timeInForce: DAY, diff --git a/order-processor/src/test/resources/newOrderSingleEquity/out-missing-field-side.yaml b/order-processor/src/test/resources/newOrderSingleEquity/out-missing-field-side.yaml index 276872c..6eadd75 100644 --- a/order-processor/src/test/resources/newOrderSingleEquity/out-missing-field-side.yaml +++ b/order-processor/src/test/resources/newOrderSingleEquity/out-missing-field-side.yaml @@ -1,8 +1,8 @@ # Sample `NewOrderSingle` message representing a client's new order sent to the Order Management System (OMS). -# missing side: buy, +# missing side: BUY, jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: side is required" ... # Sample `NewOrderSingle` message representing another client's new order sent to the Order Management System (OMS). -# missing side: sell, +# missing side: SELL, jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: side is required" ... diff --git a/order-processor/src/test/resources/newOrderSingleEquity/out-set-field-side=__.yaml b/order-processor/src/test/resources/newOrderSingleEquity/out-set-field-side=__.yaml index 9f4c0bd..e8f9ef6 100644 --- a/order-processor/src/test/resources/newOrderSingleEquity/out-set-field-side=__.yaml +++ b/order-processor/src/test/resources/newOrderSingleEquity/out-set-field-side=__.yaml @@ -1,8 +1,8 @@ # Sample `NewOrderSingle` message representing a client's new order sent to the Order Management System (OMS). -# override side: buy to side: '' +# override side: BUY to side: '' jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: side is required" ... # Sample `NewOrderSingle` message representing another client's new order sent to the Order Management System (OMS). -# override side: sell to side: '' +# override side: SELL to side: '' jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: side is required" ... From 1569cdfb558e57d41d05b72689b02ff6a50b8706 Mon Sep 17 00:00:00 2001 From: Peter Lawrey Date: Wed, 1 Jan 2025 11:02:49 +0000 Subject: [PATCH 13/31] Create test-requirements.adoc --- .../src/main/adoc/test-requirements.adoc | 150 ++++++++++++++++++ 1 file changed, 150 insertions(+) create mode 100644 order-processor/src/main/adoc/test-requirements.adoc diff --git a/order-processor/src/main/adoc/test-requirements.adoc b/order-processor/src/main/adoc/test-requirements.adoc new file mode 100644 index 0000000..b41b4b7 --- /dev/null +++ b/order-processor/src/main/adoc/test-requirements.adoc @@ -0,0 +1,150 @@ += Detailed Requirements for Unit Tests and YAML-Based Event-Driven Tests +:doctype: requirements +:toc: +:toclevels: 3 +:author: Your Team +:lang: en-GB + +== Introduction + +This document defines **how** we approach testing in this project and the **assumptions** made by the framework. It covers two main testing layers: + +1. **Unit Tests** for individual classes or components (e.g., `OMSImplEdgeCaseTest`). +2. **YAML-Based Event-Driven Tests** for end-to-end scenarios (often referred to as Behavior-Driven Development, or BDD-style tests). + +By following these guidelines, we ensure consistency, clarity, and good coverage for all key features and edge cases. + +== 1. Overall Testing Goals + +1. We aim for **80% or higher coverage** of business logic in critical classes like `OMSImpl`. +2. We must confirm that inbound events (such as `NewOrderSingle` or `CancelOrderRequest`) result in correct outbound messages (`ExecutionReport`, `OrderCancelReject`) or errors (`jvmError(...)`) under normal and exceptional conditions. +3. The tests must be easy to extend and maintain, reflecting changes in the system’s requirements. + +== 2. Assumptions Made by the Framework + +Several tasks are handled by the broader **event-driven framework**, not directly in the application code. Specifically: + +1. **Validation Calls**: + - The framework calls `dto.validate()` *before* handing an event (like `NewOrderSingle`) to `OMSImpl`. + - The framework calls `validate()` on any outbound message (e.g., `ExecutionReport`) before it is actually persisted to the outbound queue. + +2. **Single-Threaded Operation**: + - The system runs in a single-threaded loop for demonstration purposes. No concurrency issues are tested here. + +3. **Exception Handling**: + - If `OMSImpl` throws an unhandled exception, the framework intercepts it and calls `OMSOut.jvmError(...)`. + - This means the `OMSImpl` class itself does not usually handle or catch exceptions directly. + +4. **Queue Persistence**: + - All inbound and outbound events are persisted automatically by the framework (Chronicle Queue usage). + - Tests can rely on replay or tailers for verifying message flows. + +== 3. Unit Testing Requirements + +=== 3.1 Purpose + +Unit tests focus on **individual classes** (e.g., `OMSImpl`, utility classes, DTO logic) to ensure correctness in isolation. We do not require full queue-based flows in these tests; rather, we rely on mocks or stubs for any external interactions. + +=== 3.2 Key Requirements + +1. **Test Coverage of Edge Cases** + - Negative or zero `orderQty` + - Null or empty strings for mandatory fields + - Invalid enumerations (e.g., unknown `Side`) + - Large numeric values (boundary testing) + +2. **Mocking External Interfaces** + - Use a mock of `OMSOut` when testing `OMSImpl`. + - Capture or inspect the outbound messages (like `ExecutionReport`) to verify correctness. + +3. **Deterministic Timestamps** + - If `OMSImpl` or related classes generate timestamps or IDs based on time, tests must control or mock the clock (e.g., `SystemTimeProvider.CLOCK`) so the results are repeatable. + +4. **Consistent Error and Reject Logic** + - Where relevant, confirm that the correct method (`executionReport` vs. `orderCancelReject` vs. `jvmError`) is called under erroneous conditions. + +5. **No Assumed Validation** + - Even though the framework calls `validate()`, unit tests can optionally explore what happens if a `DTO` with invalid fields reaches `OMSImpl`. This ensures robust handling or clarifies the contract. + +=== 3.3 Example Tests + +- `OMSImplEdgeCaseTest` + - Confirms how `OMSImpl` processes or rejects invalid `NewOrderSingle`. + - Ensures that “no such order” paths produce `orderCancelReject`. + - Demonstrates correct behavior if exceptions occur in the outbound interface. + +== 4. YAML-Based Event-Driven Tests + +=== 4.1 Purpose and Structure + +These tests simulate **end-to-end** flows: +- *Given* some optional setup in `_setup.yaml` (if needed), +- *When* inbound events are described in `in.yaml`, +- *Then* the system is expected to produce matching events in `out.yaml`. + +They are typically used with a runner (e.g., `YamlTester`) that replays the inbound events through `OMSImpl` and verifies the generated output events line-by-line or field-by-field. + +=== 4.2 File Organization + +*Each test scenario* is typically contained in a directory, e.g.: +- `cancelOrderRequest/` + - `in.yaml` (describes the request events) + - `out.yaml` (describes the expected system responses) +- `newOrderSingle/` + - `in.yaml` + - `out.yaml` + +Additional subdirectories (e.g., `cancelAll`, `newOrderSingleEquity`) handle specialized scenarios. + +=== 4.3 Requirements for YAML Scenarios + +1. **Scenario Headings** + - Each event block in `in.yaml` and `out.yaml` should have a short heading comment describing what the test block does. + +2. **Consistent Field Names and Enum Cases** + - Maintain the same naming conventions (`side: BUY` vs. `SELL` in uppercase, `ordType: MARKET` vs. `LIMIT`). + +3. **Timestamps** + - Use consistent date/time formats (e.g., `yyyy-MM-dd'T'HH:mm:ss.SSSSSS`) and avoid timezones if not needed. + +4. **One-to-One Input/Output** + - Each event in `in.yaml` should correspond to exactly one or more events in `out.yaml` (depending on the scenario). + - The test harness compares these systematically to confirm correctness. + +5. **Edge Case Coverage** + - Some directories should highlight boundary or invalid data (like missing fields or negative numbers) to confirm the system returns correct `orderCancelReject` or calls `jvmError`. + +=== 4.4 Running YAML Tests + +1. **YamlTester** picks up each directory listed in `OMSImplTest` (see `paths = Arrays.asList("newOrderSingle", "cancelOrderRequest", ...)`). +2. **in.yaml** events are fed into a new instance of `OMSImpl`. +3. The results are captured and compared against the contents of `out.yaml`. +4. If there is any mismatch, the test fails. + +== 5. Additional Considerations + +=== 5.1 Single-Threaded Focus + +Because this is a demonstration with single-threaded assumptions, concurrency or multi-thread race conditions are out of scope for these tests. If concurrency were required, we would add specialized integration or load tests. + +=== 5.2 Maintaining Requirements Sync + +Whenever the code or the domain logic changes: +- Update or add new YAML scenarios demonstrating the revised behavior. +- Update unit tests with new edge cases or coverage. +- Reflect these changes in the AsciiDoc requirements, ensuring that tests remain a **living** specification. + +=== 5.3 Future Enhancements + +1. **Integration with Chronicle Services** + - For advanced failover or multi-threading, we may create new YAML tests that simulate node crashes or queue replication scenarios. + +2. **CI Pipeline** + - We can add steps to lint YAML for syntax errors and to run all tests automatically after each code commit. + +== 6. Summary + +This document outlines how to structure both **unit tests** and **YAML-based event-driven tests**, ensuring they align with the **framework assumptions** (validation, single-threaded design, exception interception). By maintaining consistent naming, timestamps, and enumerations, the tests remain clear and consistent with the domain logic. Each new feature or bug fix should be accompanied by **at least one** new YAML scenario or unit test to preserve coverage and maintain system quality. + +Save the file as something like test-requirements.adoc in your documentation directory so the entire team can reference it. This ensures everyone uses the same guidelines for creating or extending unit tests and YAML-based tests. + From f365c5f4f33f907042a8d759481f4a89832e53b6 Mon Sep 17 00:00:00 2001 From: Peter Lawrey Date: Wed, 1 Jan 2025 11:15:36 +0000 Subject: [PATCH 14/31] Second Phase - Update tests --- order-processor/aide/notes-on-using-aide.adoc | 18 +++++++++++++----- .../cancelAll/out-missing-field-clOrdID.yaml | 3 ++- .../cancelAll/out-set-field-clOrdID=__.yaml | 12 ++++++++++++ .../out-set-field-sendingTime=__.yaml | 2 +- .../src/test/resources/cancelAll/out.yaml | 3 ++- .../out-missing-field-side.yaml | 1 + .../test/resources/cancelOrderRequest/out.yaml | 1 + .../out-missing-field-account.yaml | 1 + .../out-missing-field-clOrdID.yaml | 1 + .../out-missing-field-currency.yaml | 1 + .../out-missing-field-ordType.yaml | 1 + .../out-missing-field-orderQty.yaml | 1 + .../out-missing-field-price.yaml | 1 + .../out-missing-field-sender.yaml | 1 + .../out-missing-field-sendingTime.yaml | 1 + .../newOrderSingle/out-missing-field-side.yaml | 1 + .../out-missing-field-symbol.yaml | 1 + .../out-missing-field-target.yaml | 1 + .../out-missing-field-timeInForce.yaml | 1 + .../out-missing-field-transactTime.yaml | 1 + .../out-set-field-clOrdID=__.yaml | 7 +++++++ .../out-set-field-ordType=__.yaml | 1 + .../out-set-field-orderQty=-1.yaml | 1 + .../out-set-field-orderQty=NaN.yaml | 1 + .../newOrderSingle/out-set-field-price=-1.yaml | 1 + .../out-set-field-price=NaN.yaml | 1 + .../out-set-field-sendingTime=__.yaml | 1 + .../newOrderSingle/out-set-field-side=__.yaml | 1 + .../out-set-field-symbol=__.yaml | 1 + .../src/test/resources/newOrderSingle/out.yaml | 1 + .../out-missing-field-account.yaml | 2 ++ .../out-missing-field-clOrdID.yaml | 2 ++ .../out-missing-field-ordType.yaml | 2 ++ .../out-missing-field-orderQty.yaml | 2 ++ .../out-missing-field-price.yaml | 2 ++ .../out-missing-field-sender.yaml | 2 ++ .../out-missing-field-sendingTime.yaml | 6 ++++-- .../out-missing-field-side.yaml | 2 ++ .../out-missing-field-symbol.yaml | 2 ++ .../out-missing-field-target.yaml | 2 ++ .../out-missing-field-timeInForce.yaml | 2 ++ .../out-missing-field-transactTime.yaml | 6 ++++-- .../out-missing-msg-0.yaml | 1 + .../out-msg-0-duplicated.yaml | 3 +++ .../out-newOrderSingleEquity.yaml | 4 ++++ .../out-set-field-clOrdID=__.yaml | 10 ++++++++++ .../out-set-field-ordType=__.yaml | 2 ++ .../out-set-field-orderQty=-1.yaml | 2 ++ .../out-set-field-orderQty=NaN.yaml | 2 ++ .../out-set-field-price=-1.yaml | 2 ++ .../out-set-field-price=NaN.yaml | 2 ++ .../out-set-field-sendingTime=__.yaml | 6 ++++-- .../out-set-field-side=__.yaml | 2 ++ .../out-set-field-symbol=__.yaml | 2 ++ .../resources/newOrderSingleEquity/out.yaml | 2 ++ 55 files changed, 127 insertions(+), 14 deletions(-) create mode 100644 order-processor/src/test/resources/cancelAll/out-set-field-clOrdID=__.yaml create mode 100644 order-processor/src/test/resources/newOrderSingle/out-set-field-clOrdID=__.yaml create mode 100644 order-processor/src/test/resources/newOrderSingleEquity/out-set-field-clOrdID=__.yaml diff --git a/order-processor/aide/notes-on-using-aide.adoc b/order-processor/aide/notes-on-using-aide.adoc index 448ae35..b7a6e69 100644 --- a/order-processor/aide/notes-on-using-aide.adoc +++ b/order-processor/aide/notes-on-using-aide.adoc @@ -5,14 +5,15 @@ :lang: en-GB :toc: - -Keeping notes on how the project is progressing and what changes you have made is helpful. This can help you understand the project better and make the necessary changes at the end to complete it. +Keeping notes on how the project is progressing and what changes you have made is helpful. +This can help you understand the project better and make the necessary changes at the end to complete it. NOTE: This document can be a template for recording notes on your own project and for helping the AI suggest improvements, as it is also a source of information between phases. == First Phase - Requirements -Create a skeleton for the project and see what the AI can generate for you. These are more a source of inspiration than a source of truth. +Create a skeleton for the project and see what the AI can generate for you. +These are more a source of inspiration than a source of truth. NOTE: Using `o1` is usally better than `4o` as it is more likely to be understood by the AI and produce a better result. `4o` works better for converting images, or web pages to asciidoc. @@ -58,23 +59,30 @@ Reran `AppDocumentApp` to create a new context.asciidoc file. ** (Reran `AppDocumentApp` to create a new increment.asciidoc file so it has the changes I just made) ** Merged using `Compare with Clipboard` - `generate a unit test for OMSImpl that covers edge cases` - +* Minor fixes to the tests to ensure they compile. +- Run tests with `-Dregress.tests` to update the out.yaml files. +* This created three new agitated tests after correcting the case of `clOrdID` in the YAML files. +- `Generate a details requirements asciidoc for unit tests and yaml based event driven tests including assumptions made by the framework` == Third Phase + Review the project - Look at more specific improvements - Make selective changes == Fourth Phase + Restructure the project to make it easier to work with - Refactor the requirements, code, and/or tests to make them easier for the AI to work with and understand. == Fifth Phase + Finalize the project - Review the changes made in the previous phases to ensure they are consistent and correct. - Make any necessary corrections or adjustments once you feel you aren't advancing. -NOTE: You must understand the project to make the necessary changes at the end to make it complete. If you don't understand something, ask for clarification or skip it and possibly return to it later. +NOTE: You must understand the project to make the necessary changes at the end to make it complete. +If you don't understand something, ask for clarification or skip it and possibly return to it later. diff --git a/order-processor/src/test/resources/cancelAll/out-missing-field-clOrdID.yaml b/order-processor/src/test/resources/cancelAll/out-missing-field-clOrdID.yaml index eeee4c2..9fb3840 100644 --- a/order-processor/src/test/resources/cancelAll/out-missing-field-clOrdID.yaml +++ b/order-processor/src/test/resources/cancelAll/out-missing-field-clOrdID.yaml @@ -1,10 +1,11 @@ +# Scenario: Cancel a single existing order # This is a test for the CancelOrderRequest operation. # The CLIENT1 (sender) is sending a request to the Order Management System (OMS1, the target) # to cancel an order that was previously placed. The order is identified by its unique ID (clOrdID). # missing clOrdID: dkj4378fwh, jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: clOrdID is required" ... -# This is a test for the CancelAll operation. +# Scenario: Cancel all orders for EURUSD # The CLIENT1 (sender) sends a request to the OMS1 (target) to cancel all orders. # missing clOrdID: CANCEL_ALL, jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: clOrdID is required" diff --git a/order-processor/src/test/resources/cancelAll/out-set-field-clOrdID=__.yaml b/order-processor/src/test/resources/cancelAll/out-set-field-clOrdID=__.yaml new file mode 100644 index 0000000..f87aa2f --- /dev/null +++ b/order-processor/src/test/resources/cancelAll/out-set-field-clOrdID=__.yaml @@ -0,0 +1,12 @@ +# Scenario: Cancel a single existing order +# This is a test for the CancelOrderRequest operation. +# The CLIENT1 (sender) is sending a request to the Order Management System (OMS1, the target) +# to cancel an order that was previously placed. The order is identified by its unique ID (clOrdID). +# override clOrdID: dkj4378fwh to clOrdID: '' +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: clOrdID is required" +... +# Scenario: Cancel all orders for EURUSD +# The CLIENT1 (sender) sends a request to the OMS1 (target) to cancel all orders. +# override clOrdID: CANCEL_ALL to clOrdID: '' +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: clOrdID is required" +... diff --git a/order-processor/src/test/resources/cancelAll/out-set-field-sendingTime=__.yaml b/order-processor/src/test/resources/cancelAll/out-set-field-sendingTime=__.yaml index e6210a9..fe6fb75 100644 --- a/order-processor/src/test/resources/cancelAll/out-set-field-sendingTime=__.yaml +++ b/order-processor/src/test/resources/cancelAll/out-set-field-sendingTime=__.yaml @@ -1,6 +1,6 @@ jvmError: "Unhandled Exception net.openhft.chronicle.core.io.InvalidMarshallableException: sendingTime is required" ... -# This is a test for the CancelAll operation. +# Scenario: Cancel all orders for EURUSD # The CLIENT1 (sender) sends a request to the OMS1 (target) to cancel all orders. # override sendingTime: 2019-12-03T09:54:37.134475 to sendingTime: '' jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: sendingTime is required" diff --git a/order-processor/src/test/resources/cancelAll/out.yaml b/order-processor/src/test/resources/cancelAll/out.yaml index 9f4907d..6fe4471 100755 --- a/order-processor/src/test/resources/cancelAll/out.yaml +++ b/order-processor/src/test/resources/cancelAll/out.yaml @@ -1,3 +1,4 @@ +# Scenario: Cancel a single existing order # This is a test for the CancelOrderRequest operation. # The CLIENT1 (sender) is sending a request to the Order Management System (OMS1, the target) # to cancel an order that was previously placed. The order is identified by its unique ID (clOrdID). @@ -11,7 +12,7 @@ orderCancelReject: { reason: No such order } ... -# This is a test for the CancelAll operation. +# Scenario: Cancel all orders for EURUSD # The CLIENT1 (sender) sends a request to the OMS1 (target) to cancel all orders. --- orderCancelReject: { diff --git a/order-processor/src/test/resources/cancelOrderRequest/out-missing-field-side.yaml b/order-processor/src/test/resources/cancelOrderRequest/out-missing-field-side.yaml index 01b568a..220f26d 100644 --- a/order-processor/src/test/resources/cancelOrderRequest/out-missing-field-side.yaml +++ b/order-processor/src/test/resources/cancelOrderRequest/out-missing-field-side.yaml @@ -1,3 +1,4 @@ +# Scenario: Cancel a single existing order # This is a test for the CancelOrderRequest operation. # The CLIENT1 (sender) is sending a request to the Order Management System (OMS1, the target) # to cancel an order that was previously placed. The order is identified by its unique ID (clOrdID). diff --git a/order-processor/src/test/resources/cancelOrderRequest/out.yaml b/order-processor/src/test/resources/cancelOrderRequest/out.yaml index 46ed97b..b15e719 100644 --- a/order-processor/src/test/resources/cancelOrderRequest/out.yaml +++ b/order-processor/src/test/resources/cancelOrderRequest/out.yaml @@ -1,3 +1,4 @@ +# Scenario: Cancel a single existing order # This is a test for the CancelOrderRequest operation. # The CLIENT1 (sender) is sending a request to the Order Management System (OMS1, the target) # to cancel an order that was previously placed. The order is identified by its unique ID (clOrdID). diff --git a/order-processor/src/test/resources/newOrderSingle/out-missing-field-account.yaml b/order-processor/src/test/resources/newOrderSingle/out-missing-field-account.yaml index 7f4cd65..eb41b2d 100644 --- a/order-processor/src/test/resources/newOrderSingle/out-missing-field-account.yaml +++ b/order-processor/src/test/resources/newOrderSingle/out-missing-field-account.yaml @@ -1,3 +1,4 @@ +# Scenario: Basic NewOrderSingle for EURUSD # This is a test for the NewOrderSingle operation. # The client (sender) is sending a new order to the Order Management System (OMS, the target). # The order details such as the trading symbol, order quantity, price, and the order type are specified. diff --git a/order-processor/src/test/resources/newOrderSingle/out-missing-field-clOrdID.yaml b/order-processor/src/test/resources/newOrderSingle/out-missing-field-clOrdID.yaml index 6151611..ba37d7f 100644 --- a/order-processor/src/test/resources/newOrderSingle/out-missing-field-clOrdID.yaml +++ b/order-processor/src/test/resources/newOrderSingle/out-missing-field-clOrdID.yaml @@ -1,3 +1,4 @@ +# Scenario: Basic NewOrderSingle for EURUSD # This is a test for the NewOrderSingle operation. # The client (sender) is sending a new order to the Order Management System (OMS, the target). # The order details such as the trading symbol, order quantity, price, and the order type are specified. diff --git a/order-processor/src/test/resources/newOrderSingle/out-missing-field-currency.yaml b/order-processor/src/test/resources/newOrderSingle/out-missing-field-currency.yaml index ea365b4..bd78c4f 100644 --- a/order-processor/src/test/resources/newOrderSingle/out-missing-field-currency.yaml +++ b/order-processor/src/test/resources/newOrderSingle/out-missing-field-currency.yaml @@ -1,3 +1,4 @@ +# Scenario: Basic NewOrderSingle for EURUSD # This is a test for the NewOrderSingle operation. # The client (sender) is sending a new order to the Order Management System (OMS, the target). # The order details such as the trading symbol, order quantity, price, and the order type are specified. diff --git a/order-processor/src/test/resources/newOrderSingle/out-missing-field-ordType.yaml b/order-processor/src/test/resources/newOrderSingle/out-missing-field-ordType.yaml index 66905e1..2a0aded 100644 --- a/order-processor/src/test/resources/newOrderSingle/out-missing-field-ordType.yaml +++ b/order-processor/src/test/resources/newOrderSingle/out-missing-field-ordType.yaml @@ -1,3 +1,4 @@ +# Scenario: Basic NewOrderSingle for EURUSD # This is a test for the NewOrderSingle operation. # The client (sender) is sending a new order to the Order Management System (OMS, the target). # The order details such as the trading symbol, order quantity, price, and the order type are specified. diff --git a/order-processor/src/test/resources/newOrderSingle/out-missing-field-orderQty.yaml b/order-processor/src/test/resources/newOrderSingle/out-missing-field-orderQty.yaml index 747ee03..bc904b5 100644 --- a/order-processor/src/test/resources/newOrderSingle/out-missing-field-orderQty.yaml +++ b/order-processor/src/test/resources/newOrderSingle/out-missing-field-orderQty.yaml @@ -1,3 +1,4 @@ +# Scenario: Basic NewOrderSingle for EURUSD # This is a test for the NewOrderSingle operation. # The client (sender) is sending a new order to the Order Management System (OMS, the target). # The order details such as the trading symbol, order quantity, price, and the order type are specified. diff --git a/order-processor/src/test/resources/newOrderSingle/out-missing-field-price.yaml b/order-processor/src/test/resources/newOrderSingle/out-missing-field-price.yaml index 7cf33c6..8145ef8 100644 --- a/order-processor/src/test/resources/newOrderSingle/out-missing-field-price.yaml +++ b/order-processor/src/test/resources/newOrderSingle/out-missing-field-price.yaml @@ -1,3 +1,4 @@ +# Scenario: Basic NewOrderSingle for EURUSD # This is a test for the NewOrderSingle operation. # The client (sender) is sending a new order to the Order Management System (OMS, the target). # The order details such as the trading symbol, order quantity, price, and the order type are specified. diff --git a/order-processor/src/test/resources/newOrderSingle/out-missing-field-sender.yaml b/order-processor/src/test/resources/newOrderSingle/out-missing-field-sender.yaml index 977ee29..da4ea75 100644 --- a/order-processor/src/test/resources/newOrderSingle/out-missing-field-sender.yaml +++ b/order-processor/src/test/resources/newOrderSingle/out-missing-field-sender.yaml @@ -1,3 +1,4 @@ +# Scenario: Basic NewOrderSingle for EURUSD # This is a test for the NewOrderSingle operation. # The client (sender) is sending a new order to the Order Management System (OMS, the target). # The order details such as the trading symbol, order quantity, price, and the order type are specified. diff --git a/order-processor/src/test/resources/newOrderSingle/out-missing-field-sendingTime.yaml b/order-processor/src/test/resources/newOrderSingle/out-missing-field-sendingTime.yaml index 19a9fec..f331fd4 100644 --- a/order-processor/src/test/resources/newOrderSingle/out-missing-field-sendingTime.yaml +++ b/order-processor/src/test/resources/newOrderSingle/out-missing-field-sendingTime.yaml @@ -1,3 +1,4 @@ +# Scenario: Basic NewOrderSingle for EURUSD # This is a test for the NewOrderSingle operation. # The client (sender) is sending a new order to the Order Management System (OMS, the target). # The order details such as the trading symbol, order quantity, price, and the order type are specified. diff --git a/order-processor/src/test/resources/newOrderSingle/out-missing-field-side.yaml b/order-processor/src/test/resources/newOrderSingle/out-missing-field-side.yaml index 343423c..55d3337 100644 --- a/order-processor/src/test/resources/newOrderSingle/out-missing-field-side.yaml +++ b/order-processor/src/test/resources/newOrderSingle/out-missing-field-side.yaml @@ -1,3 +1,4 @@ +# Scenario: Basic NewOrderSingle for EURUSD # This is a test for the NewOrderSingle operation. # The client (sender) is sending a new order to the Order Management System (OMS, the target). # The order details such as the trading symbol, order quantity, price, and the order type are specified. diff --git a/order-processor/src/test/resources/newOrderSingle/out-missing-field-symbol.yaml b/order-processor/src/test/resources/newOrderSingle/out-missing-field-symbol.yaml index 7ab22c1..fca05c0 100644 --- a/order-processor/src/test/resources/newOrderSingle/out-missing-field-symbol.yaml +++ b/order-processor/src/test/resources/newOrderSingle/out-missing-field-symbol.yaml @@ -1,3 +1,4 @@ +# Scenario: Basic NewOrderSingle for EURUSD # This is a test for the NewOrderSingle operation. # The client (sender) is sending a new order to the Order Management System (OMS, the target). # The order details such as the trading symbol, order quantity, price, and the order type are specified. diff --git a/order-processor/src/test/resources/newOrderSingle/out-missing-field-target.yaml b/order-processor/src/test/resources/newOrderSingle/out-missing-field-target.yaml index 2eabd5e..6a51124 100644 --- a/order-processor/src/test/resources/newOrderSingle/out-missing-field-target.yaml +++ b/order-processor/src/test/resources/newOrderSingle/out-missing-field-target.yaml @@ -1,3 +1,4 @@ +# Scenario: Basic NewOrderSingle for EURUSD # This is a test for the NewOrderSingle operation. # The client (sender) is sending a new order to the Order Management System (OMS, the target). # The order details such as the trading symbol, order quantity, price, and the order type are specified. diff --git a/order-processor/src/test/resources/newOrderSingle/out-missing-field-timeInForce.yaml b/order-processor/src/test/resources/newOrderSingle/out-missing-field-timeInForce.yaml index ebf52ff..4839b2c 100644 --- a/order-processor/src/test/resources/newOrderSingle/out-missing-field-timeInForce.yaml +++ b/order-processor/src/test/resources/newOrderSingle/out-missing-field-timeInForce.yaml @@ -1,3 +1,4 @@ +# Scenario: Basic NewOrderSingle for EURUSD # This is a test for the NewOrderSingle operation. # The client (sender) is sending a new order to the Order Management System (OMS, the target). # The order details such as the trading symbol, order quantity, price, and the order type are specified. diff --git a/order-processor/src/test/resources/newOrderSingle/out-missing-field-transactTime.yaml b/order-processor/src/test/resources/newOrderSingle/out-missing-field-transactTime.yaml index f441764..39ec491 100644 --- a/order-processor/src/test/resources/newOrderSingle/out-missing-field-transactTime.yaml +++ b/order-processor/src/test/resources/newOrderSingle/out-missing-field-transactTime.yaml @@ -1,3 +1,4 @@ +# Scenario: Basic NewOrderSingle for EURUSD # This is a test for the NewOrderSingle operation. # The client (sender) is sending a new order to the Order Management System (OMS, the target). # The order details such as the trading symbol, order quantity, price, and the order type are specified. diff --git a/order-processor/src/test/resources/newOrderSingle/out-set-field-clOrdID=__.yaml b/order-processor/src/test/resources/newOrderSingle/out-set-field-clOrdID=__.yaml new file mode 100644 index 0000000..1fc4964 --- /dev/null +++ b/order-processor/src/test/resources/newOrderSingle/out-set-field-clOrdID=__.yaml @@ -0,0 +1,7 @@ +# Scenario: Basic NewOrderSingle for EURUSD +# This is a test for the NewOrderSingle operation. +# The client (sender) is sending a new order to the Order Management System (OMS, the target). +# The order details such as the trading symbol, order quantity, price, and the order type are specified. +# override clOrdID: dkj4378fwh to clOrdID: '' +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: clOrdID is required" +... diff --git a/order-processor/src/test/resources/newOrderSingle/out-set-field-ordType=__.yaml b/order-processor/src/test/resources/newOrderSingle/out-set-field-ordType=__.yaml index 9fe24cb..4255be9 100644 --- a/order-processor/src/test/resources/newOrderSingle/out-set-field-ordType=__.yaml +++ b/order-processor/src/test/resources/newOrderSingle/out-set-field-ordType=__.yaml @@ -1,3 +1,4 @@ +# Scenario: Basic NewOrderSingle for EURUSD # This is a test for the NewOrderSingle operation. # The client (sender) is sending a new order to the Order Management System (OMS, the target). # The order details such as the trading symbol, order quantity, price, and the order type are specified. diff --git a/order-processor/src/test/resources/newOrderSingle/out-set-field-orderQty=-1.yaml b/order-processor/src/test/resources/newOrderSingle/out-set-field-orderQty=-1.yaml index d703438..750ca7d 100644 --- a/order-processor/src/test/resources/newOrderSingle/out-set-field-orderQty=-1.yaml +++ b/order-processor/src/test/resources/newOrderSingle/out-set-field-orderQty=-1.yaml @@ -1,3 +1,4 @@ +# Scenario: Basic NewOrderSingle for EURUSD # This is a test for the NewOrderSingle operation. # The client (sender) is sending a new order to the Order Management System (OMS, the target). # The order details such as the trading symbol, order quantity, price, and the order type are specified. diff --git a/order-processor/src/test/resources/newOrderSingle/out-set-field-orderQty=NaN.yaml b/order-processor/src/test/resources/newOrderSingle/out-set-field-orderQty=NaN.yaml index 4e0f246..191c946 100644 --- a/order-processor/src/test/resources/newOrderSingle/out-set-field-orderQty=NaN.yaml +++ b/order-processor/src/test/resources/newOrderSingle/out-set-field-orderQty=NaN.yaml @@ -1,3 +1,4 @@ +# Scenario: Basic NewOrderSingle for EURUSD # This is a test for the NewOrderSingle operation. # The client (sender) is sending a new order to the Order Management System (OMS, the target). # The order details such as the trading symbol, order quantity, price, and the order type are specified. diff --git a/order-processor/src/test/resources/newOrderSingle/out-set-field-price=-1.yaml b/order-processor/src/test/resources/newOrderSingle/out-set-field-price=-1.yaml index 420232c..51d398d 100644 --- a/order-processor/src/test/resources/newOrderSingle/out-set-field-price=-1.yaml +++ b/order-processor/src/test/resources/newOrderSingle/out-set-field-price=-1.yaml @@ -1,3 +1,4 @@ +# Scenario: Basic NewOrderSingle for EURUSD # This is a test for the NewOrderSingle operation. # The client (sender) is sending a new order to the Order Management System (OMS, the target). # The order details such as the trading symbol, order quantity, price, and the order type are specified. diff --git a/order-processor/src/test/resources/newOrderSingle/out-set-field-price=NaN.yaml b/order-processor/src/test/resources/newOrderSingle/out-set-field-price=NaN.yaml index 531d1ea..3d97260 100644 --- a/order-processor/src/test/resources/newOrderSingle/out-set-field-price=NaN.yaml +++ b/order-processor/src/test/resources/newOrderSingle/out-set-field-price=NaN.yaml @@ -1,3 +1,4 @@ +# Scenario: Basic NewOrderSingle for EURUSD # This is a test for the NewOrderSingle operation. # The client (sender) is sending a new order to the Order Management System (OMS, the target). # The order details such as the trading symbol, order quantity, price, and the order type are specified. diff --git a/order-processor/src/test/resources/newOrderSingle/out-set-field-sendingTime=__.yaml b/order-processor/src/test/resources/newOrderSingle/out-set-field-sendingTime=__.yaml index 66d7d03..bbd044d 100644 --- a/order-processor/src/test/resources/newOrderSingle/out-set-field-sendingTime=__.yaml +++ b/order-processor/src/test/resources/newOrderSingle/out-set-field-sendingTime=__.yaml @@ -1,3 +1,4 @@ +# Scenario: Basic NewOrderSingle for EURUSD # This is a test for the NewOrderSingle operation. # The client (sender) is sending a new order to the Order Management System (OMS, the target). # The order details such as the trading symbol, order quantity, price, and the order type are specified. diff --git a/order-processor/src/test/resources/newOrderSingle/out-set-field-side=__.yaml b/order-processor/src/test/resources/newOrderSingle/out-set-field-side=__.yaml index 3446f04..12d3eaf 100644 --- a/order-processor/src/test/resources/newOrderSingle/out-set-field-side=__.yaml +++ b/order-processor/src/test/resources/newOrderSingle/out-set-field-side=__.yaml @@ -1,3 +1,4 @@ +# Scenario: Basic NewOrderSingle for EURUSD # This is a test for the NewOrderSingle operation. # The client (sender) is sending a new order to the Order Management System (OMS, the target). # The order details such as the trading symbol, order quantity, price, and the order type are specified. diff --git a/order-processor/src/test/resources/newOrderSingle/out-set-field-symbol=__.yaml b/order-processor/src/test/resources/newOrderSingle/out-set-field-symbol=__.yaml index 4755ffe..e4f5876 100644 --- a/order-processor/src/test/resources/newOrderSingle/out-set-field-symbol=__.yaml +++ b/order-processor/src/test/resources/newOrderSingle/out-set-field-symbol=__.yaml @@ -1,3 +1,4 @@ +# Scenario: Basic NewOrderSingle for EURUSD # This is a test for the NewOrderSingle operation. # The client (sender) is sending a new order to the Order Management System (OMS, the target). # The order details such as the trading symbol, order quantity, price, and the order type are specified. diff --git a/order-processor/src/test/resources/newOrderSingle/out.yaml b/order-processor/src/test/resources/newOrderSingle/out.yaml index 3fd3e7c..4e229e0 100644 --- a/order-processor/src/test/resources/newOrderSingle/out.yaml +++ b/order-processor/src/test/resources/newOrderSingle/out.yaml @@ -1,3 +1,4 @@ +# Scenario: Basic NewOrderSingle for EURUSD # This is a test for the NewOrderSingle operation. # The client (sender) is sending a new order to the Order Management System (OMS, the target). # The order details such as the trading symbol, order quantity, price, and the order type are specified. diff --git a/order-processor/src/test/resources/newOrderSingleEquity/out-missing-field-account.yaml b/order-processor/src/test/resources/newOrderSingleEquity/out-missing-field-account.yaml index 7f6dad1..7e21111 100644 --- a/order-processor/src/test/resources/newOrderSingleEquity/out-missing-field-account.yaml +++ b/order-processor/src/test/resources/newOrderSingleEquity/out-missing-field-account.yaml @@ -1,7 +1,9 @@ +# Scenario: Submit a new AAPL equity order # Sample `NewOrderSingle` message representing a client's new order sent to the Order Management System (OMS). # missing account: ACC-12345, jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: account is required" ... +# Scenario: Submit a new GOOGL equity order # Sample `NewOrderSingle` message representing another client's new order sent to the Order Management System (OMS). # missing account: ACC-67890, jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: account is required" diff --git a/order-processor/src/test/resources/newOrderSingleEquity/out-missing-field-clOrdID.yaml b/order-processor/src/test/resources/newOrderSingleEquity/out-missing-field-clOrdID.yaml index 94d9869..5f0b911 100644 --- a/order-processor/src/test/resources/newOrderSingleEquity/out-missing-field-clOrdID.yaml +++ b/order-processor/src/test/resources/newOrderSingleEquity/out-missing-field-clOrdID.yaml @@ -1,7 +1,9 @@ +# Scenario: Submit a new AAPL equity order # Sample `NewOrderSingle` message representing a client's new order sent to the Order Management System (OMS). # missing clOrdID: ABC1234567, jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: clOrdID is required" ... +# Scenario: Submit a new GOOGL equity order # Sample `NewOrderSingle` message representing another client's new order sent to the Order Management System (OMS). # missing clOrdID: XYZ7654321, jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: clOrdID is required" diff --git a/order-processor/src/test/resources/newOrderSingleEquity/out-missing-field-ordType.yaml b/order-processor/src/test/resources/newOrderSingleEquity/out-missing-field-ordType.yaml index b982906..f32ecfe 100644 --- a/order-processor/src/test/resources/newOrderSingleEquity/out-missing-field-ordType.yaml +++ b/order-processor/src/test/resources/newOrderSingleEquity/out-missing-field-ordType.yaml @@ -1,7 +1,9 @@ +# Scenario: Submit a new AAPL equity order # Sample `NewOrderSingle` message representing a client's new order sent to the Order Management System (OMS). # missing ordType: LIMIT, jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: ordType is required" ... +# Scenario: Submit a new GOOGL equity order # Sample `NewOrderSingle` message representing another client's new order sent to the Order Management System (OMS). # missing ordType: MARKET, jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: ordType is required" diff --git a/order-processor/src/test/resources/newOrderSingleEquity/out-missing-field-orderQty.yaml b/order-processor/src/test/resources/newOrderSingleEquity/out-missing-field-orderQty.yaml index 89e95f0..20a0824 100644 --- a/order-processor/src/test/resources/newOrderSingleEquity/out-missing-field-orderQty.yaml +++ b/order-processor/src/test/resources/newOrderSingleEquity/out-missing-field-orderQty.yaml @@ -1,7 +1,9 @@ +# Scenario: Submit a new AAPL equity order # Sample `NewOrderSingle` message representing a client's new order sent to the Order Management System (OMS). # missing orderQty: 1000, jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: orderQty is required" ... +# Scenario: Submit a new GOOGL equity order # Sample `NewOrderSingle` message representing another client's new order sent to the Order Management System (OMS). # missing orderQty: 500, jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: orderQty is required" diff --git a/order-processor/src/test/resources/newOrderSingleEquity/out-missing-field-price.yaml b/order-processor/src/test/resources/newOrderSingleEquity/out-missing-field-price.yaml index 89962b9..58eee0e 100644 --- a/order-processor/src/test/resources/newOrderSingleEquity/out-missing-field-price.yaml +++ b/order-processor/src/test/resources/newOrderSingleEquity/out-missing-field-price.yaml @@ -1,7 +1,9 @@ +# Scenario: Submit a new AAPL equity order # Sample `NewOrderSingle` message representing a client's new order sent to the Order Management System (OMS). # missing price: 150.25, jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: price is invalid" ... +# Scenario: Submit a new GOOGL equity order # Sample `NewOrderSingle` message representing another client's new order sent to the Order Management System (OMS). # missing price: 2750.50, jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: price is invalid" diff --git a/order-processor/src/test/resources/newOrderSingleEquity/out-missing-field-sender.yaml b/order-processor/src/test/resources/newOrderSingleEquity/out-missing-field-sender.yaml index 04638c8..3168902 100644 --- a/order-processor/src/test/resources/newOrderSingleEquity/out-missing-field-sender.yaml +++ b/order-processor/src/test/resources/newOrderSingleEquity/out-missing-field-sender.yaml @@ -1,7 +1,9 @@ +# Scenario: Submit a new AAPL equity order # Sample `NewOrderSingle` message representing a client's new order sent to the Order Management System (OMS). # missing sender: CLIENT1, jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: sender is required" ... +# Scenario: Submit a new GOOGL equity order # Sample `NewOrderSingle` message representing another client's new order sent to the Order Management System (OMS). # missing sender: CLIENT2, jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: sender is required" diff --git a/order-processor/src/test/resources/newOrderSingleEquity/out-missing-field-sendingTime.yaml b/order-processor/src/test/resources/newOrderSingleEquity/out-missing-field-sendingTime.yaml index d1f1039..6611ff3 100644 --- a/order-processor/src/test/resources/newOrderSingleEquity/out-missing-field-sendingTime.yaml +++ b/order-processor/src/test/resources/newOrderSingleEquity/out-missing-field-sendingTime.yaml @@ -1,8 +1,10 @@ +# Scenario: Submit a new AAPL equity order # Sample `NewOrderSingle` message representing a client's new order sent to the Order Management System (OMS). -# missing sendingTime: 2023-10-20T14:25:37.134475Z, +# missing sendingTime: 2023-10-20T14:25:37.134475, jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: sendingTime is required" ... +# Scenario: Submit a new GOOGL equity order # Sample `NewOrderSingle` message representing another client's new order sent to the Order Management System (OMS). -# missing sendingTime: 2023-10-20T14:27:15.987654Z, +# missing sendingTime: 2023-10-20T14:27:15.987654, jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: sendingTime is required" ... diff --git a/order-processor/src/test/resources/newOrderSingleEquity/out-missing-field-side.yaml b/order-processor/src/test/resources/newOrderSingleEquity/out-missing-field-side.yaml index 6eadd75..df860d7 100644 --- a/order-processor/src/test/resources/newOrderSingleEquity/out-missing-field-side.yaml +++ b/order-processor/src/test/resources/newOrderSingleEquity/out-missing-field-side.yaml @@ -1,7 +1,9 @@ +# Scenario: Submit a new AAPL equity order # Sample `NewOrderSingle` message representing a client's new order sent to the Order Management System (OMS). # missing side: BUY, jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: side is required" ... +# Scenario: Submit a new GOOGL equity order # Sample `NewOrderSingle` message representing another client's new order sent to the Order Management System (OMS). # missing side: SELL, jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: side is required" diff --git a/order-processor/src/test/resources/newOrderSingleEquity/out-missing-field-symbol.yaml b/order-processor/src/test/resources/newOrderSingleEquity/out-missing-field-symbol.yaml index fb9e404..ea5301b 100644 --- a/order-processor/src/test/resources/newOrderSingleEquity/out-missing-field-symbol.yaml +++ b/order-processor/src/test/resources/newOrderSingleEquity/out-missing-field-symbol.yaml @@ -1,7 +1,9 @@ +# Scenario: Submit a new AAPL equity order # Sample `NewOrderSingle` message representing a client's new order sent to the Order Management System (OMS). # missing symbol: AAPL, jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: symbol is required" ... +# Scenario: Submit a new GOOGL equity order # Sample `NewOrderSingle` message representing another client's new order sent to the Order Management System (OMS). # missing symbol: GOOGL, jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: symbol is required" diff --git a/order-processor/src/test/resources/newOrderSingleEquity/out-missing-field-target.yaml b/order-processor/src/test/resources/newOrderSingleEquity/out-missing-field-target.yaml index a9e57a6..9bdab01 100644 --- a/order-processor/src/test/resources/newOrderSingleEquity/out-missing-field-target.yaml +++ b/order-processor/src/test/resources/newOrderSingleEquity/out-missing-field-target.yaml @@ -1,7 +1,9 @@ +# Scenario: Submit a new AAPL equity order # Sample `NewOrderSingle` message representing a client's new order sent to the Order Management System (OMS). # missing target: OMS1, jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: target is required" ... +# Scenario: Submit a new GOOGL equity order # Sample `NewOrderSingle` message representing another client's new order sent to the Order Management System (OMS). # missing target: OMS1, jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: target is required" diff --git a/order-processor/src/test/resources/newOrderSingleEquity/out-missing-field-timeInForce.yaml b/order-processor/src/test/resources/newOrderSingleEquity/out-missing-field-timeInForce.yaml index c6794ea..fd40326 100644 --- a/order-processor/src/test/resources/newOrderSingleEquity/out-missing-field-timeInForce.yaml +++ b/order-processor/src/test/resources/newOrderSingleEquity/out-missing-field-timeInForce.yaml @@ -1,7 +1,9 @@ +# Scenario: Submit a new AAPL equity order # Sample `NewOrderSingle` message representing a client's new order sent to the Order Management System (OMS). # missing timeInForce: DAY, jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: timeInForce is required" ... +# Scenario: Submit a new GOOGL equity order # Sample `NewOrderSingle` message representing another client's new order sent to the Order Management System (OMS). # missing timeInForce: DAY, jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: timeInForce is required" diff --git a/order-processor/src/test/resources/newOrderSingleEquity/out-missing-field-transactTime.yaml b/order-processor/src/test/resources/newOrderSingleEquity/out-missing-field-transactTime.yaml index 649cb69..e8dfedd 100644 --- a/order-processor/src/test/resources/newOrderSingleEquity/out-missing-field-transactTime.yaml +++ b/order-processor/src/test/resources/newOrderSingleEquity/out-missing-field-transactTime.yaml @@ -1,8 +1,10 @@ +# Scenario: Submit a new AAPL equity order # Sample `NewOrderSingle` message representing a client's new order sent to the Order Management System (OMS). -# missing transactTime: 2023-10-20T14:25:37.134475Z, +# missing transactTime: 2023-10-20T14:25:37.134475, jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: transactTime is required" ... +# Scenario: Submit a new GOOGL equity order # Sample `NewOrderSingle` message representing another client's new order sent to the Order Management System (OMS). -# missing transactTime: 2023-10-20T14:27:15.987654Z, +# missing transactTime: 2023-10-20T14:27:15.987654, jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: transactTime is required" ... diff --git a/order-processor/src/test/resources/newOrderSingleEquity/out-missing-msg-0.yaml b/order-processor/src/test/resources/newOrderSingleEquity/out-missing-msg-0.yaml index e7a9f30..036446a 100644 --- a/order-processor/src/test/resources/newOrderSingleEquity/out-missing-msg-0.yaml +++ b/order-processor/src/test/resources/newOrderSingleEquity/out-missing-msg-0.yaml @@ -1,4 +1,5 @@ # Missing message 0 +# Scenario: Submit a new GOOGL equity order # Sample `NewOrderSingle` message representing another client's new order sent to the Order Management System (OMS). --- executionReport: { diff --git a/order-processor/src/test/resources/newOrderSingleEquity/out-msg-0-duplicated.yaml b/order-processor/src/test/resources/newOrderSingleEquity/out-msg-0-duplicated.yaml index 506be95..6e781ca 100644 --- a/order-processor/src/test/resources/newOrderSingleEquity/out-msg-0-duplicated.yaml +++ b/order-processor/src/test/resources/newOrderSingleEquity/out-msg-0-duplicated.yaml @@ -1,3 +1,4 @@ +# Scenario: Submit a new AAPL equity order # Sample `NewOrderSingle` message representing a client's new order sent to the Order Management System (OMS). --- executionReport: { @@ -19,6 +20,7 @@ executionReport: { text: Not ready } ... +# Scenario: Submit a new GOOGL equity order # Sample `NewOrderSingle` message representing another client's new order sent to the Order Management System (OMS). --- executionReport: { @@ -40,6 +42,7 @@ executionReport: { text: Not ready } ... +# Scenario: Submit a new AAPL equity order # Sample `NewOrderSingle` message representing a client's new order sent to the Order Management System (OMS). --- executionReport: { diff --git a/order-processor/src/test/resources/newOrderSingleEquity/out-newOrderSingleEquity.yaml b/order-processor/src/test/resources/newOrderSingleEquity/out-newOrderSingleEquity.yaml index 03568c7..344334b 100644 --- a/order-processor/src/test/resources/newOrderSingleEquity/out-newOrderSingleEquity.yaml +++ b/order-processor/src/test/resources/newOrderSingleEquity/out-newOrderSingleEquity.yaml @@ -1,3 +1,4 @@ +# Scenario: Submit a new AAPL equity order # Sample `NewOrderSingle` message representing a client's new order sent to the Order Management System (OMS). --- executionReport: { @@ -19,6 +20,7 @@ executionReport: { text: Not ready } ... +# Scenario: Submit a new GOOGL equity order # Sample `NewOrderSingle` message representing another client's new order sent to the Order Management System (OMS). --- executionReport: { @@ -40,6 +42,7 @@ executionReport: { text: Not ready } ... +# Scenario: Submit a new AAPL equity order # Sample `NewOrderSingle` message representing a client's new order sent to the Order Management System (OMS). --- executionReport: { @@ -61,6 +64,7 @@ executionReport: { text: Not ready } ... +# Scenario: Submit a new GOOGL equity order # Sample `NewOrderSingle` message representing another client's new order sent to the Order Management System (OMS). --- executionReport: { diff --git a/order-processor/src/test/resources/newOrderSingleEquity/out-set-field-clOrdID=__.yaml b/order-processor/src/test/resources/newOrderSingleEquity/out-set-field-clOrdID=__.yaml new file mode 100644 index 0000000..0b1c9a1 --- /dev/null +++ b/order-processor/src/test/resources/newOrderSingleEquity/out-set-field-clOrdID=__.yaml @@ -0,0 +1,10 @@ +# Scenario: Submit a new AAPL equity order +# Sample `NewOrderSingle` message representing a client's new order sent to the Order Management System (OMS). +# override clOrdID: ABC1234567 to clOrdID: '' +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: clOrdID is required" +... +# Scenario: Submit a new GOOGL equity order +# Sample `NewOrderSingle` message representing another client's new order sent to the Order Management System (OMS). +# override clOrdID: XYZ7654321 to clOrdID: '' +jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: clOrdID is required" +... diff --git a/order-processor/src/test/resources/newOrderSingleEquity/out-set-field-ordType=__.yaml b/order-processor/src/test/resources/newOrderSingleEquity/out-set-field-ordType=__.yaml index 06693db..7934909 100644 --- a/order-processor/src/test/resources/newOrderSingleEquity/out-set-field-ordType=__.yaml +++ b/order-processor/src/test/resources/newOrderSingleEquity/out-set-field-ordType=__.yaml @@ -1,7 +1,9 @@ +# Scenario: Submit a new AAPL equity order # Sample `NewOrderSingle` message representing a client's new order sent to the Order Management System (OMS). # override ordType: LIMIT to ordType: '' jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: ordType is required" ... +# Scenario: Submit a new GOOGL equity order # Sample `NewOrderSingle` message representing another client's new order sent to the Order Management System (OMS). # override ordType: MARKET to ordType: '' jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: ordType is required" diff --git a/order-processor/src/test/resources/newOrderSingleEquity/out-set-field-orderQty=-1.yaml b/order-processor/src/test/resources/newOrderSingleEquity/out-set-field-orderQty=-1.yaml index cbdc31f..85034f2 100644 --- a/order-processor/src/test/resources/newOrderSingleEquity/out-set-field-orderQty=-1.yaml +++ b/order-processor/src/test/resources/newOrderSingleEquity/out-set-field-orderQty=-1.yaml @@ -1,7 +1,9 @@ +# Scenario: Submit a new AAPL equity order # Sample `NewOrderSingle` message representing a client's new order sent to the Order Management System (OMS). # override orderQty: 1000 to orderQty: -1 jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: orderQty is invalid" ... +# Scenario: Submit a new GOOGL equity order # Sample `NewOrderSingle` message representing another client's new order sent to the Order Management System (OMS). # override orderQty: 500 to orderQty: -1 jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: orderQty is invalid" diff --git a/order-processor/src/test/resources/newOrderSingleEquity/out-set-field-orderQty=NaN.yaml b/order-processor/src/test/resources/newOrderSingleEquity/out-set-field-orderQty=NaN.yaml index 6b5b99b..77a3449 100644 --- a/order-processor/src/test/resources/newOrderSingleEquity/out-set-field-orderQty=NaN.yaml +++ b/order-processor/src/test/resources/newOrderSingleEquity/out-set-field-orderQty=NaN.yaml @@ -1,7 +1,9 @@ +# Scenario: Submit a new AAPL equity order # Sample `NewOrderSingle` message representing a client's new order sent to the Order Management System (OMS). # override orderQty: 1000 to orderQty: NaN jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: orderQty is invalid" ... +# Scenario: Submit a new GOOGL equity order # Sample `NewOrderSingle` message representing another client's new order sent to the Order Management System (OMS). # override orderQty: 500 to orderQty: NaN jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: orderQty is invalid" diff --git a/order-processor/src/test/resources/newOrderSingleEquity/out-set-field-price=-1.yaml b/order-processor/src/test/resources/newOrderSingleEquity/out-set-field-price=-1.yaml index 3ee5b2c..7ffcbf2 100644 --- a/order-processor/src/test/resources/newOrderSingleEquity/out-set-field-price=-1.yaml +++ b/order-processor/src/test/resources/newOrderSingleEquity/out-set-field-price=-1.yaml @@ -1,7 +1,9 @@ +# Scenario: Submit a new AAPL equity order # Sample `NewOrderSingle` message representing a client's new order sent to the Order Management System (OMS). # override price: 150.25 to price: -1 jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: price is invalid" ... +# Scenario: Submit a new GOOGL equity order # Sample `NewOrderSingle` message representing another client's new order sent to the Order Management System (OMS). # override price: 2750.50 to price: -1 jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: price is invalid" diff --git a/order-processor/src/test/resources/newOrderSingleEquity/out-set-field-price=NaN.yaml b/order-processor/src/test/resources/newOrderSingleEquity/out-set-field-price=NaN.yaml index e4e14a6..78c6f71 100644 --- a/order-processor/src/test/resources/newOrderSingleEquity/out-set-field-price=NaN.yaml +++ b/order-processor/src/test/resources/newOrderSingleEquity/out-set-field-price=NaN.yaml @@ -1,7 +1,9 @@ +# Scenario: Submit a new AAPL equity order # Sample `NewOrderSingle` message representing a client's new order sent to the Order Management System (OMS). # override price: 150.25 to price: NaN jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: price is invalid" ... +# Scenario: Submit a new GOOGL equity order # Sample `NewOrderSingle` message representing another client's new order sent to the Order Management System (OMS). # override price: 2750.50 to price: NaN jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: price is invalid" diff --git a/order-processor/src/test/resources/newOrderSingleEquity/out-set-field-sendingTime=__.yaml b/order-processor/src/test/resources/newOrderSingleEquity/out-set-field-sendingTime=__.yaml index 07b71f4..8d464d5 100644 --- a/order-processor/src/test/resources/newOrderSingleEquity/out-set-field-sendingTime=__.yaml +++ b/order-processor/src/test/resources/newOrderSingleEquity/out-set-field-sendingTime=__.yaml @@ -1,8 +1,10 @@ +# Scenario: Submit a new AAPL equity order # Sample `NewOrderSingle` message representing a client's new order sent to the Order Management System (OMS). -# override sendingTime: 2023-10-20T14:25:37.134475Z to sendingTime: '' +# override sendingTime: 2023-10-20T14:25:37.134475 to sendingTime: '' jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: sendingTime is required" ... +# Scenario: Submit a new GOOGL equity order # Sample `NewOrderSingle` message representing another client's new order sent to the Order Management System (OMS). -# override sendingTime: 2023-10-20T14:27:15.987654Z to sendingTime: '' +# override sendingTime: 2023-10-20T14:27:15.987654 to sendingTime: '' jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: sendingTime is required" ... diff --git a/order-processor/src/test/resources/newOrderSingleEquity/out-set-field-side=__.yaml b/order-processor/src/test/resources/newOrderSingleEquity/out-set-field-side=__.yaml index e8f9ef6..800a295 100644 --- a/order-processor/src/test/resources/newOrderSingleEquity/out-set-field-side=__.yaml +++ b/order-processor/src/test/resources/newOrderSingleEquity/out-set-field-side=__.yaml @@ -1,7 +1,9 @@ +# Scenario: Submit a new AAPL equity order # Sample `NewOrderSingle` message representing a client's new order sent to the Order Management System (OMS). # override side: BUY to side: '' jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: side is required" ... +# Scenario: Submit a new GOOGL equity order # Sample `NewOrderSingle` message representing another client's new order sent to the Order Management System (OMS). # override side: SELL to side: '' jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: side is required" diff --git a/order-processor/src/test/resources/newOrderSingleEquity/out-set-field-symbol=__.yaml b/order-processor/src/test/resources/newOrderSingleEquity/out-set-field-symbol=__.yaml index c9cc17b..f899261 100644 --- a/order-processor/src/test/resources/newOrderSingleEquity/out-set-field-symbol=__.yaml +++ b/order-processor/src/test/resources/newOrderSingleEquity/out-set-field-symbol=__.yaml @@ -1,7 +1,9 @@ +# Scenario: Submit a new AAPL equity order # Sample `NewOrderSingle` message representing a client's new order sent to the Order Management System (OMS). # override symbol: AAPL to symbol: '' jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: symbol is required" ... +# Scenario: Submit a new GOOGL equity order # Sample `NewOrderSingle` message representing another client's new order sent to the Order Management System (OMS). # override symbol: GOOGL to symbol: '' jvmError: "net.openhft.chronicle.core.io.InvalidMarshallableException: symbol is required" diff --git a/order-processor/src/test/resources/newOrderSingleEquity/out.yaml b/order-processor/src/test/resources/newOrderSingleEquity/out.yaml index 251404a..626850d 100644 --- a/order-processor/src/test/resources/newOrderSingleEquity/out.yaml +++ b/order-processor/src/test/resources/newOrderSingleEquity/out.yaml @@ -1,3 +1,4 @@ +# Scenario: Submit a new AAPL equity order # Sample `NewOrderSingle` message representing a client's new order sent to the Order Management System (OMS). --- executionReport: { @@ -19,6 +20,7 @@ executionReport: { text: Not ready } ... +# Scenario: Submit a new GOOGL equity order # Sample `NewOrderSingle` message representing another client's new order sent to the Order Management System (OMS). --- executionReport: { From 18e16752bf934bccc0dddcfca0c8e0bf66b0896e Mon Sep 17 00:00:00 2001 From: Peter Lawrey Date: Wed, 1 Jan 2025 11:22:04 +0000 Subject: [PATCH 15/31] Second Phase - Update tests --- order-processor/aide/notes-on-using-aide.adoc | 2 ++ order-processor/src/main/adoc/test-requirements.adoc | 3 --- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/order-processor/aide/notes-on-using-aide.adoc b/order-processor/aide/notes-on-using-aide.adoc index b7a6e69..398ea9b 100644 --- a/order-processor/aide/notes-on-using-aide.adoc +++ b/order-processor/aide/notes-on-using-aide.adoc @@ -63,6 +63,8 @@ Reran `AppDocumentApp` to create a new context.asciidoc file. - Run tests with `-Dregress.tests` to update the out.yaml files. * This created three new agitated tests after correcting the case of `clOrdID` in the YAML files. - `Generate a details requirements asciidoc for unit tests and yaml based event driven tests including assumptions made by the framework` +- `improve and reduce duplication in src/main/adoc files` +* Ten reasonable suggestions to improve the requirements and test documents, but none compelling. == Third Phase diff --git a/order-processor/src/main/adoc/test-requirements.adoc b/order-processor/src/main/adoc/test-requirements.adoc index b41b4b7..38da585 100644 --- a/order-processor/src/main/adoc/test-requirements.adoc +++ b/order-processor/src/main/adoc/test-requirements.adoc @@ -145,6 +145,3 @@ Whenever the code or the domain logic changes: == 6. Summary This document outlines how to structure both **unit tests** and **YAML-based event-driven tests**, ensuring they align with the **framework assumptions** (validation, single-threaded design, exception interception). By maintaining consistent naming, timestamps, and enumerations, the tests remain clear and consistent with the domain logic. Each new feature or bug fix should be accompanied by **at least one** new YAML scenario or unit test to preserve coverage and maintain system quality. - -Save the file as something like test-requirements.adoc in your documentation directory so the entire team can reference it. This ensures everyone uses the same guidelines for creating or extending unit tests and YAML-based tests. - From 93fdb123f7839f7cf739285b7dc5e7ffbb129263 Mon Sep 17 00:00:00 2001 From: Peter Lawrey Date: Wed, 1 Jan 2025 11:23:59 +0000 Subject: [PATCH 16/31] TBD --- order-processor/aide/notes-on-using-aide.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/order-processor/aide/notes-on-using-aide.adoc b/order-processor/aide/notes-on-using-aide.adoc index 398ea9b..464bd96 100644 --- a/order-processor/aide/notes-on-using-aide.adoc +++ b/order-processor/aide/notes-on-using-aide.adoc @@ -66,7 +66,7 @@ Reran `AppDocumentApp` to create a new context.asciidoc file. - `improve and reduce duplication in src/main/adoc files` * Ten reasonable suggestions to improve the requirements and test documents, but none compelling. -== Third Phase +== Third Phase - TBD Review the project From accc4cfeb3572702649f9d842522af0b4e875f17 Mon Sep 17 00:00:00 2001 From: Peter Lawrey Date: Wed, 1 Jan 2025 20:01:37 +0000 Subject: [PATCH 17/31] Improve reference to the FIX spec --- order-processor/aide/notes-on-using-aide.adoc | 7 +- .../src/main/adoc/oms-api-requirements.adoc | 3 + .../src/main/adoc/selected-fix-4.2.adoc | 131 ++++++++++++++++++ .../main/java/town/lost/oms/api/OMSIn.java | 27 ++-- 4 files changed, 153 insertions(+), 15 deletions(-) create mode 100644 order-processor/src/main/adoc/selected-fix-4.2.adoc diff --git a/order-processor/aide/notes-on-using-aide.adoc b/order-processor/aide/notes-on-using-aide.adoc index 464bd96..83b89e7 100644 --- a/order-processor/aide/notes-on-using-aide.adoc +++ b/order-processor/aide/notes-on-using-aide.adoc @@ -66,12 +66,13 @@ Reran `AppDocumentApp` to create a new context.asciidoc file. - `improve and reduce duplication in src/main/adoc files` * Ten reasonable suggestions to improve the requirements and test documents, but none compelling. -== Third Phase - TBD +== Third Phase - Code Improvements Review the project -- Look at more specific improvements -- Make selective changes +- `Suggest improvements to the classes in town.lost.oms.api` +* +- `based on the FIX 4.2 specification, extract a description of messages in town.lost.oms.dto and their fields in asciidoc` == Fourth Phase diff --git a/order-processor/src/main/adoc/oms-api-requirements.adoc b/order-processor/src/main/adoc/oms-api-requirements.adoc index a2b903c..50a8fe3 100644 --- a/order-processor/src/main/adoc/oms-api-requirements.adoc +++ b/order-processor/src/main/adoc/oms-api-requirements.adoc @@ -8,6 +8,9 @@ This requirements document provides **sufficient detail to recreate** both the ` In this system, an **Order Management System (OMS)** receives inbound events (orders, cancellations) and emits outbound events (execution reports, reject messages). This design uses **Chronicle Queue** for persistence and asynchronous event-driven processing, with **Chronicle Wire** for serialization. +NOTE: All event messages in this system (e.g., `NewOrderSingle`, `CancelOrderRequest`, `ExecutionReport`) are conceptually derived from the Financial Information eXchange (FIX) Protocol, version 4.2. See https://www.fixtrading.org/standards/fix-4-2/ for more details on field definitions, message flow, and usage conventions in FIX-based trading systems. + + == Assumptions 1. **Single-Threaded**: This demo runs in a single-threaded context (e.g., one thread polling events from the queue). diff --git a/order-processor/src/main/adoc/selected-fix-4.2.adoc b/order-processor/src/main/adoc/selected-fix-4.2.adoc new file mode 100644 index 0000000..138ac63 --- /dev/null +++ b/order-processor/src/main/adoc/selected-fix-4.2.adoc @@ -0,0 +1,131 @@ += Selected FIX 4.2 Messages + +== NewOrderSingle (35=D) + +**Message Type**: D (New Order - Single) + +**DTO Class**: `town.lost.oms.dto.NewOrderSingle` + +The fields in `NewOrderSingle` commonly align with the following FIX tags: + +|=== +| DTO Field | Typical FIX Tag | Description +| `clOrdID` | 11 | Unique ID assigned by the client to identify the order. +| `symbol` | 55 | Ticker symbol or security identifier. +| `side` | 54 | Side of the order: 1 = Buy, 2 = Sell, etc. In your code, this is the `Side` enum. +| `orderQty` | 38 | Quantity of the order to be bought or sold. +| `price` | 44 | Price for Limit or other price-based orders. +| `timeInForce` | 59 | Time in Force instructions, e.g. 0 = Day, 1 = GTC, 3 = IOC, etc. Mapped to your `TimeInForce` enum. +| `currency` | 15 | Currency in which the order is denominated (e.g., USD). +| `account` | 1 | Account number or name under which the order is placed. +| `transactTime`| 60 | Time at which the transaction occurred. +| `sender` / `target` | (Custom) | In FIX, the `SenderCompID` (49) and `TargetCompID` (56) fields often track the sender/receiver. In your DTO, these are `long` fields for IDs, but they conceptually align with the notion of comp IDs. +|=== + +In FIX 4.2, a **New Order Single** is identified by **MsgType (35) = D**. The system typically uses this message to submit a new order to the market or execution venue. + +''' + +== CancelOrderRequest (35=F) + +**Message Type**: F (Order Cancel Request) + +**DTO Class**: `town.lost.oms.dto.CancelOrderRequest` + +`CancelOrderRequest` parallels the **Order Cancel Request** in FIX 4.2: + +|=== +| DTO Field | Typical FIX Tag | Description +| `clOrdID` | 11 | New (unique) client ID for this cancel request. +| `origClOrdID`| 41 | Original client ID of the order to be canceled (links this request to the original order). +| `symbol` | 55 | Symbol of the order to be canceled. +| `side` | 54 | Side of the order (buy or sell). +| `account` | 1 | Account under which the original order was placed. +| `sendingTime`| 52 | In FIX, *SendingTime* is tag 52. You may have a similar timestamp in `CancelOrderRequest` for logging. +| `transactTime`| 60 (optional) | Some versions include the transaction time for the cancel request. +| `sender` / `target` | (Custom) | Similar concept to `SenderCompID` (49) / `TargetCompID` (56). +|=== + +FIX 4.2 expects an **Order Cancel Request** to be answered by either an **Order Cancel Reject** (if not canceled) or an **ExecutionReport** (if partially or fully canceled). + +''' + +== CancelAll (Custom) + +**Message Type**: (Not standard in FIX 4.2) + +**DTO Class**: `town.lost.oms.dto.CancelAll` + +`CancelAll` does **not** have a direct equivalent in the FIX 4.2 standard. While FIX 4.4 and certain vendor extensions introduced mass-cancel or mass-action messages, they are not part of the official FIX 4.2 schema. + +|=== +| DTO Field | Custom / Extended Tag | Description +| `symbol` | Could reuse 55 | If you intend to mass-cancel by symbol, you might reuse `Symbol (55)`. +| `clOrdID` | 11 | In some custom implementations, a new unique ID for the mass cancel request. +| `sender` / `target` | (Custom) | Same concept of participants as in other messages (comp IDs in standard FIX). +| `sendingTime` | 52 (optional) | Could track when the mass cancel was requested. +|=== + +**Usage**: The system you’re building demonstrates how to handle mass-cancel requests. This is an **extension** beyond FIX 4.2’s default messages. + +''' + +== ExecutionReport (35=8) + +**Message Type**: 8 (Execution Report) + +**DTO Class**: `town.lost.oms.dto.ExecutionReport` + +In FIX 4.2, **ExecutionReport** is among the most common messages, used for order status, fills, partial fills, etc.: + +|=== +| DTO Field | Typical FIX Tag | Description +| `symbol` | 55 | Symbol or security identifier. +| `side` | 54 | Side of the order (1=Buy, 2=Sell). +| `orderQty` | 38 | Original quantity requested. +| `price` | 44 | Price associated with the order (especially for limit orders). +| `orderID` | 37 | Unique identifier for the order as assigned by the broker/exchange. +| `clOrdID` | 11 | The client’s original order ID if relevant. +| `lastPx` | 31 | Price of the last fill. +| `leavesQty` | 151 | Quantity remaining open (not filled yet). +| `cumQty` | 14 | Cumulative quantity filled so far. +| `avgPx` | 6 | Average fill price across all partial executions. +| `text` | 58 | Free-form text, e.g. reason for acceptance or comments. +| `transactTime` | 60 | Time of the transaction or fill. +| `sendingTime` | 52 (optional) | The time at which the ExecutionReport is being sent. +| `ordType` | 40 | Could reflect the original order type (e.g., Limit, Market). +| `sender` / `target` | (Custom) | Conceptually like `SenderCompID` (49) / `TargetCompID` (56). +|=== + +A single `ExecutionReport` can indicate **new**, **partial fill**, **fill**, **done for day**, **canceled**, or other statuses. The **ExecType (150)** and **OrdStatus (39)** fields in real FIX can reflect these states, but your DTO has not directly exposed them. + +''' + +== OrderCancelReject (35=9) + +**Message Type**: 9 (Order Cancel Reject) + +**DTO Class**: `town.lost.oms.dto.OrderCancelReject` + +Corresponding to the **Order Cancel Reject** message in FIX 4.2: + +|=== +| DTO Field | Typical FIX Tag | Description +| `symbol` | 55 | Symbol for which the cancellation was requested. +| `clOrdID` | 11 | Client order ID referencing which cancel request is being rejected. +| `reason` | 58 or 102 | In some FIX flows, `Text (58)` or `CxlRejReason (102)` can carry the reason for rejection. +| `sender` / `target` | (Custom)| Aligns with `SenderCompID (49)` / `TargetCompID (56)` in standard FIX. +| `sendingTime`| 52 (optional) | Timestamp of when the reject was emitted. +|=== + +The “reject” scenario typically arises if the **Order Cancel Request** was invalid, the order couldn’t be found, or the order is already filled/closed. + +''' + +== Summary + +- **FIX 4.2** uses **MsgType (35)** to identify each message type (e.g., `35=D` for New Order Single, `35=F` for Order Cancel Request, `35=8` for ExecutionReport, `35=9` for OrderCancelReject). +- Many **standard fields** have well-known tag numbers (e.g., `ClOrdID (11)`, `Symbol (55)`, `Side (54)`, `Price (44)`), which map directly to your DTO fields like `clOrdID`, `symbol`, `side`, `price`. +- **CancelAll** is **not** part of FIX 4.2 but can be considered a **custom or extension** message that follows a similar pattern. + +By aligning your data classes (`NewOrderSingle`, `CancelOrderRequest`, `ExecutionReport`, etc.) with these **standard FIX tags and message types**, you retain the ability to integrate with other FIX-based systems in the future, or to parse/emit raw FIX messages if required. \ No newline at end of file diff --git a/order-processor/src/main/java/town/lost/oms/api/OMSIn.java b/order-processor/src/main/java/town/lost/oms/api/OMSIn.java index b16c8b1..bb8a11e 100644 --- a/order-processor/src/main/java/town/lost/oms/api/OMSIn.java +++ b/order-processor/src/main/java/town/lost/oms/api/OMSIn.java @@ -10,19 +10,20 @@ import town.lost.oms.dto.NewOrderSingle; /** - * The {@code OMSIn} interface defines the operations that can be performed by an Order Management System (OMS). + * The {@code OMSIn} interface defines inbound operations for an Order Management System (OMS). *

- * It includes methods to handle new single orders, cancel order requests, and cancel all orders. - * Each method receives an instance of a data transfer object that represents the details of the operation. - * - * @see NewOrderSingle - * @see CancelOrderRequest - * @see CancelAll + * This interface is typically called by the event-driven framework after the inbound DTOs have + * passed validation. Implementors (e.g., {@link town.lost.oms.OMSImpl}) are responsible + * for generating the appropriate outbound events (e.g., {@code ExecutionReport}, {@code OrderCancelReject}) + * via an {@link OMSOut} reference. + *

+ * Thread-safety: This demo assumes a single-threaded environment. */ public interface OMSIn { /** - * Handles a new single order submission. + * Processes a new single-order submission. Typically leads to an {@code ExecutionReport} on success + * or an {@code OrderCancelReject} on failure (if the order is invalid). * * @param nos The {@link NewOrderSingle} object representing the details of the new order. */ @@ -30,17 +31,19 @@ public interface OMSIn { void newOrderSingle(NewOrderSingle nos); /** - * Handles a cancel order request. + * Processes a request to cancel a specific existing order. If no matching order is found, + * the implementation is expected to generate an {@code OrderCancelReject} with a reason like "No such order". * - * @param cor The {@link CancelOrderRequest} object representing the details of the cancel order request. + * @param cor The {@link CancelOrderRequest} object representing the details of the cancel request. */ @MethodId(2) void cancelOrderRequest(CancelOrderRequest cor); /** - * Handles a cancel all orders request. + * Processes a request to cancel all orders matching a specific filter (e.g., by symbol). + * In a minimal demo scenario, this may reject if no orders exist or simply log the cancellation attempt. * - * @param cancelAll The {@link CancelAll} object representing the details of the cancel all orders request. + * @param cancelAll The {@link CancelAll} object representing the details of the mass-cancel request. */ void cancelAll(CancelAll cancelAll); } From 50dee91b97f383eca6a68e403f450af48661ca77 Mon Sep 17 00:00:00 2001 From: Peter Lawrey Date: Wed, 1 Jan 2025 20:17:02 +0000 Subject: [PATCH 18/31] Improve reference to the FIX spec --- .../java/town/lost/oms/api/ErrorListener.java | 16 +++++---- .../main/java/town/lost/oms/api/OMSIn.java | 35 ++++++++++--------- .../main/java/town/lost/oms/api/OMSOut.java | 29 ++++++++------- .../java/town/lost/oms/dto/CancelAll.java | 9 +++++ .../town/lost/oms/dto/CancelOrderRequest.java | 18 +++++++--- .../town/lost/oms/dto/ExecutionReport.java | 32 ++++++++++------- .../town/lost/oms/dto/NewOrderSingle.java | 25 +++++++------ .../town/lost/oms/dto/OrderCancelReject.java | 13 ++++--- .../java/town/lost/oms/dto/package-info.java | 3 +- 9 files changed, 111 insertions(+), 69 deletions(-) diff --git a/order-processor/src/main/java/town/lost/oms/api/ErrorListener.java b/order-processor/src/main/java/town/lost/oms/api/ErrorListener.java index 419cca2..4a318a6 100644 --- a/order-processor/src/main/java/town/lost/oms/api/ErrorListener.java +++ b/order-processor/src/main/java/town/lost/oms/api/ErrorListener.java @@ -1,16 +1,20 @@ package town.lost.oms.api; /** - * This interface provides a method to handle JVM errors. - * Implementations of this interface will define how these errors are handled. + * The {@code ErrorListener} interface defines how critical errors or exceptions + * are communicated back to the system. This is conceptually similar to capturing + * unexpected events in a FIX flow (e.g., invalid data or system faults) and passing + * them upstream to a monitoring or logging component. + *

+ * In a FIX-based system, errors might also be reflected in Reject messages or + * {@code jvmError(msg)} calls if the framework intercepts an unhandled exception. */ public interface ErrorListener { /** - * This method handles JVM errors. - * It takes a String message which provides details about the error. + * Called when any critical system error occurs. * - * @param msg a String providing details about the JVM error. + * @param msg A descriptive message about the JVM error or exception. */ void jvmError(String msg); -} \ No newline at end of file +} diff --git a/order-processor/src/main/java/town/lost/oms/api/OMSIn.java b/order-processor/src/main/java/town/lost/oms/api/OMSIn.java index bb8a11e..75950cb 100644 --- a/order-processor/src/main/java/town/lost/oms/api/OMSIn.java +++ b/order-processor/src/main/java/town/lost/oms/api/OMSIn.java @@ -12,38 +12,39 @@ /** * The {@code OMSIn} interface defines inbound operations for an Order Management System (OMS). *

- * This interface is typically called by the event-driven framework after the inbound DTOs have - * passed validation. Implementors (e.g., {@link town.lost.oms.OMSImpl}) are responsible - * for generating the appropriate outbound events (e.g., {@code ExecutionReport}, {@code OrderCancelReject}) - * via an {@link OMSOut} reference. - *

- * Thread-safety: This demo assumes a single-threaded environment. + * In FIX 4.2 terms, these methods handle the logical equivalents of: + *

    + *
  • NewOrderSingle (35=D) - see {@link NewOrderSingle}
  • + *
  • OrderCancelRequest (35=F) - see {@link CancelOrderRequest}
  • + *
  • A custom "CancelAll" message (not a standard FIX 4.2 message) - see {@link CancelAll}
  • + *
+ * The framework typically validates each DTO before calling these methods. On success or failure, + * an appropriate outbound message (e.g., ExecutionReport, OrderCancelReject) is generated by + * the OMS implementation. */ public interface OMSIn { /** - * Processes a new single-order submission. Typically leads to an {@code ExecutionReport} on success - * or an {@code OrderCancelReject} on failure (if the order is invalid). + * Processes a new single-order submission (FIX 4.2 MsgType=35=D). * - * @param nos The {@link NewOrderSingle} object representing the details of the new order. + * @param nos The {@link NewOrderSingle} representing the details of the new order. */ - @MethodId(1) + @MethodId('D') void newOrderSingle(NewOrderSingle nos); /** - * Processes a request to cancel a specific existing order. If no matching order is found, - * the implementation is expected to generate an {@code OrderCancelReject} with a reason like "No such order". + * Processes a request to cancel a specific order (FIX 4.2 MsgType=35=F). * - * @param cor The {@link CancelOrderRequest} object representing the details of the cancel request. + * @param cor The {@link CancelOrderRequest} representing the details of the cancel request. */ - @MethodId(2) + @MethodId('F') void cancelOrderRequest(CancelOrderRequest cor); /** - * Processes a request to cancel all orders matching a specific filter (e.g., by symbol). - * In a minimal demo scenario, this may reject if no orders exist or simply log the cancellation attempt. + * Processes a request to cancel all orders for a particular symbol or filter + * (custom extension beyond FIX 4.2). * - * @param cancelAll The {@link CancelAll} object representing the details of the mass-cancel request. + * @param cancelAll The {@link CancelAll} object representing the mass-cancel request. */ void cancelAll(CancelAll cancelAll); } diff --git a/order-processor/src/main/java/town/lost/oms/api/OMSOut.java b/order-processor/src/main/java/town/lost/oms/api/OMSOut.java index 9176d14..0e686e5 100644 --- a/order-processor/src/main/java/town/lost/oms/api/OMSOut.java +++ b/order-processor/src/main/java/town/lost/oms/api/OMSOut.java @@ -9,30 +9,33 @@ import town.lost.oms.dto.OrderCancelReject; /** - * The {@code OMSOut} interface defines the output operations that an Order Management System (OMS) can perform. + * The {@code OMSOut} interface defines outbound operations from the Order Management System (OMS). *

- * It includes methods to handle execution reports and order cancel rejections. - *

It extends the {@link ErrorListener} interface to handle any JVM errors that may occur during processing. - * Each method receives an instance of a data transfer object that represents the details of the operation. - * - * @see ExecutionReport - * @see OrderCancelReject - * @see ErrorListener + * In FIX 4.2 terms, these correspond to: + *

    + *
  • ExecutionReport (35=8) - see {@link ExecutionReport}
  • + *
  • OrderCancelReject (35=9) - see {@link OrderCancelReject}
  • + *
+ * This interface also extends {@link ErrorListener} to handle critical errors. + * Typically, these methods are called by {@code OMSImpl} once inbound requests + * have been processed and an outcome is determined. */ -public interface OMSOut extends ErrorListener{ +public interface OMSOut extends ErrorListener { /** - * Handles an execution report. + * Handles an execution report (FIX 4.2 MsgType=35=8), which might indicate a new, + * partially filled, filled, or canceled order status. * - * @param er The {@link ExecutionReport} object representing the details of the execution report. + * @param er The {@link ExecutionReport} object representing the order’s current state. */ @MethodId(11) void executionReport(ExecutionReport er); /** - * Handles an order cancel reject. + * Handles an order-cancel-reject (FIX 4.2 MsgType=35=9), indicating that a cancellation + * request could not be honored (e.g., no such order). * - * @param ocr The {@link OrderCancelReject} object representing the details of the order cancel reject. + * @param ocr The {@link OrderCancelReject} object representing the reason for rejection. */ @MethodId(12) void orderCancelReject(OrderCancelReject ocr); diff --git a/order-processor/src/main/java/town/lost/oms/dto/CancelAll.java b/order-processor/src/main/java/town/lost/oms/dto/CancelAll.java index 99ef069..8225706 100755 --- a/order-processor/src/main/java/town/lost/oms/dto/CancelAll.java +++ b/order-processor/src/main/java/town/lost/oms/dto/CancelAll.java @@ -18,6 +18,15 @@ *

This class extends the {@link AbstractEvent} class with the type parameter {@link CancelAll}, indicating that * it is processed as a {@code CancelAll} event. * + * Potential FIX-like mappings: + *

    + *
  • {@code symbol} - Could reuse Symbol (FIX 55) if mass-cancel is by symbol.
  • + *
  • {@code clOrdID} (FIX 11) - A unique ID for the cancel-all request.
  • + *
  • {@code sendingTime} (FIX 52, optional) - Time the request was sent.
  • + *
  • {@code sender}/{@code target} - Align conceptually with SenderCompID (49)/TargetCompID (56).
  • + *
+ * If the system does not support partial or symbolic mass cancels, it may simply issue + * an {@code OrderCancelReject} with reason "No orders to cancel" for empty sets. *

This class is typically used like this: * *

diff --git a/order-processor/src/main/java/town/lost/oms/dto/CancelOrderRequest.java b/order-processor/src/main/java/town/lost/oms/dto/CancelOrderRequest.java
index e90cda7..3662202 100644
--- a/order-processor/src/main/java/town/lost/oms/dto/CancelOrderRequest.java
+++ b/order-processor/src/main/java/town/lost/oms/dto/CancelOrderRequest.java
@@ -20,13 +20,21 @@
  *
  * 

Each {@code CancelOrderRequest} contains the symbol of the financial instrument for the order that is being requested to be cancelled and the client order ID. * + * The {@code CancelOrderRequest} class parallels a FIX 4.2 "Order Cancel Request" (MsgType=35=F). + *

+ * Typical FIX tag mappings: *

    - *
  • symbol: The symbol of the financial instrument for the order.
  • - *
  • account: The account associated with the order.
  • - *
  • clOrdID: The client order ID, a unique identifier for the order.
  • - *
  • origClOrdID: The original client order ID of the order to be canceled.
  • - *
  • side: The side of the order (e.g., buy or sell).
  • + *
  • {@code clOrdID} (FIX 11) - New unique client ID for this cancel request.
  • + *
  • {@code origClOrdID} (FIX 41) - Original client ID of the order to be canceled.
  • + *
  • {@code symbol} (FIX 55) - Symbol of the order to be canceled.
  • + *
  • {@code side} (FIX 54) - Side of the order (buy/sell).
  • + *
  • {@code account} (FIX 1) - Account under which the original order was placed.
  • + *
  • {@code sendingTime} (FIX 52) - Time the cancel request is being sent.
  • + *
  • {@code transactTime} (FIX 60, optional) - May store the transaction time for the request.
  • + *
  • {@code sender}/{@code target} - Conceptually match SenderCompID (49) / TargetCompID (56).
  • *
+ * If the request cannot be fulfilled, an {@code OrderCancelReject} (MsgType=35=9) may result, + * indicating why the cancel was rejected. * *

The {@code symbol} and {@code account} fields are encoded using {@link ShortTextLongConverter} * to save space, while the client order IDs are strings for identification purposes. diff --git a/order-processor/src/main/java/town/lost/oms/dto/ExecutionReport.java b/order-processor/src/main/java/town/lost/oms/dto/ExecutionReport.java index 7e2cd4d..a0bb16a 100644 --- a/order-processor/src/main/java/town/lost/oms/dto/ExecutionReport.java +++ b/order-processor/src/main/java/town/lost/oms/dto/ExecutionReport.java @@ -24,20 +24,26 @@ * *

Each {@code ExecutionReport} contains various pieces of information about the order execution, including: * + * The {@code ExecutionReport} class corresponds to a FIX 4.2 "Execution Report" (MsgType=35=8). + * Used for order status updates, including partial fills, full fills, cancellations, etc. + *

+ * Typical FIX tag mappings: *

    - *
  • symbol: The identifier of the financial instrument.
  • - *
  • transactTime: The transaction time in nanoseconds.
  • - *
  • orderQty: The quantity of the order.
  • - *
  • price: The price of the order.
  • - *
  • orderID: The unique identifier of the order.
  • - *
  • lastPx: The last traded price of the order.
  • - *
  • leavesQty: The remaining quantity of the order.
  • - *
  • cumQty: The accumulated quantity of the order.
  • - *
  • avgPx: The average price of the order.
  • - *
  • side: The side of the order (buy or sell).
  • - *
  • ordType: The type of the order (e.g., market or limit).
  • - *
  • clOrdID: The client order ID.
  • - *
  • text: An optional text message about the order execution.
  • + *
  • {@code symbol} (FIX 55) - Security identifier.
  • + *
  • {@code side} (FIX 54) - Side of the order.
  • + *
  • {@code orderQty} (FIX 38) - Original quantity.
  • + *
  • {@code price} (FIX 44) - Limit price (if applicable).
  • + *
  • {@code orderID} (FIX 37) - Unique identifier assigned by the broker/exchange.
  • + *
  • {@code clOrdID} (FIX 11) - Client’s original order ID (if relevant).
  • + *
  • {@code lastPx} (FIX 31) - Price of the last fill.
  • + *
  • {@code leavesQty} (FIX 151) - Remaining quantity not yet filled.
  • + *
  • {@code cumQty} (FIX 14) - Total quantity filled so far.
  • + *
  • {@code avgPx} (FIX 6) - Average fill price across partial executions.
  • + *
  • {@code text} (FIX 58) - Free-form text for clarifications or rejections.
  • + *
  • {@code transactTime} (FIX 60) - Time of the transaction/fill.
  • + *
  • {@code sendingTime} (FIX 52, optional) - Time at which the ExecutionReport is sent.
  • + *
  • {@code ordType} (FIX 40) - The original order type (e.g., MARKET, LIMIT).
  • + *
  • {@code sender}/{@code target} - Analogous to SenderCompID (49)/TargetCompID (56).
  • *
* *

The {@code symbol} field is encoded using {@link ShortTextLongConverter}, and {@code transactTime} and {@code orderID} diff --git a/order-processor/src/main/java/town/lost/oms/dto/NewOrderSingle.java b/order-processor/src/main/java/town/lost/oms/dto/NewOrderSingle.java index ec0c897..ed1cfcb 100644 --- a/order-processor/src/main/java/town/lost/oms/dto/NewOrderSingle.java +++ b/order-processor/src/main/java/town/lost/oms/dto/NewOrderSingle.java @@ -23,18 +23,23 @@ * *

Each {@code NewOrderSingle} contains various pieces of information about the order, including: * + * The {@code NewOrderSingle} class corresponds to a FIX 4.2 "New Order - Single" (MsgType=35=D). + *

+ * Typical FIX tag mappings: *

    - *
  • symbol: The identifier of the financial instrument.
  • - *
  • transactTime: The transaction time in nanoseconds.
  • - *
  • account: The account associated with the order.
  • - *
  • orderQty: The quantity of the order.
  • - *
  • price: The price of the order.
  • - *
  • side: The side of the order (buy or sell).
  • - *
  • clOrdID: The client order ID.
  • - *
  • ordType: The type of the order (e.g., market or limit).
  • - *
  • timeInForce: The time-in-force instruction for the order.
  • - *
  • currency: The currency of the order.
  • + *
  • {@code clOrdID} (FIX 11) - Unique ID assigned by the client.
  • + *
  • {@code symbol} (FIX 55) - Ticker symbol or security identifier.
  • + *
  • {@code side} (FIX 54) - Side of the order (1=Buy, 2=Sell).
  • + *
  • {@code orderQty} (FIX 38) - Quantity of the order to be bought/sold.
  • + *
  • {@code price} (FIX 44) - Price for limit or other price-based orders.
  • + *
  • {@code timeInForce} (FIX 59) - Time in Force (Day, GTC, IOC, etc.).
  • + *
  • {@code currency} (FIX 15) - Currency (e.g., USD).
  • + *
  • {@code account} (FIX 1) - Account number or name.
  • + *
  • {@code transactTime} (FIX 60) - Timestamp of the transaction.
  • + *
  • {@code sender}/{@code target} - Conceptually match SenderCompID (49) / TargetCompID (56).
  • *
+ * This DTO is used for creating new orders in the system. Validation is performed + * externally (by the framework) before passing to {@code OMSIn#newOrderSingle}. * *

The symbol is encoded using {@link ShortText} and the transaction time is in nanoseconds, both to save space. * The client order ID is a string that identifies the order, the side indicates whether the order is to buy or sell, diff --git a/order-processor/src/main/java/town/lost/oms/dto/OrderCancelReject.java b/order-processor/src/main/java/town/lost/oms/dto/OrderCancelReject.java index 3cb6b85..bc128b1 100644 --- a/order-processor/src/main/java/town/lost/oms/dto/OrderCancelReject.java +++ b/order-processor/src/main/java/town/lost/oms/dto/OrderCancelReject.java @@ -19,12 +19,17 @@ * This indicates that the event will be processed into a {@link OrderCancelReject} that will be sent to the order * system. * - *

Each {@code OrderCancelReject} event contains: + * The {@code OrderCancelReject} class corresponds to a FIX 4.2 "Order Cancel Reject" (MsgType=35=9). + *

+ * Typical FIX tag mappings: *

    - *
  • symbol: The identifier for the financial instrument involved in the order.
  • - *
  • clOrdID: The client order ID of the order that was requested to be canceled.
  • - *
  • reason: A string explaining why the cancellation request was rejected.
  • + *
  • {@code symbol} (FIX 55) - Symbol for the order whose cancellation is rejected.
  • + *
  • {@code clOrdID} (FIX 11) - The client order ID referencing which cancel request is rejected.
  • + *
  • {@code reason} could map to {@code CxlRejReason (102)} or {@code Text (58)} for free-form explanation.
  • + *
  • {@code sender}/{@code target} - Conceptually align with SenderCompID (49) / TargetCompID (56).
  • + *
  • {@code sendingTime} (FIX 52, optional) - Timestamp for when the reject was emitted.
  • *
+ * Generally used if a {@code CancelOrderRequest} (35=F) is invalid or cannot be satisfied. * *

Note that the symbol field is encoded using {@link ShortText} to minimize the space required for storage and transmission. */ diff --git a/order-processor/src/main/java/town/lost/oms/dto/package-info.java b/order-processor/src/main/java/town/lost/oms/dto/package-info.java index 4753564..1f7ccdc 100755 --- a/order-processor/src/main/java/town/lost/oms/dto/package-info.java +++ b/order-processor/src/main/java/town/lost/oms/dto/package-info.java @@ -1,5 +1,6 @@ /** - * Provides the classes and interfaces necessary to create and manage Order Management System (OMS) data transfer objects (DTOs). + * Provides the classes and interfaces necessary to create and manage Order Management System (OMS) data transfer objects (DTOs) aligned with + * the FIX 4.2 specification (https://www.fixtrading.org/standards/fix-4-2/). * *

This package includes the following key components: * From 70c0c00593cba5b7f24c5a0728b2a52d87f1a39d Mon Sep 17 00:00:00 2001 From: Peter Lawrey Date: Wed, 1 Jan 2025 20:30:01 +0000 Subject: [PATCH 19/31] Improve reference to the FIX spec --- order-processor/README.adoc | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/order-processor/README.adoc b/order-processor/README.adoc index b4f54be..7b20d85 100644 --- a/order-processor/README.adoc +++ b/order-processor/README.adoc @@ -9,13 +9,24 @@ The Order Processor Demo showcases the implementation of services using messages * <> * <> * <> -* <> -* <> +== Requirements + +The Order Processor Demo has the following requirements documents: + +- link:src/main/adoc/oms-requirements.adoc[OMS Requirements]: This document outlines the requirements for the Order Management System (OMS) in the project. +- link:src/main/adoc/oms-api-requirements.adoc[OMS API Requirements]: This document outlines the requirements for the OMS API in the project. +- link:src/main/adoc/test-requirements.adoc[Test Requirements]: This document outlines the requirements for testing the project. +- link:src/main/adoc/selected-fix-4.2.adoc[Selected FIX 4.2]: This document outlines the selected FIX 4.2 messages and their corresponding DTO classes used in the project. + +This project uses link:aide/README.adoc[AIDE] to maintain consistency between requirements, tests, and code. See https://github.com/peter-lawrey/aide[AIDE Project] for more information. + +[#getting-started] == Getting Started To get started with the Order Processor Demo, you need to clone the repository to your local machine. Once you have cloned the repository, you can open it in your IDE of choice (we recommend IntelliJ CE for this project). +[#prerequisites] == Prerequisites Before you begin, ensure you have the following installed: @@ -25,6 +36,7 @@ Before you begin, ensure you have the following installed: - **Integrated Development Environment (IDE)**: IntelliJ IDEA Community Edition (recommended) or any other IDE - **Internet Access**: Required for Maven to download necessary dependencies +[#installation] == Installation Here are the steps to clone the repository and open it in IntelliJ: @@ -34,8 +46,10 @@ Here are the steps to clone the repository and open it in IntelliJ: 3. Navigate to the cloned repository and click 'Open' 4. Wait for the IDE to import the project and download the necessary dependencies +[#running-the-project] == Running the Project +// change this to be [mermaid] to be seen in the rendered output [source,mermaid] .... sequenceDiagram @@ -56,10 +70,12 @@ To run the project, you need to compile the project and run the main classes for You can run these classes directly from your IDE, or you can use `mvn compile exec:exec@id` on the command line to run the compiled classes. +[#contributing] == Contributing We appreciate any contributions to the Order Processor Demo. If you find any bugs or issues, please open an issue in the repository. If you want to contribute to the code, please fork the repository, make your changes, and open a pull request. +[#acknowledgments] == Acknowledgments - Inspired by real-world financial trading systems. From c36df0c58b929c015e0f1a4b36446f3b8b619313 Mon Sep 17 00:00:00 2001 From: Peter Lawrey Date: Wed, 1 Jan 2025 20:31:58 +0000 Subject: [PATCH 20/31] Updated README.adoc links --- order-processor/README.adoc | 1 + order-processor/aide/README.adoc | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/order-processor/README.adoc b/order-processor/README.adoc index 7b20d85..5cd6516 100644 --- a/order-processor/README.adoc +++ b/order-processor/README.adoc @@ -1,5 +1,6 @@ = Order Processor Demo Peter Lawrey +:lang: en-GB The Order Processor Demo showcases the implementation of services using messages based on the Financial Information Exchange (FIX) protocol. This project simulates an order processing system, including features such as creating new orders, cancelling specific orders, and cancelling all orders. The project is designed to mimic real-world financial trading systems, demonstrating how such a system can be built using Java. diff --git a/order-processor/aide/README.adoc b/order-processor/aide/README.adoc index 6bb259b..ee54b2d 100644 --- a/order-processor/aide/README.adoc +++ b/order-processor/aide/README.adoc @@ -4,6 +4,8 @@ :lang: en-GB :toc: +Copied from https://github.com/peter-lawrey/aide[Peter Lawrey's AIDE Project] + This folder contains supplemental AsciiDoc files that guide usage, workflows, or style conventions for AI-based projects. == Overview @@ -13,7 +15,6 @@ aide/ ├─ aide-glossary.adoc → Terminology definitions ├─ aide-style-guide.adoc → Project coding style guidelines ├─ aide-workflow.adoc → AI workflow explanation -├─ examples.adoc → Example using AI └─ notes-on-using-aide.adoc → Notes on using AIDE ---- From a3dbb9ff49c3391567f20f4c7458da014ea71f77 Mon Sep 17 00:00:00 2001 From: Peter Lawrey Date: Wed, 1 Jan 2025 08:32:55 +0000 Subject: [PATCH 21/31] First phase updates --- order-processor/context.asciidoc | 3199 ++++++++++++++++++++++++++++++ 1 file changed, 3199 insertions(+) create mode 100644 order-processor/context.asciidoc diff --git a/order-processor/context.asciidoc b/order-processor/context.asciidoc new file mode 100644 index 0000000..aedf202 --- /dev/null +++ b/order-processor/context.asciidoc @@ -0,0 +1,3199 @@ += Directory Content + +== File: pom.xml +.... + + + 4.0.0 + + + run.chronicle + chronicle-queue-demo + 1.0-SNAPSHOT + ../pom.xml + + + order-processor + OpenHFT/${project.parent.artifactId}/${project.artifactId} + + + + net.openhft + jlbh + + + + + + + org.codehaus.mojo + exec-maven-plugin + + + bench + + exec + + + ${java.home}/bin/java + ${jvm.requiredArgs} -classpath %classpath town.lost.oms.OMSBenchmarkMain + + + + adder + + exec + + + ${java.home}/bin/java + ${jvm.requiredArgs} -classpath %classpath town.lost.oms.OrderAdderMain + + + + viewer + + exec + + + ${java.home}/bin/java + ${jvm.requiredArgs} -classpath %classpath town.lost.oms.OrderViewerMain + + + + dump + + exec + + + ${java.home}/bin/java + ${jvm.requiredArgs} -classpath %classpath net.openhft.chronicle.queue.DumpQueueMain in + + + + + + + +.... + +Lines 70, Blanks 4, Tokens 645 + +== File: README.adoc +.... += Order Processor Demo +Peter Lawrey + +The Order Processor Demo showcases the implementation of services using messages based on the Financial Information Exchange (FIX) protocol. This project simulates an order processing system, including features such as creating new orders, cancelling specific orders, and cancelling all orders. The project is designed to mimic real-world financial trading systems, demonstrating how such a system can be built using Java. + +== Table of Contents + +* <> +* <> +* <> +* <> +* <> +* <> + +== Getting Started + +To get started with the Order Processor Demo, you need to clone the repository to your local machine. Once you have cloned the repository, you can open it in your IDE of choice (we recommend IntelliJ CE for this project). + +== Prerequisites + +Before you begin, ensure you have the following installed: + +- **Java Development Kit (JDK)**: Java 8 Update 202+, Java 11, Java 17, or Java 21 +- **Apache Maven**: Version 3.6.5 or higher +- **Integrated Development Environment (IDE)**: IntelliJ IDEA Community Edition (recommended) or any other IDE +- **Internet Access**: Required for Maven to download necessary dependencies + +== Installation + +Here are the steps to clone the repository and open it in IntelliJ: + +1. Clone the repository with `git clone ` +2. Open IntelliJ IDE and select 'Open' +3. Navigate to the cloned repository and click 'Open' +4. Wait for the IDE to import the project and download the necessary dependencies + +== Running the Project + +[source,mermaid] +.... +sequenceDiagram +autonumber +Client->>+OMS: NewOrderSingle +Note right of Client: via Chronicle Queue, persisted +Note right of OMS: processes event +OMS->>-Client: ExecutionReport +Note right of Client: via Chronicle Queue, persisted +.... + +To run the project, you need to compile the project and run the main classes for different features: + +- `OrderAdderMain`: This class simulates the creation of new orders. +- `OrderViewerMain`: This class simulates the viewing of created orders. +- `OrderBenchmarkMain`: This class benchmarks round trip time of writing a `NewOrderSingle` a queue, have the `OMSImpl` produce an `ExecutionReport` and read that report. +- `OMSImpl`: This class handles the processing of the orders, including cancelling orders. + +You can run these classes directly from your IDE, or you can use `mvn compile exec:exec@id` on the command line to run the compiled classes. + +== Contributing + +We appreciate any contributions to the Order Processor Demo. If you find any bugs or issues, please open an issue in the repository. If you want to contribute to the code, please fork the repository, make your changes, and open a pull request. + +== Acknowledgments + +- Inspired by real-world financial trading systems. +- Uses the FIX protocol for message exchange. +.... + +Lines 46, Blanks 20, Tokens 663 + +== File: src\main\java\town\lost\oms\api\ErrorListener.java +.... +package town.lost.oms.api; + +/** + * This interface provides a method to handle JVM errors. + * Implementations of this interface will define how these errors are handled. + */ +public interface ErrorListener { + + /** + * This method handles JVM errors. + * It takes a String message which provides details about the error. + * + * @param msg a String providing details about the JVM error. + */ + void jvmError(String msg); +} +.... + +Lines 14, Blanks 2, Tokens 93 + +== File: src\main\java\town\lost\oms\api\OMSIn.java +.... +/* + */ + +package town.lost.oms.api; + +import net.openhft.chronicle.bytes.MethodId; +import town.lost.oms.dto.CancelAll; +import town.lost.oms.dto.CancelOrderRequest; +import town.lost.oms.dto.NewOrderSingle; + +/** + * The {@code OMSIn} interface defines the operations that can be performed by an Order Management System (OMS). + *

+ * It includes methods to handle new single orders, cancel order requests, and cancel all orders. + * Each method receives an instance of a data transfer object that represents the details of the operation. + * + * @see NewOrderSingle + * @see CancelOrderRequest + * @see CancelAll + */ +public interface OMSIn { + + /** + * Handles a new single order submission. + * + * @param nos The {@link NewOrderSingle} object representing the details of the new order. + */ + @MethodId(1) + void newOrderSingle(NewOrderSingle nos); + + /** + * Handles a cancel order request. + * + * @param cor The {@link CancelOrderRequest} object representing the details of the cancel order request. + */ + @MethodId(2) + void cancelOrderRequest(CancelOrderRequest cor); + + /** + * Handles a cancel all orders request. + * + * @param cancelAll The {@link CancelAll} object representing the details of the cancel all orders request. + */ + void cancelAll(CancelAll cancelAll); +} +.... + +Lines 39, Blanks 6, Tokens 309 + +== File: src\main\java\town\lost\oms\api\OMSOut.java +.... +/* + */ + +package town.lost.oms.api; + +import net.openhft.chronicle.bytes.MethodId; +import town.lost.oms.dto.ExecutionReport; +import town.lost.oms.dto.OrderCancelReject; + +/** + * The {@code OMSOut} interface defines the output operations that an Order Management System (OMS) can perform. + *

+ * It includes methods to handle execution reports and order cancel rejections. + *

It extends the {@link ErrorListener} interface to handle any JVM errors that may occur during processing. + * Each method receives an instance of a data transfer object that represents the details of the operation. + * + * @see ExecutionReport + * @see OrderCancelReject + * @see ErrorListener + */ +public interface OMSOut extends ErrorListener{ + + /** + * Handles an execution report. + * + * @param er The {@link ExecutionReport} object representing the details of the execution report. + */ + @MethodId(11) + void executionReport(ExecutionReport er); + + /** + * Handles an order cancel reject. + * + * @param ocr The {@link OrderCancelReject} object representing the details of the order cancel reject. + */ + @MethodId(12) + void orderCancelReject(OrderCancelReject ocr); +} +.... + +Lines 33, Blanks 5, Tokens 267 + +== File: src\main\java\town\lost\oms\api\package-info.java +.... +/** + * Provides the API interfaces for interactions with the Order Management System (OMS) in the 'town.lost.oms' project. + * + *

This package includes the following interfaces: + * + *

    + *
  • {@link town.lost.oms.api.OMSIn}, which defines the methods for inbound operations that an OMS can perform, such as handling new single orders, cancel order requests, and cancel all orders.
  • + *
  • {@link town.lost.oms.api.OMSOut}, which defines the methods for outbound operations from the OMS, including handling execution reports and order cancel rejections.
  • + *
+ * + *

Each interface includes methods that correspond to specific actions within the OMS, facilitating communication between clients and the OMS. + * + *

For more details, refer to the documentation for each individual interface. + */ +package town.lost.oms.api; + +.... + +Lines 15, Blanks 1, Tokens 187 + +== File: src\main\java\town\lost\oms\dto\AbstractEvent.java +.... +/* + */ + +package town.lost.oms.dto; + +import net.openhft.chronicle.bytes.BytesIn; +import net.openhft.chronicle.bytes.BytesOut; +import net.openhft.chronicle.core.Jvm; +import net.openhft.chronicle.core.io.InvalidMarshallableException; +import net.openhft.chronicle.core.io.Validatable; +import net.openhft.chronicle.wire.*; +import net.openhft.chronicle.wire.converter.NanoTime; +import net.openhft.chronicle.wire.converter.ShortText; + +/** + * The {@code AbstractEvent} class represents a generic event in the system. + * + *

This class provides common fields such as sender, target, and sendingTime which can be extended + * by more specific events. Fields sender and target are encoded as {@link ShortText} for efficient storage and transmission. + * + *

The class is generic, with the type parameter being a subclass of {@code AbstractEvent}. This allows + * methods in this class to return an instance of the subclass, enabling method chaining in the subclass. + * + *

The encoding of the marshalled bytes can be controlled via system properties "bytesInBinary" and "pregeneratedMarshallable". + */ +public class AbstractEvent> extends SelfDescribingMarshallable implements Validatable { + // Used to control the benchmark + public static final boolean BYTES_IN_BINARY = Jvm.getBoolean("bytesInBinary", true); + + // Used to control the benchmark + public static final boolean PREGENERATED_MARSHALLABLE = Jvm.getBoolean("pregeneratedMarshallable", true); + private static final int MARSHALLABLE_VERSION = 1; + + // The sender of the event + @ShortText + private long sender; + + // The target of the event + @ShortText + private long target; + + // The sending time of the event, in nanoseconds. + @NanoTime + private long sendingTime; + + @Override + public boolean usesSelfDescribingMessage() { + return !BYTES_IN_BINARY; + } + + /** + * Get the sender of the event. + * + * @return The sender's value as a long. + */ + public long sender() { + return sender; + } + + /** + * Set the sender of the event. + * + * @param sender The sender's value to set, as a long. + * @return This AbstractEvent instance, to facilitate method chaining. + */ + @SuppressWarnings("unchecked") + public E sender(long sender) { + this.sender = sender; + return (E) this; + } + + /** + * Get the target of the event. + * + * @return The target's value as a long. + */ + public long target() { + return target; + } + + /** + * Set the target of the event. + * + * @param target The target's value to set, as a long. + * @return This AbstractEvent instance, to facilitate method chaining. + */ + @SuppressWarnings("unchecked") + public E target(long target) { + this.target = target; + return (E) this; + } + + /** + * Get the sending time of the event. + * + * @return The sending time as a long. + */ + public long sendingTime() { + return sendingTime; + } + + /** + * Set the sending time of the event. + * + * @param sendingTime The sending time to set, as a long. + * @return This AbstractEvent instance, to facilitate method chaining. + */ + @SuppressWarnings("unchecked") + public E sendingTime(long sendingTime) { + this.sendingTime = sendingTime; + return (E) this; + } + + @Override + public void writeMarshallable(BytesOut out) { + if (PREGENERATED_MARSHALLABLE) { + out.writeStopBit(MARSHALLABLE_VERSION); + out.writeLong(sender); + out.writeLong(target); + out.writeLong(sendingTime); + } else { + super.writeMarshallable(out); + } + } + + @Override + public void readMarshallable(BytesIn in) { + if (PREGENERATED_MARSHALLABLE) { + int version = (int) in.readStopBit(); + if (version == MARSHALLABLE_VERSION) { + sender = in.readLong(); + target = in.readLong(); + sendingTime = in.readLong(); + } else { + throw new IllegalStateException("Unknown version " + version); + } + } else { + super.readMarshallable(in); + } + } + + @Override + public void writeMarshallable(WireOut out) { + if (PREGENERATED_MARSHALLABLE) { + out.write("sender").writeLong(ShortTextLongConverter.INSTANCE, sender); + out.write("target").writeLong(ShortTextLongConverter.INSTANCE, target); + out.write("sendingTime").writeLong(MicroTimestampLongConverter.INSTANCE, sendingTime); + } else { + super.writeMarshallable(out); + } + } + + @Override + public void readMarshallable(WireIn in) { + if (PREGENERATED_MARSHALLABLE) { + sender = in.read("sender").readLong(ShortTextLongConverter.INSTANCE); + target = in.read("target").readLong(ShortTextLongConverter.INSTANCE); + sendingTime = in.read("sendingTime").readLong(MicroTimestampLongConverter.INSTANCE); + } else { + super.readMarshallable(in); + } + } + + @Override + public void validate() throws InvalidMarshallableException { + if (sender == 0) + throw new InvalidMarshallableException("sender is required"); + if (target == 0) + throw new InvalidMarshallableException("target is required"); + if (sendingTime == 0) + throw new InvalidMarshallableException("sendingTime is required"); + } +} +.... + +Lines 154, Blanks 19, Tokens 1262 + +== File: src\main\java\town\lost\oms\dto\CancelAll.java +.... +/* + */ + +package town.lost.oms.dto; + +import net.openhft.chronicle.bytes.BytesIn; +import net.openhft.chronicle.bytes.BytesOut; +import net.openhft.chronicle.core.io.InvalidMarshallableException; +import net.openhft.chronicle.wire.ShortTextLongConverter; +import net.openhft.chronicle.wire.WireIn; +import net.openhft.chronicle.wire.WireOut; +import net.openhft.chronicle.wire.converter.ShortText; + +/** + * The {@code CancelAll} class represents an event that is used to cancel all active orders for a particular symbol. + * + *

This class extends the {@link AbstractEvent} class with the type parameter {@link CancelAll}, indicating that + * it is processed as a {@code CancelAll} event. + * + *

This class is typically used like this: + * + *

+ * public void cancelAll(CancelAll cancelAll) {
+ *     // Iterate over active orders and cancel those matching the symbol
+ *     for (Order order : activeOrders) {
+ *         if (order.symbol() == cancelAll.symbol()) {
+ *             CancelOrderRequest request = new CancelOrderRequest()
+ *                 .sender(cancelAll.target())
+ *                 .target(cancelAll.sender())
+ *                 .symbol(order.symbol())
+ *                 .clOrdID(order.clOrdID())
+ *                 .sendingTime(cancelAll.sendingTime())
+ *                 .origClOrdID(order.origClOrdID())
+ *                 .side(order.side());
+ *             out.cancelOrderRequest(request);
+ *         }
+ *     }
+ * }
+ * }
+ * + *

Note that the {@code symbol} field is encoded using {@link ShortTextLongConverter} to minimize storage and transmission size. + */ +public class CancelAll extends AbstractEvent { + private static final int MASHALLABLE_VERSION = 1; + // Symbol for which all orders are to be canceled. + @ShortText + private long symbol; + + // Client order ID. + private String clOrdID = ""; + + /** + * Get the symbol for which all orders are to be canceled. + * + * @return The symbol value as a long. + */ + public long symbol() { + return symbol; + } + + /** + * Set the symbol for which all orders are to be canceled. + * + * @param symbol The symbol value to set, as a long. + * @return This CancelAll instance, to facilitate method chaining. + */ + public CancelAll symbol(long symbol) { + this.symbol = symbol; + return this; + } + + /** + * Retrieves the client order ID. + * + * @return The client order ID as a {@code String}. + */ + public String clOrdID() { + return clOrdID; + } + + /** + * Sets the client order ID and returns the {@code CancelAll} object. This method allows for method chaining. + * + * @param clOrdID The client order ID. + * @return The {@code CancelAll} object with the client order ID set. + */ + public CancelAll clOrdID(String clOrdID) { + this.clOrdID = clOrdID; + return this; + } + + @Override + public void writeMarshallable(WireOut out) { + super.writeMarshallable(out); + if (PREGENERATED_MARSHALLABLE) { + out.write("symbol").writeLong(ShortTextLongConverter.INSTANCE, symbol); + out.write("clOrdID").object(String.class, clOrdID); + } + } + + @Override + public void readMarshallable(WireIn in) { + super.readMarshallable(in); + if (PREGENERATED_MARSHALLABLE) { + symbol = in.read("symbol").readLong(ShortTextLongConverter.INSTANCE); + clOrdID = in.read("clOrdID").object(clOrdID, String.class); + } + } + + @Override + public void writeMarshallable(BytesOut out) { + super.writeMarshallable(out); + if (PREGENERATED_MARSHALLABLE) { + out.writeStopBit(MASHALLABLE_VERSION); + out.writeLong(symbol); + out.writeObject(String.class, clOrdID); + } + } + + @Override + public void readMarshallable(BytesIn in) { + super.readMarshallable(in); + if (PREGENERATED_MARSHALLABLE) { + int version = (int) in.readStopBit(); + if (version == MASHALLABLE_VERSION) { + symbol = in.readLong(); + clOrdID = in.readObject(String.class); + } else { + throw new IllegalStateException("Unknown version " + version); + } + } + } + + /** + * Validates the fields of this {@code CancelAll} event. + * + * @throws InvalidMarshallableException if any required field is missing or invalid. + */ + @Override + public void validate() throws InvalidMarshallableException { + super.validate(); + if (symbol == 0) { + throw new InvalidMarshallableException("symbol is required"); + } + if (clOrdID == null || clOrdID.isEmpty()) { + throw new InvalidMarshallableException("clOrdID is required"); + } + } +} +.... + +Lines 136, Blanks 13, Tokens 1121 + +== File: src\main\java\town\lost\oms\dto\CancelOrderRequest.java +.... +/* + */ + +package town.lost.oms.dto; + +import net.openhft.chronicle.bytes.BytesIn; +import net.openhft.chronicle.bytes.BytesOut; +import net.openhft.chronicle.core.io.InvalidMarshallableException; +import net.openhft.chronicle.wire.ShortTextLongConverter; +import net.openhft.chronicle.wire.WireIn; +import net.openhft.chronicle.wire.WireOut; +import net.openhft.chronicle.wire.converter.ShortText; + +/** + * The {@code CancelOrderRequest} class represents a request to cancel an order in a trading system. + * + *

This class extends the {@link AbstractEvent} class, with the type parameter being {@link CancelOrderRequest}. + * This indicates that the event will be processed into a {@link CancelOrderRequest} that represents a request to cancel an order. + * + *

Each {@code CancelOrderRequest} contains the symbol of the financial instrument for the order that is being requested to be cancelled and the client order ID. + * + *

    + *
  • symbol: The symbol of the financial instrument for the order.
  • + *
  • account: The account associated with the order.
  • + *
  • clOrdID: The client order ID, a unique identifier for the order.
  • + *
  • origClOrdID: The original client order ID of the order to be canceled.
  • + *
  • side: The side of the order (e.g., buy or sell).
  • + *
+ * + *

The {@code symbol} and {@code account} fields are encoded using {@link ShortTextLongConverter} + * to save space, while the client order IDs are strings for identification purposes. + */ +public class CancelOrderRequest extends AbstractEvent { + private static final int MASHALLABLE_VERSION = 1; + // Symbol of the financial instrument for the order. + @ShortText + private long symbol; + @ShortText + private long account; + + // Client order ID. + private String clOrdID = ""; + + // Original client order ID. + private String origClOrdID = ""; + + // Side of the order (e.g., buy or sell). + private Side side; + + + /** + * Retrieves the symbol of the financial instrument for the order. + * + * @return The symbol as a {@code long}. + */ + public long symbol() { + return symbol; + } + + /** + * Sets the symbol of the financial instrument for the order and returns the {@code CancelOrderRequest} object. This method allows for method chaining. + * + * @param symbol The symbol of the financial instrument for the order. + * @return The {@code CancelOrderRequest} object with the symbol set. + */ + public CancelOrderRequest symbol(long symbol) { + this.symbol = symbol; + return this; + } + + + /** + * Retrieves the account associated with the order. + * + * @return The account as a {@code long}. + */ + public long account() { + return account; + } + + /** + * Sets the account associated with the order. + * + * @param account The account to set. + * @return This {@code CancelOrderRequest} instance for method chaining. + */ + public CancelOrderRequest account(long account) { + this.account = account; + return this; + } + + /** + * Retrieves the client order ID. + * + * @return The client order ID as a {@code String}. + */ + public String clOrdID() { + return clOrdID; + } + + /** + * Sets the client order ID and returns the {@code CancelOrderRequest} object. This method allows for method chaining. + * + * @param clOrdID The client order ID. + * @return The {@code CancelOrderRequest} object with the client order ID set. + */ + public CancelOrderRequest clOrdID(String clOrdID) { + this.clOrdID = clOrdID; + return this; + } + + /** + * Retrieves the original client order ID. + * + * @return The original client order ID as a {@code String}. + */ + public String origClOrdID() { + return origClOrdID; + } + + /** + * Sets the original client order ID. + * + * @param origClOrdID The original client order ID to set. + * @return This {@code CancelOrderRequest} instance for method chaining. + */ + public CancelOrderRequest origClOrdID(String origClOrdID) { + this.origClOrdID = origClOrdID; + return this; + } + + /** + * Retrieves the side of the order. + * + * @return The side of the order as a {@link Side} enum. + */ + public Side side() { + return side; + } + + /** + * Sets the side of the order. + * + * @param side The side to set (e.g., buy or sell). + * @return This {@code CancelOrderRequest} instance for method chaining. + */ + public CancelOrderRequest side(Side side) { + this.side = side; + return this; + } + + @Override + public void writeMarshallable(WireOut out) { + super.writeMarshallable(out); + if (PREGENERATED_MARSHALLABLE) { + out.write("symbol").writeLong(ShortTextLongConverter.INSTANCE, symbol); + out.write("account").writeLong(ShortTextLongConverter.INSTANCE, account); + out.write("clOrdID").object(String.class, clOrdID); + out.write("origClOrdID").object(String.class, origClOrdID); + out.write("side").object(Side.class, side); + } + } + + @Override + public void readMarshallable(WireIn in) { + super.readMarshallable(in); + if (PREGENERATED_MARSHALLABLE) { + symbol = in.read("symbol").readLong(ShortTextLongConverter.INSTANCE); + account = in.read("account").readLong(ShortTextLongConverter.INSTANCE); + clOrdID = in.read("clOrdID").object(clOrdID, String.class); + origClOrdID = in.read("origClOrdID").object(origClOrdID, String.class); + side = in.read("side").object(Side.class); + } + } + + @Override + public void writeMarshallable(BytesOut out) { + super.writeMarshallable(out); + if (PREGENERATED_MARSHALLABLE) { + out.writeStopBit(MASHALLABLE_VERSION); + out.writeLong(symbol); + out.writeLong(account); + out.writeObject(String.class, clOrdID); + out.writeObject(String.class, origClOrdID); + out.writeObject(Side.class, side); + } + } + + @SuppressWarnings("unchecked") + @Override + public void readMarshallable(BytesIn in) { + super.readMarshallable(in); + if (PREGENERATED_MARSHALLABLE) { + int version = (int) in.readStopBit(); + if (version == MASHALLABLE_VERSION) { + symbol = in.readLong(); + account = in.readLong(); + clOrdID = in.readObject(String.class); + origClOrdID = in.readObject(String.class); + side = in.readObject(Side.class); + } else { + throw new IllegalStateException("Unknown version " + version); + } + } + } + + /** + * Validates the fields of this {@code CancelOrderRequest}. + * + * @throws InvalidMarshallableException if any required field is missing or invalid. + */ + @Override + public void validate() throws InvalidMarshallableException { + if (symbol == 0) { + throw new InvalidMarshallableException("symbol is required"); + } + if (account == 0) { + throw new InvalidMarshallableException("account is required"); + } + if (clOrdID == null || clOrdID.isEmpty()) { + throw new InvalidMarshallableException("clOrdID is required"); + } + if (origClOrdID == null || origClOrdID.isEmpty()) { + throw new InvalidMarshallableException("origClOrdID is required"); + } + if (side == null) { + throw new InvalidMarshallableException("side is required"); + } + } +} +.... + +Lines 207, Blanks 23, Tokens 1812 + +== File: src\main\java\town\lost\oms\dto\Ccy.java +.... +package town.lost.oms.dto; + +/** + * The {@code Ccy} enum represents international currency codes as per the ISO 4217 standard. + * + *

Each constant corresponds to a specific currency, identified by its three-letter code. + * Additional information such as the currency name, country, and symbol can be accessed through + * the provided methods. + * + *

Example usage: + *

{@code
+ * Ccy currency = Ccy.USD;
+ * System.out.println("Currency Code: " + currency.getCode());
+ * System.out.println("Currency Name: " + currency.getCurrencyName());
+ * System.out.println("Country: " + currency.getCountry());
+ * System.out.println("Symbol: " + currency.getSymbol());
+ * }
+ * + *

This would output: + *

+ * Currency Code: USD
+ * Currency Name: United States Dollar
+ * Country: United States
+ * Symbol: $
+ * 
+ */ +public enum Ccy { + /** + * United States Dollar. + */ + USD("United States Dollar", "United States", "$"), + /** + * Euro. + */ + EUR("Euro", "Eurozone", "€"), + /** + * British Pound Sterling. + */ + GBP("Pound Sterling", "United Kingdom", "£"), + /** + * Japanese Yen. + */ + JPY("Japanese Yen", "Japan", "¥"), + /** + * Swiss Franc. + */ + CHF("Swiss Franc", "Switzerland", "CHF"), + /** + * Canadian Dollar. + */ + CAD("Canadian Dollar", "Canada", "C$"), + /** + * Australian Dollar. + */ + AUD("Australian Dollar", "Australia", "A$"), + /** + * New Zealand Dollar. + */ + NZD("New Zealand Dollar", "New Zealand", "NZ$"), + /** + * Chinese Yuan Renminbi. + */ + CNY("Chinese Yuan", "China", "¥"), + /** + * Swedish Krona. + */ + SEK("Swedish Krona", "Sweden", "kr"), + /** + * Norwegian Krone. + */ + NOK("Norwegian Krone", "Norway", "kr"), + /** + * Russian Ruble. + */ + RUB("Russian Ruble", "Russia", "₽"), + /** + * South African Rand. + */ + ZAR("South African Rand", "South Africa", "R"), + /** + * Singapore Dollar. + */ + SGD("Singapore Dollar", "Singapore", "S$"), + /** + * Hong Kong Dollar. + */ + HKD("Hong Kong Dollar", "Hong Kong", "HK$"); + + // Fields + private final String currencyName; + private final String country; + private final String symbol; + + /** + * Constructs a {@code Ccy} enum constant with the specified currency name, country, and symbol. + * + * @param currencyName the full name of the currency + * @param country the country or region where the currency is used + * @param symbol the symbol representing the currency + */ + Ccy(String currencyName, String country, String symbol) { + this.currencyName = currencyName; + this.country = country; + this.symbol = symbol; + } + + /** + * Gets the full name of the currency. + * + * @return the currency name + */ + public String getCurrencyName() { + return currencyName; + } + + /** + * Gets the country or region where the currency is used. + * + * @return the country or region + */ + public String getCountry() { + return country; + } + + /** + * Gets the symbol representing the currency. + * + * @return the currency symbol + */ + public String getSymbol() { + return symbol; + } + + /** + * Gets the three-letter currency code as per the ISO 4217 standard. + * + * @return the currency code + */ + public String getCode() { + return this.name(); + } +} + +.... + +Lines 135, Blanks 8, Tokens 853 + +== File: src\main\java\town\lost\oms\dto\ExecutionReport.java +.... +/* + */ + +package town.lost.oms.dto; + +import net.openhft.chronicle.bytes.BytesIn; +import net.openhft.chronicle.bytes.BytesOut; +import net.openhft.chronicle.core.io.InvalidMarshallableException; +import net.openhft.chronicle.wire.ShortTextLongConverter; +import net.openhft.chronicle.wire.NanoTimestampLongConverter; +import net.openhft.chronicle.wire.WireIn; +import net.openhft.chronicle.wire.WireOut; +import net.openhft.chronicle.wire.converter.NanoTime; +import net.openhft.chronicle.wire.converter.ShortText; + +import static town.lost.oms.dto.ValidateUtil.*; + +/** + * The {@code ExecutionReport} class represents the execution report of an order in a trading system. + * + *

This class extends the {@link AbstractEvent} class, with the type parameter being {@link ExecutionReport}. + * This indicates that the event will be processed into an {@link ExecutionReport} that represents the execution report of an order. + * + *

Each {@code ExecutionReport} contains various pieces of information about the order execution, including: + * + *

    + *
  • symbol: The identifier of the financial instrument.
  • + *
  • transactTime: The transaction time in nanoseconds.
  • + *
  • orderQty: The quantity of the order.
  • + *
  • price: The price of the order.
  • + *
  • orderID: The unique identifier of the order.
  • + *
  • lastPx: The last traded price of the order.
  • + *
  • leavesQty: The remaining quantity of the order.
  • + *
  • cumQty: The accumulated quantity of the order.
  • + *
  • avgPx: The average price of the order.
  • + *
  • side: The side of the order (buy or sell).
  • + *
  • ordType: The type of the order (e.g., market or limit).
  • + *
  • clOrdID: The client order ID.
  • + *
  • text: An optional text message about the order execution.
  • + *
+ * + *

The {@code symbol} field is encoded using {@link ShortTextLongConverter}, and {@code transactTime} and {@code orderID} + * use {@link NanoTimestampLongConverter} to save space. + */ +public class ExecutionReport extends AbstractEvent { + private static final int MASHALLABLE_VERSION = 1; + // Symbol of the financial instrument. + @ShortText + private long symbol; + + // Transaction time in nanoseconds. + @NanoTime + private long transactTime; + + // Quantity of the order. + private double orderQty; + + // Price of the order. + private double price; + + // Order ID in nanoseconds. + @NanoTime + private long orderID; + + // Last traded price of the order. + private double lastPx; + + // Remaining quantity of the order. + private double leavesQty; + + // Accumulated quantity of the order. + private double cumQty; + + // Average price of the order. + private double avgPx; + + // Side of the order (buy or sell). + private Side side; + + // Type of the order (market or limit). + private OrderType ordType; + + // Client order ID. + private String clOrdID = ""; + + // Optional text message about the order execution. + private String text = null; + + /** + * Retrieves the client order ID. + * + * @return the client order ID as a {@code String} + */ + public String clOrdID() { + return clOrdID; + } + + /** + * Sets the client order ID. + * + * @param clOrdID the client order ID to set + * @return this {@code ExecutionReport} instance for method chaining + */ + public ExecutionReport clOrdID(String clOrdID) { + this.clOrdID = clOrdID; + return this; + } + + /** + * Retrieves the symbol of the financial instrument. + * + * @return the symbol as a {@code long} + */ + public long symbol() { + return symbol; + } + + /** + * Sets the symbol of the financial instrument. + * + * @param symbol the symbol to set + * @return this {@code ExecutionReport} instance for method chaining + */ + public ExecutionReport symbol(long symbol) { + this.symbol = symbol; + return this; + } + + /** + * Retrieves the side of the order (buy or sell). + * + * @return the side as a {@link Side} enum value + */ + public Side side() { + return side; + } + + /** + * Sets the side of the order (buy or sell). + * + * @param side the side to set + * @return this {@code ExecutionReport} instance for method chaining + */ + public ExecutionReport side(Side side) { + this.side = side; + return this; + } + + /** + * Retrieves the transaction time in nanoseconds. + * + * @return the transaction time as a {@code long} + */ + public long transactTime() { + return transactTime; + } + + /** + * Sets the transaction time in nanoseconds. + * + * @param transactTime the transaction time to set + * @return this {@code ExecutionReport} instance for method chaining + */ + public ExecutionReport transactTime(long transactTime) { + this.transactTime = transactTime; + return this; + } + + /** + * Retrieves the quantity of the order. + * + * @return the order quantity as a {@code double} + */ + public double orderQty() { + return orderQty; + } + + /** + * Sets the quantity of the order. + * + * @param orderQty the order quantity to set + * @return this {@code ExecutionReport} instance for method chaining + */ + public ExecutionReport orderQty(double orderQty) { + this.orderQty = orderQty; + return this; + } + + /** + * Retrieves the price of the order. + * + * @return the price as a {@code double} + */ + public double price() { + return price; + } + + /** + * Sets the price of the order. + * + * @param price the price to set + * @return this {@code ExecutionReport} instance for method chaining + */ + public ExecutionReport price(double price) { + this.price = price; + return this; + } + + /** + * Retrieves the order ID. + * + * @return the order ID as a {@code long} + */ + public long orderID() { + return orderID; + } + + /** + * Sets the order ID. + * + * @param orderID the order ID to set + * @return this {@code ExecutionReport} instance for method chaining + */ + public ExecutionReport orderID(long orderID) { + this.orderID = orderID; + return this; + } + /** + * Returns the order type. + * + * @return the order type + */ + public OrderType ordType() { + return ordType; + } + /** + * Sets the order type and returns this ExecutionReport instance. + * + * @param ordType the order type to set + * @return this ExecutionReport instance + */ + public ExecutionReport ordType(OrderType ordType) { + this.ordType = ordType; + return this; + } + + /** + * Retrieves the last traded price of the order. + * + * @return the last traded price as a {@code double} + */ + public double lastPx() { + return lastPx; + } + + /** + * Sets the last traded price of the order. + * + * @param lastPx the last price to set + * @return this {@code ExecutionReport} instance for method chaining + */ + public ExecutionReport lastPx(double lastPx) { + this.lastPx = lastPx; + return this; + } + + /** + * Retrieves the remaining quantity of the order. + * + * @return the leaves quantity as a {@code double} + */ + public double leavesQty() { + return leavesQty; + } + + /** + * Sets the remaining quantity of the order. + * + * @param leavesQty the leaves quantity to set + * @return this {@code ExecutionReport} instance for method chaining + */ + public ExecutionReport leavesQty(double leavesQty) { + this.leavesQty = leavesQty; + return this; + } + + /** + * Retrieves the accumulated quantity of the order. + * + * @return the cumulative quantity as a {@code double} + */ + public double cumQty() { + return cumQty; + } + + /** + * Sets the accumulated quantity of the order. + * + * @param cumQty the cumulative quantity to set + * @return this {@code ExecutionReport} instance for method chaining + */ + public ExecutionReport cumQty(double cumQty) { + this.cumQty = cumQty; + return this; + } + + /** + * Retrieves the average price of the order. + * + * @return the average price as a {@code double} + */ + public double avgPx() { + return avgPx; + } + + /** + * Sets the average price and returns this ExecutionReport instance. + * + * @param avgPx the average price to set + * @return this ExecutionReport instance + */ + public ExecutionReport avgPx(double avgPx) { + this.avgPx = avgPx; + return this; + } + + /** + * Retrieves the optional text message about the order execution. + * + * @return the text message as a {@code String} + */ + public String text() { + return text; + } + + /** + * Sets the text of the report and returns this ExecutionReport instance. + * + * @param text the text message to set + * @return this {@code ExecutionReport} instance for method chaining + */ + public ExecutionReport text(String text) { + this.text = text; + return this; + } + + @Override + public void writeMarshallable(BytesOut out) { + super.writeMarshallable(out); + if (PREGENERATED_MARSHALLABLE) { + out.writeStopBit(MASHALLABLE_VERSION); + out.writeLong(symbol); + out.writeLong(transactTime); + out.writeDouble(orderQty); + out.writeDouble(price); + out.writeLong(orderID); + out.writeDouble(lastPx); + out.writeDouble(leavesQty); + out.writeDouble(cumQty); + out.writeDouble(avgPx); + out.writeObject(Side.class, side); + out.writeObject(OrderType.class, ordType); + out.writeObject(String.class, clOrdID); + out.writeObject(String.class, text); + } + } + + @Override + public void readMarshallable(BytesIn in) { + super.readMarshallable(in); + if (PREGENERATED_MARSHALLABLE) { + int version = (int) in.readStopBit(); + if (version == MASHALLABLE_VERSION) { + symbol = in.readLong(); + transactTime = in.readLong(); + orderQty = in.readDouble(); + price = in.readDouble(); + orderID = in.readLong(); + lastPx = in.readDouble(); + leavesQty = in.readDouble(); + cumQty = in.readDouble(); + avgPx = in.readDouble(); + side = in.readObject(Side.class); + ordType = in.readObject(OrderType.class); + clOrdID = in.readObject(String.class); + text = in.readObject(String.class); + } else { + throw new IllegalStateException("Unknown version " + version); + } + } + } + + @Override + public void writeMarshallable(WireOut out) { + super.writeMarshallable(out); + if (PREGENERATED_MARSHALLABLE) { + out.write("symbol").writeLong(ShortTextLongConverter.INSTANCE, symbol); + out.write("transactTime").writeLong(NanoTimestampLongConverter.INSTANCE, transactTime); + out.write("orderQty").writeDouble(orderQty); + out.write("price").writeDouble(price); + out.write("orderID").writeLong(NanoTimestampLongConverter.INSTANCE, orderID); + out.write("lastPx").writeDouble(lastPx); + out.write("leavesQty").writeDouble(leavesQty); + out.write("cumQty").writeDouble(cumQty); + out.write("avgPx").writeDouble(avgPx); + out.write("side").object(Side.class, side); + out.write("ordType").object(OrderType.class, ordType); + out.write("clOrdID").object(String.class, clOrdID); + out.write("text").object(String.class, text); + } + } + + @Override + public void readMarshallable(WireIn in) { + super.readMarshallable(in); + if (PREGENERATED_MARSHALLABLE) { + symbol = in.read("symbol").readLong(ShortTextLongConverter.INSTANCE); + transactTime = in.read("transactTime").readLong(NanoTimestampLongConverter.INSTANCE); + orderQty = in.read("orderQty").readDouble(); + price = in.read("price").readDouble(); + orderID = in.read("orderID").readLong(NanoTimestampLongConverter.INSTANCE); + lastPx = in.read("lastPx").readDouble(); + leavesQty = in.read("leavesQty").readDouble(); + cumQty = in.read("cumQty").readDouble(); + avgPx = in.read("avgPx").readDouble(); + side = in.read("side").object(side, Side.class); + ordType = in.read("ordType").object(ordType, OrderType.class); + clOrdID = in.read("clOrdID").object(clOrdID, String.class); + text = in.read("text").object(text, String.class); + } + } + + /** + * Validates the fields of this {@code ExecutionReport} event. + * + * @throws InvalidMarshallableException if any required field is missing or invalid + */ + @Override + public void validate() throws InvalidMarshallableException { + super.validate(); + if (symbol == 0) + throw new InvalidMarshallableException("symbol is required"); + if (transactTime == 0) + throw new InvalidMarshallableException("transactTime is required"); + if (invalidQuantity(orderQty)) + throw new InvalidMarshallableException("orderQty is invalid"); + if (invalidPrice(price)) + throw new InvalidMarshallableException("price is invalid"); + if (orderID == 0) + throw new InvalidMarshallableException("orderID is required"); + if (lastPx != 0 && invalidPrice(lastPx)) + throw new InvalidMarshallableException("lastPx is invalid"); + if (invalidQuantity(leavesQty)) + throw new InvalidMarshallableException("leavesQty is invalid"); + if (invalidQuantity(cumQty)) + throw new InvalidMarshallableException("cumQty is invalid"); + if (avgPx != 0 && invalidPrice(avgPx)) + throw new InvalidMarshallableException("avgPx is invalid"); + if (side == null) + throw new InvalidMarshallableException("side is required"); + if (ordType == null) + throw new InvalidMarshallableException("ordType is required"); + if (clOrdID == null || clOrdID.isEmpty()) + throw new InvalidMarshallableException("clOrdID is required"); + } +} +.... + +Lines 421, Blanks 45, Tokens 3417 + +== File: src\main\java\town\lost\oms\dto\NewOrderSingle.java +.... +/* + */ + +package town.lost.oms.dto; + +import net.openhft.chronicle.bytes.BytesIn; +import net.openhft.chronicle.bytes.BytesOut; +import net.openhft.chronicle.core.io.InvalidMarshallableException; +import net.openhft.chronicle.wire.*; +import net.openhft.chronicle.wire.converter.NanoTime; +import net.openhft.chronicle.wire.converter.ShortText; + +import static town.lost.oms.dto.ValidateUtil.invalidPrice; +import static town.lost.oms.dto.ValidateUtil.invalidQuantity; + +/** + * The {@code NewOrderSingle} class represents a new single order in a trading system. + * + *

This class extends the {@link AbstractEvent} class, with the type parameter being {@link NewOrderSingle}. + * This indicates that the event will be processed into a {@link NewOrderSingle} that will be sent to the order + * system. + * + *

Each {@code NewOrderSingle} contains various pieces of information about the order, including: + * + *

    + *
  • symbol: The identifier of the financial instrument.
  • + *
  • transactTime: The transaction time in nanoseconds.
  • + *
  • account: The account associated with the order.
  • + *
  • orderQty: The quantity of the order.
  • + *
  • price: The price of the order.
  • + *
  • side: The side of the order (buy or sell).
  • + *
  • clOrdID: The client order ID.
  • + *
  • ordType: The type of the order (e.g., market or limit).
  • + *
  • timeInForce: The time-in-force instruction for the order.
  • + *
  • currency: The currency of the order.
  • + *
+ * + *

The symbol is encoded using {@link ShortText} and the transaction time is in nanoseconds, both to save space. + * The client order ID is a string that identifies the order, the side indicates whether the order is to buy or sell, + * and the order type indicates whether the order is a market order or a limit order. + */ +public class NewOrderSingle extends AbstractEvent { + private static final int MASHALLABLE_VERSION = 1; + // Symbol of the financial instrument. + @ShortText + private long symbol; + // Transaction time in nanoseconds. + @NanoTime + private long transactTime; + @ShortText + private long account; + + // Quantity of the order. + private double orderQty; + + // Price of the order. + private double price; + + // Side of the order (buy or sell). + private Side side; + + // Client order ID. + private String clOrdID = ""; + + // Type of the order (market or limit). + private OrderType ordType; + + // Time-in-force instruction for the order. + private TimeInForce timeInForce; + + // Currency of the order. + private Ccy currency; + + /** + * Get the client order ID. + * + * @return The client order ID as a string. + */ + public String clOrdID() { + return clOrdID; + } + + /** + * Sets the client order ID. + * + * @param clOrdID The client order ID to set, as a string. + * @return This {@code NewOrderSingle} instance, to facilitate method chaining. + */ + public NewOrderSingle clOrdID(String clOrdID) { + this.clOrdID = clOrdID; + return this; + } + + /** + * Get the symbol of the financial instrument. + * + * @return The symbol as a long. + */ + public long symbol() { + return symbol; + } + + /** + * Sets the symbol of the financial instrument. + * + * @param symbol The symbol to set, as a long. + * @return This {@code NewOrderSingle} instance, to facilitate method chaining. + */ + public NewOrderSingle symbol(long symbol) { + this.symbol = symbol; + return this; + } + + /** + * Get the side of the order (buy or sell). + * + * @return The side as a {@link Side} enum value. + */ + public Side side() { + return side; + } + + /** + * Sets the side of the order (buy or sell). + * + * @param side The side to set, as a {@link Side} enum value. + * @return This {@code NewOrderSingle} instance, to facilitate method chaining. + */ + public NewOrderSingle side(Side side) { + this.side = side; + return this; + } + + /** + * Get the transaction time in nanoseconds. + * + * @return The transaction time as a long. + */ + public long transactTime() { + return transactTime; + } + + /** + * Sets the transaction time in nanoseconds. + * + * @param transactTime The transaction time to set, as a long. + * @return This {@code NewOrderSingle} instance, to facilitate method chaining. + */ + public NewOrderSingle transactTime(long transactTime) { + this.transactTime = transactTime; + return this; + } + + /** + * Get the quantity of the order. + * + * @return The order quantity as a double. + */ + public double orderQty() { + return orderQty; + } + + /** + * Sets the quantity of the order. + * + * @param orderQty The order quantity to set, as a double. + * @return This {@code NewOrderSingle} instance, to facilitate method chaining. + */ + public NewOrderSingle orderQty(double orderQty) { + this.orderQty = orderQty; + return this; + } + + /** + * Get the price of the order. + * + * @return The price as a double. + */ + public double price() { + return price; + } + + /** + * Sets the price of the order. + * + * @param price The price to set, as a double. + * @return This {@code NewOrderSingle} instance, to facilitate method chaining. + */ + public NewOrderSingle price(double price) { + this.price = price; + return this; + } + + /** + * Get the type of the order (market or limit). + * + * @return The order type as an {@link OrderType} enum value. + */ + public OrderType ordType() { + return ordType; + } + + /** + * Sets the type of the order (market or limit). + * + * @param ordType The order type to set, as an {@link OrderType} enum value. + * @return This {@code NewOrderSingle} instance, to facilitate method chaining. + */ + public NewOrderSingle ordType(OrderType ordType) { + this.ordType = ordType; + return this; + } + + /** + * Retrieves the time-in-force instruction for the order. + * + * @return The time-in-force as a {@link TimeInForce} enum value. + */ + public TimeInForce timeInForce() { + return timeInForce; + } + + /** + * Sets the time-in-force instruction for the order. + * + * @param timeInForce The time-in-force to set. + * @return This {@code NewOrderSingle} instance for method chaining. + */ + public NewOrderSingle timeInForce(TimeInForce timeInForce) { + this.timeInForce = timeInForce; + return this; + } + + /** + * Retrieves the account associated with the order. + * + * @return The account as a {@code long}. + */ + public long account() { + return account; + } + + /** + * Sets the account associated with the order. + * + * @param account The account to set. + * @return This {@code NewOrderSingle} instance for method chaining. + */ + public NewOrderSingle account(long account) { + this.account = account; + return this; + } + + /** + * Retrieves the currency of the order. + * + * @return The currency as a {@link Ccy} enum value. + */ + public Ccy currency() { + return currency; + } + + /** + * Sets the currency of the order. + * + * @param currency The currency to set. + * @return This {@code NewOrderSingle} instance for method chaining. + */ + public NewOrderSingle currency(Ccy currency) { + this.currency = currency; + return this; + } + + @Override + public void writeMarshallable(BytesOut out) { + super.writeMarshallable(out); + if (PREGENERATED_MARSHALLABLE) { + out.writeStopBit(MASHALLABLE_VERSION); + out.writeLong(symbol); + out.writeLong(transactTime); + out.writeLong(account); + out.writeDouble(orderQty); + out.writeDouble(price); + out.writeObject(Side.class, side); + out.writeObject(OrderType.class, ordType); + out.writeObject(String.class, clOrdID); + out.writeObject(TimeInForce.class, timeInForce); + out.writeObject(Ccy.class, currency); + } + } + + @Override + public void readMarshallable(BytesIn in) { + super.readMarshallable(in); + if (PREGENERATED_MARSHALLABLE) { + int version = (int) in.readStopBit(); + if (version == MASHALLABLE_VERSION) { + symbol = in.readLong(); + transactTime = in.readLong(); + account = in.readLong(); + orderQty = in.readDouble(); + price = in.readDouble(); + side = in.readObject(Side.class); + ordType = in.readObject(OrderType.class); + clOrdID = in.readObject(String.class); + timeInForce = in.readObject(TimeInForce.class); + currency = in.readObject(Ccy.class); + } + } + } + + @Override + public void writeMarshallable(WireOut out) { + super.writeMarshallable(out); + if (PREGENERATED_MARSHALLABLE) { + out.write("symbol").writeLong(ShortTextLongConverter.INSTANCE, symbol); + out.write("transactTime").writeLong(NanoTimestampLongConverter.INSTANCE, transactTime); + out.write("account").writeLong(ShortTextLongConverter.INSTANCE, account); + out.write("orderQty").writeDouble(orderQty); + out.write("price").writeDouble(price); + out.write("side").object(Side.class, side); + out.write("ordType").object(OrderType.class, ordType); + out.write("clOrdID").object(String.class, clOrdID); + out.write("timeInForce").object(TimeInForce.class, timeInForce); + out.write("currency").object(Ccy.class, currency); + } + } + + @Override + public void readMarshallable(WireIn in) { + super.readMarshallable(in); + if (PREGENERATED_MARSHALLABLE) { + symbol = in.read("symbol").readLong(ShortTextLongConverter.INSTANCE); + transactTime = in.read("transactTime").readLong(NanoTimestampLongConverter.INSTANCE); + account = in.read("account").readLong(ShortTextLongConverter.INSTANCE); + orderQty = in.read("orderQty").readDouble(); + price = in.read("price").readDouble(); + side = in.read("side").object(side, Side.class); + ordType = in.read("ordType").object(OrderType.class); + clOrdID = in.read("clOrdID").object(clOrdID, String.class); + timeInForce = in.read("timeInForce").object(TimeInForce.class); + currency = in.read("currency").object(Ccy.class); + } + } + + /** + * Validates the fields of this {@code NewOrderSingle} event. + * + * @throws InvalidMarshallableException if any required field is missing or invalid. + */ + @Override + public void validate() throws InvalidMarshallableException { + super.validate(); + if (symbol == 0) + throw new InvalidMarshallableException("symbol is required"); + if (transactTime == 0) + throw new InvalidMarshallableException("transactTime is required"); + if (account == 0) + throw new InvalidMarshallableException("account is required"); + if (orderQty == 0) + throw new InvalidMarshallableException("orderQty is required"); + if (invalidQuantity(orderQty)) + throw new InvalidMarshallableException("orderQty is invalid"); + if (invalidPrice(price)) + throw new InvalidMarshallableException("price is invalid"); + if (side == null) + throw new InvalidMarshallableException("side is required"); + if (ordType == null) + throw new InvalidMarshallableException("ordType is required"); + if (clOrdID == null || clOrdID.isEmpty()) + throw new InvalidMarshallableException("clOrdID is required"); + if (timeInForce == null) + throw new InvalidMarshallableException("timeInForce is required"); + if (currency == null) + throw new InvalidMarshallableException("currency is required"); + } +} +.... + +Lines 341, Blanks 36, Tokens 2886 + +== File: src\main\java\town\lost\oms\dto\OrderCancelReject.java +.... +/* + */ + +package town.lost.oms.dto; + +import net.openhft.chronicle.bytes.BytesIn; +import net.openhft.chronicle.bytes.BytesOut; +import net.openhft.chronicle.core.io.InvalidMarshallableException; +import net.openhft.chronicle.wire.ShortTextLongConverter; +import net.openhft.chronicle.wire.WireIn; +import net.openhft.chronicle.wire.WireOut; +import net.openhft.chronicle.wire.converter.ShortText; + +/** + * The {@code OrderCancelReject} class represents an event that is used to indicate that a request to cancel an order was rejected. + * + *

This class extends the {@link AbstractEvent} class, with the type parameter being {@link OrderCancelReject}. + * This indicates that the event will be processed into a {@link OrderCancelReject} that will be sent to the order + * system. + * + *

Each {@code OrderCancelReject} event contains: + *

    + *
  • symbol: The identifier for the financial instrument involved in the order.
  • + *
  • clOrdID: The client order ID of the order that was requested to be canceled.
  • + *
  • reason: A string explaining why the cancellation request was rejected.
  • + *
+ * + *

Note that the symbol field is encoded using {@link ShortText} to minimize the space required for storage and transmission. + */ +public class OrderCancelReject extends AbstractEvent { + private static final int MASHALLABLE_VERSION = 1; + // Symbol for which the order cancellation was rejected. + @ShortText + private long symbol; + + // Client order ID of the order that was requested to be canceled. + private String clOrdID = ""; + + // Reason for the rejection of the order cancellation request. + private String reason = ""; + + /** + * Get the client order ID of the order that was requested to be canceled. + * + * @return The client order ID as a string. + */ + public String clOrdID() { + return clOrdID; + } + + /** + * Set the client order ID of the order that was requested to be canceled. + * + * @param clOrdID The client order ID to set, as a string. + * @return This OrderCancelReject instance, to facilitate method chaining. + */ + public OrderCancelReject clOrdID(String clOrdID) { + this.clOrdID = clOrdID; + return this; + } + + /** + * Get the symbol for which the order cancellation was rejected. + * + * @return The symbol value as a {@code long}. + */ + public long symbol() { + return symbol; + } + + /** + * Set the symbol for which the order cancellation was rejected. + * + * @param symbol The symbol value to set, as a long. + * @return This {@code OrderCancelReject} instance, to facilitate method chaining. + */ + public OrderCancelReject symbol(long symbol) { + this.symbol = symbol; + return this; + } + + /** + * Get the reason for the rejection of the order cancellation request. + * + * @return The reason as a string. + */ + public String reason() { + return reason; + } + + /** + * Set the reason for the rejection of the order cancellation request. + * + * @param reason The reason to set, as a string. + * @return This {@code OrderCancelReject} instance, to facilitate method chaining. + */ + public OrderCancelReject reason(String reason) { + this.reason = reason; + return this; + } + + @Override + public void writeMarshallable(WireOut out) { + validate(); + super.writeMarshallable(out); + if (PREGENERATED_MARSHALLABLE) { + out.write("symbol").writeLong(ShortTextLongConverter.INSTANCE, symbol); + out.write("clOrdID").object(String.class, clOrdID); + out.write("reason").object(String.class, reason); + } + } + + @Override + public void readMarshallable(WireIn in) { + super.readMarshallable(in); + if (PREGENERATED_MARSHALLABLE) { + symbol = in.read("symbol").readLong(ShortTextLongConverter.INSTANCE); + clOrdID = in.read("clOrdID").object(clOrdID, String.class); + reason = in.read("reason").object(reason, String.class); + } + } + + @Override + public void writeMarshallable(BytesOut out) { + validate(); + super.writeMarshallable(out); + if (PREGENERATED_MARSHALLABLE) { + out.writeStopBit(MASHALLABLE_VERSION); + out.writeLong(symbol); + out.writeObject(String.class, clOrdID); + out.writeObject(String.class, reason); + } + } + + @Override + public void readMarshallable(BytesIn in) { + super.readMarshallable(in); + if (PREGENERATED_MARSHALLABLE) { + int version = (int) in.readStopBit(); + if (version == MASHALLABLE_VERSION) { + symbol = in.readLong(); + clOrdID = in.readObject(String.class); + reason = in.readObject(String.class); + } else { + throw new IllegalStateException("Unknown version " + version); + } + } + } + + /** + * Validates the fields of this {@code OrderCancelReject} event. + * + * @throws InvalidMarshallableException if any required field is missing or invalid. + */ + @Override + public void validate() throws InvalidMarshallableException { + super.validate(); + if (symbol == 0) + throw new InvalidMarshallableException("symbol is required"); + if (clOrdID == null || clOrdID.isEmpty()) + throw new InvalidMarshallableException("clOrdID is required"); + if (reason == null || reason.isEmpty()) + throw new InvalidMarshallableException("reason is required"); + } +} +.... + +Lines 149, Blanks 16, Tokens 1294 + +== File: src\main\java\town\lost\oms\dto\OrderType.java +.... +/* + */ + +package town.lost.oms.dto; +/** + * Enumeration for the types of orders in a trading system. + * + *

It contains two values: MARKET and LIMIT, which represent the two main types of orders that can be placed + * in a trading system. A MARKET order is an order to buy or sell a security at the current market price, + * whereas a LIMIT order is an order to buy or sell a security at a specific price or better. + * + *

This enumeration can be used when creating trading orders, like so: + * + *

+ * NewOrderSingle nos = new NewOrderSingle()
+ *    .sender(ShortText.parse("client"))
+ *    .target(ShortText.parse("OMS"))
+ *    .clOrdID("clOrdId")
+ *    .orderQty(1e6)
+ *    .price(1.6)
+ *    .symbol(ShortText.parse("AUDUSD"))
+ *    .ordType(OrderType.LIMIT)
+ *    .side(BuySell.BUY);
+ * 
+ * + *

Note that the order type is indicated by the {@link OrderType} used in the 'ordType' field of the order. + */ +public enum OrderType { + /** + * Market order type, which means the order should be executed at the current market price. + */ + MARKET, + + /** + * Limit order type, which means the order should be executed at a specific price or better. + */ + LIMIT, + + /** + * Pegged order type, where the price is pegged to a benchmark price, such as the best bid or ask. + */ + PEGGED, + + /** + * Fill or Kill order type, which must be executed immediately in its entirety or cancelled. + */ + FILL_OR_KILL, + + /** + * Immediate or Cancel order type, which executes all or part immediately and cancels any unfilled portion. + */ + IMMEDIATE_OR_CANCEL, +} + +.... + +Lines 48, Blanks 6, Tokens 409 + +== File: src\main\java\town\lost\oms\dto\package-info.java +.... +/** + * Provides the classes and interfaces necessary to create and manage Order Management System (OMS) data transfer objects (DTOs). + * + *

This package includes the following key components: + * + *

    + *
  • AbstractEvent and its subclasses, which represent different types of events that can occur in an OMS, such as: + *
      + *
    • {@link town.lost.oms.dto.NewOrderSingle}: Represents a new order submission.
    • + *
    • {@link town.lost.oms.dto.CancelOrderRequest}: Represents a request to cancel an existing order.
    • + *
    • {@link town.lost.oms.dto.ExecutionReport}: Represents the execution status of an order.
    • + *
    • {@link town.lost.oms.dto.OrderCancelReject}: Indicates that an order cancellation request was rejected.
    • + *
    • {@link town.lost.oms.dto.CancelAll}: Represents a request to cancel all orders for a specific symbol.
    • + *
    + *
  • + *
  • Enums that define constants for various order attributes: + *
      + *
    • {@link town.lost.oms.dto.Side}: Represents the side of an order (e.g., buy or sell).
    • + *
    • {@link town.lost.oms.dto.OrderType}: Represents the type of an order (e.g., market, limit).
    • + *
    • {@link town.lost.oms.dto.TimeInForce}: Represents the time-in-force instructions for an order.
    • + *
    • {@link town.lost.oms.dto.Ccy}: Represents currency codes as per ISO 4217.
    • + *
    + *
  • + *
  • Utility Classes: + *
      + *
    • {@link town.lost.oms.dto.ValidateUtil}: Provides utility methods for validating order parameters such as price and quantity.
    • + *
    + *
  • + *
+ * + *

Each class is designed to be marshalled and unmarshalled efficiently for high-performance data transfer, leveraging serialization optimizations provided by Chronicle Wire. + * + *

For more details, refer to the documentation of each individual class. + */ +package town.lost.oms.dto; +.... + +Lines 35, Blanks 0, Tokens 493 + +== File: src\main\java\town\lost\oms\dto\Side.java +.... +/* + */ + +package town.lost.oms.dto; + +/** + * Enumeration for the direction of a trade order. + * + *

It contains two values: BUY and SELL, which represents the direction of the order. BUY (+1) means the order is to + * purchase, while SELL (-1) implies the order is to sell. + * + *

This enumeration can be used to create trading orders, like so: + * + *

{@code
+ * NewOrderSingle nos = new NewOrderSingle()
+ *    .sender(toLong("sender"))
+ *    .target(toLong("target"))
+ *    .transactTime(now())
+ *    .sendingTime(now())
+ *    .orderQty(1)
+ *    .ordType(OrderType.MARKET)
+ *    .side(Side.BUY)
+ *    .symbol(toLong("EURUSD"));
+ * }
+ * + *

Note that the direction is indicated by the {@link Side} used in the 'side' field of the order. + */ +public enum Side { + /** + * Buy order direction, represented by an integer value of +1. + * Indicates an order to purchase. + */ + BUY(+1), + + /** + * Sell order direction, represented by an integer value of -1. + * Indicates an order to sell. + */ + SELL(-1); + + /** + * The direction of the order. + */ + public final int direction; + + /** + * Constructs a BuySell enum with the specified direction. + * + * @param direction the direction of the order (+1 for buy, -1 for sell) + */ + Side(int direction) { + this.direction = direction; + } + + /** + * Gets the direction indicator of the order. + * + * @return the direction indicator as an integer + */ + public int direction() { + return direction; + } + } +.... + +Lines 57, Blanks 6, Tokens 392 + +== File: src\main\java\town\lost\oms\dto\TimeInForce.java +.... +package town.lost.oms.dto; + +/** + * The {@code TimeInForce} enum represents the time conditions under which an order will be executed. + * + *

These conditions specify how long an order remains active in the market before it is executed or expires. + * This enum includes commonly used time-in-force instructions in trading systems, conforming to standard trading practices. + * + *

Example usage: + *

{@code
+ * TimeInForce tif = TimeInForce.DAY;
+ * System.out.println("Time in Force: " + tif);
+ * }
+ * + *

This would output: + *

+ * Time in Force: DAY
+ * 
+ */ +public enum TimeInForce { + /** + * Indicates that the order should be valid for the current trading day only. + * If not executed by the end of the trading day, the order expires. + */ + DAY, + + /** + * "Good Till Cancelled" - The order remains active until it is either executed or explicitly cancelled by the trader. + * It may carry over to the next trading day. + */ + GTC, + + /** + * "Immediate or Cancel" - The order attempts to execute all or part immediately. + * Any portion not immediately executed is cancelled. + */ + IOC, + + /** + * "Fill or Kill" - The order must be executed immediately in its entirety, or it is cancelled. + * Partial fills are not allowed. + */ + FOK, + + /** + * "Good Till Date" - The order remains active until a specified date unless it is executed or cancelled before then. + */ + GTD, + + /** + * "Good Till Time" - The order remains active until a specified time on the current trading day. + * If not executed by that time, the order expires. + */ + GTT, + + /** + * "At the Opening" - The order is to be executed at the opening of the market. + * If not executed at the opening, it is cancelled. + */ + OPG, + + /** + * "At the Close" - The order is to be executed at the close of the market. + * If not executed at the close, it is cancelled. + */ + ATC, + + /** + * "Good for Auction" - The order is valid only during the auction period. + */ + GFA, + + /** + * "Good in Session" - The order is valid only during the specified trading session. + */ + GIS, + + /** + * "Good Through Crossing" - The order is valid through the crossing session. + */ + GTX, + + /** + * "Systematic Internaliser Only" - The order is to be executed only on a systematic internaliser. + */ + SIO +} +.... + +Lines 75, Blanks 12, Tokens 586 + +== File: src\main\java\town\lost\oms\dto\ValidateUtil.java +.... +/* + */ +package town.lost.oms.dto; + +/** + * The {@code ValidateUtil} class provides utility methods for validating numerical values such as price and quantity. + * + *

This class is final and cannot be instantiated. It contains static methods that check whether a given price or quantity + * is valid according to predefined business rules. + * + *

Example usage: + * + *

{@code
+ * double price = 100.0;
+ * if (ValidateUtil.invalidPrice(price)) {
+ *     throw new IllegalArgumentException("Invalid price");
+ * }
+ * }
+ */ +public final class ValidateUtil { + + /** + * Private constructor to prevent instantiation. + */ + private ValidateUtil() { + } + + /** + * Checks if the provided price is invalid. + * + *

A price is considered invalid if it is not a finite number or if it is less than or equal to zero. + * + * @param price the price to validate + * @return {@code true} if the price is invalid; {@code false} otherwise + */ + public static boolean invalidPrice(double price) { + return !Double.isFinite(price) || (price <= 0); + } + + /** + * Checks if the provided quantity is invalid. + * + *

A quantity is considered invalid if it is not a finite number or if it is less than zero. + * + * @param quantity the quantity to validate + * @return {@code true} if the quantity is invalid; {@code false} otherwise + */ + public static boolean invalidQuantity(double quantity) { + return !Double.isFinite(quantity) || (quantity < 0); + } +} +.... + +Lines 47, Blanks 4, Tokens 352 + +== File: src\main\java\town\lost\oms\OMSBenchmarkMain.java +.... +/* + */ + +package town.lost.oms; + +import net.openhft.affinity.AffinityLock; +import net.openhft.chronicle.bytes.MethodReader; +import net.openhft.chronicle.core.Jvm; +import net.openhft.chronicle.core.OS; +import net.openhft.chronicle.core.io.BackgroundResourceReleaser; +import net.openhft.chronicle.core.io.IOTools; +import net.openhft.chronicle.jlbh.JLBH; +import net.openhft.chronicle.jlbh.JLBHOptions; +import net.openhft.chronicle.jlbh.JLBHTask; +import net.openhft.chronicle.queue.ChronicleQueue; +import net.openhft.chronicle.threads.DiskSpaceMonitor; +import net.openhft.chronicle.wire.ShortTextLongConverter; +import town.lost.oms.api.OMSIn; +import town.lost.oms.api.OMSOut; +import town.lost.oms.dto.*; + +import static town.lost.oms.OrderAdderMain.now; +// isolcpus=5,6,7 set in grub.cfg +// sudo cpupower frequency-set -g performance -d 4.5g + +// -Xmx64m -XX:+UnlockCommercialFeatures -XX:+FlightRecorder -XX:StartFlightRecording=name=test,filename=test.jfr,dumponexit=true,settings=profile -XX:-UseTLAB +/* Run on a Ryzen 9 5950X, Ubuntu 20.10 +-Xmx1g -DbyteInBinary=true -DpregeneratedMarshallable=true -Dthroughput=100000 -DrunTime=120 -Dpath=/tmp +-------------------------------- SUMMARY (end to end) us ------------------------------------------- +Percentile run1 run2 run3 run4 run5 % Variation +50.0: 2.06 1.99 1.99 1.99 1.99 0.00 +90.0: 2.13 2.04 2.04 2.05 2.05 0.33 +99.0: 3.16 3.09 3.09 3.10 3.10 0.17 +99.7: 3.66 3.57 3.58 3.58 3.57 0.15 +99.9: 4.02 3.92 3.90 3.90 3.90 0.41 +99.97: 4.78 4.66 4.65 4.70 4.65 0.68 +99.99: 5.58 5.29 5.21 5.34 5.14 2.43 +99.997: 8.56 6.01 5.88 6.34 5.90 5.00 +99.999: 9.30 6.81 6.42 9.39 6.47 23.55 +worst: 50.11 74.11 10.58 13.30 11.15 80.02 +---------------------------------------------------------------------------------------------------- + +-Xmx772m -DbyteInBinary=true -DpregeneratedMarshallable=true -Dthroughput=100000 -DrunTime=120 -Dpath=/nvme/tmp +-------------------------------- SUMMARY (end to end) us ------------------------------------------- +Percentile run1 run2 run3 run4 run5 % Variation +50.0: 1.98 1.97 1.97 1.97 1.97 0.00 +90.0: 2.05 2.04 2.04 2.04 2.04 0.00 +99.0: 3.38 3.39 3.36 3.34 3.34 0.95 +99.7: 6.34 6.38 5.80 5.45 5.46 10.20 +99.9: 16.27 16.42 16.48 16.42 16.48 0.26 +99.97: 17.38 17.38 17.38 17.25 17.38 0.49 +99.99: 18.91 18.46 18.66 18.21 18.66 1.61 +99.997: 24.29 22.24 22.82 21.09 25.12 11.31 +99.999: 29.15 27.94 26.40 25.76 402.94 90.71 +worst: 214.27 51.78 537.60 50.24 1255.42 94.12 +---------------------------------------------------------------------------------------------------- + */ +public class OMSBenchmarkMain { + static { + System.setProperty("pauser.minProcessors", "2"); + System.setProperty("disableValidate", "true"); + } + + public static final int THROUGHPUT = Integer.getInteger("throughput", 100_000); + public static final int RUN_TIME = Integer.getInteger("runTime", 10); + public static final ShortTextLongConverter ShortText = ShortTextLongConverter.INSTANCE; + public static final String PATH = System.getProperty("path", OS.TMP); + public static final boolean ACCOUNT_FOR_COORDINATED_OMISSION = Jvm.getBoolean("accountForCoordinatedOmission"); + + @SuppressWarnings("try") + public static void main(String[] args) { + printProperties(); + + String tmpDir = PATH + "/bench-" + System.nanoTime(); + try (ChronicleQueue input = single(tmpDir, "/input"); + ChronicleQueue output = single(tmpDir, "/output")) { + + // processing thread + Thread processor = new Thread(() -> { + try (AffinityLock ignored = AffinityLock.acquireCore()) { + OMSOut out = output.createAppender().methodWriter(OMSOut.class); + OMSImpl oms = new OMSImpl(out); + MethodReader in = input.createTailer("test").methodReader(oms); + while (!Thread.currentThread().isInterrupted()) { + in.readOne(); + } + } + }, "processor"); + processor.start(); + + JLBH jlbh = new JLBH(new JLBHOptions() + .warmUpIterations(300_000) + .pauseAfterWarmupMS(500) + .throughput(THROUGHPUT) + .iterations(THROUGHPUT * RUN_TIME) + .runs(5) + .recordOSJitter(false) + .accountForCoordinatedOmission(ACCOUNT_FOR_COORDINATED_OMISSION) + .acquireLock(AffinityLock::acquireCore) + .jlbhTask(new MyJLBHTask(input))); + + Thread last = new Thread(() -> { + try (AffinityLock ignored = AffinityLock.acquireCore()) { + final MethodReader reader = output.createTailer().methodReader(new OMSOut() { + @Override + public void executionReport(ExecutionReport er) { + jlbh.sampleNanos(System.nanoTime() - er.sendingTime()); + } + + @Override + public void orderCancelReject(OrderCancelReject ocr) { + } + + @Override + public void jvmError(String msg) { + } + }); + while (!Thread.currentThread().isInterrupted()) + reader.readOne(); + + } catch (Throwable t) { + t.printStackTrace(); + } + }, "last"); + last.start(); + + jlbh.start(); + + processor.interrupt(); + last.interrupt(); + } + printProperties(); + Jvm.pause(1000); + IOTools.deleteDirWithFiles(tmpDir); + // for a clean shutdown + BackgroundResourceReleaser.stop(); + DiskSpaceMonitor.INSTANCE.close(); + } + + static ChronicleQueue single(String tmpDir, String x) { + return ChronicleQueue.singleBuilder(tmpDir + x) + .blockSize(OS.isSparseFileSupported() ? 64L << 30 : 64L << 20) + .build(); + } + + private static void printProperties() { + long estimatedMemory = Math.round(Runtime.getRuntime().totalMemory() / 1e6); + System.out.println("-Xmx" + estimatedMemory + "m " + + "-DbyteInBinary=" + AbstractEvent.BYTES_IN_BINARY + " " + + "-DpregeneratedMarshallable=" + AbstractEvent.PREGENERATED_MARSHALLABLE + " " + + "-Dthroughput=" + THROUGHPUT + " " + + "-DrunTime=" + RUN_TIME + " " + + "-Dpath=" + PATH + " " + + "-DaccountForCoordinatedOmission=" + ACCOUNT_FOR_COORDINATED_OMISSION); + } + + private static class MyJLBHTask implements JLBHTask { + private JLBH jlbh; + private NewOrderSingle nos; + private OMSIn in; + + public MyJLBHTask(ChronicleQueue input) { + nos = new NewOrderSingle() + .sender(ShortText.parse("client")) + .target(ShortText.parse("OMS")) + .clOrdID("clOrdId") + .orderQty(1e6) + .price(1.6) + .symbol(ShortText.parse("AUDUSD")) + .ordType(OrderType.LIMIT) + .side(Side.SELL); + in = input.createAppender().methodWriter(OMSIn.class); + } + + @Override + public void init(JLBH jlbh) { + this.jlbh = jlbh; + } + + @Override + public void run(long startTimeNS) { + nos.sendingTime(startTimeNS).transactTime(now()); + in.newOrderSingle(nos); + } + } +} +.... + +Lines 164, Blanks 22, Tokens 2236 + +== File: src\main\java\town\lost\oms\OMSImpl.java +.... +/* + */ + +package town.lost.oms; + +import net.openhft.chronicle.core.time.SystemTimeProvider; +import town.lost.oms.api.OMSIn; +import town.lost.oms.api.OMSOut; +import town.lost.oms.dto.*; + +/** + * The {@code OMSImpl} class is a concrete implementation of the {@link OMSIn} interface, + * acting as an intermediary between client requests and the {@link OMSOut} interface. + * + *

This class processes requests for order creation, order cancellation, and cancelling all orders. + * It generates appropriate responses using {@link ExecutionReport} and {@link OrderCancelReject} + * and sends them through the {@link OMSOut} interface. + * + *

Note: This class is not thread-safe. If multiple threads are expected to use + * the same instance of {@code OMSImpl}, synchronization or separate instances per thread should be used. + * + *

Usage Example:

+ * + *
{@code
+ * OMSOut omsOut = new OMSOutImplementation();
+ * OMSIn oms = new OMSImpl(omsOut);
+ *
+ * NewOrderSingle newOrder = new NewOrderSingle()
+ *     .sender("ClientA")
+ *     .target("OMS")
+ *     .clOrdID("Order123")
+ *     .symbol("AAPL")
+ *     .orderQty(100)
+ *     .price(150.00)
+ *     .side(Side.BUY)
+ *     .ordType(OrderType.MARKET)
+ *     .transactTime(System.currentTimeMillis());
+ *
+ * oms.newOrderSingle(newOrder);
+ * }
+ */ +public class OMSImpl implements OMSIn { + // The outbound interface for sending execution reports and order cancel rejections + private final OMSOut out; + + // Reusable instance of ExecutionReport for creating new orders + private final ExecutionReport er = new ExecutionReport(); + + // Reusable instance of OrderCancelReject for cancelling orders + private final OrderCancelReject ocr = new OrderCancelReject(); + + /** + * Constructs a new {@code OMSImpl} with a given outbound interface. + * + * @param out the outbound interface to be used for sending responses + */ + public OMSImpl(OMSOut out) { + this.out = out; + } + + /** + * Processes a new single order request. The request's details are populated into an execution report and sent out. + * + * @param nos the {@link NewOrderSingle} request to process + */ + @Override + public void newOrderSingle(NewOrderSingle nos) { + // Reset the execution report DTO + er.reset(); + final long orderID = SystemTimeProvider.CLOCK.currentTimeNanos(); // Generate unique order ID + + // Populate the ExecutionReport with request details + er.sender(nos.target()) + .target(nos.sender()) + .symbol(nos.symbol()) + .clOrdID(nos.clOrdID()) + .ordType(nos.ordType()) + .orderQty(nos.orderQty()) + .price(nos.price()) + .side(nos.side()) + .sendingTime(nos.sendingTime()) + .transactTime(nos.transactTime()) + .leavesQty(0) + .cumQty(0) + .avgPx(0) + .orderID(orderID) + .text("Not ready"); + + // Send execution report + out.executionReport(er); + } + + /** + * Processes a cancel order request. The request's details are populated into an order cancel rejection and sent out. + * + * @param cor the {@link CancelOrderRequest} request to process + */ + @Override + public void cancelOrderRequest(CancelOrderRequest cor) { + // Reset the reusable OrderCancelReject instance + ocr.reset(); + // Populate OrderCancelReject with request details + ocr.sender(cor.target()) + .target(cor.sender()) + .symbol(cor.symbol()) + .clOrdID(cor.clOrdID()) + .sendingTime(cor.sendingTime()) + .reason("No such order"); + + // Send order cancellation rejection + out.orderCancelReject(ocr); + } + + /** + * Processes a cancel all orders request. The request's details are populated into an order cancel rejection and sent out. + * + * @param cancelAll the {@link CancelAll} request to process + */ + @Override + public void cancelAll(CancelAll cancelAll) { + // Reset the reusable OrderCancelReject instance + ocr.reset(); + // Populate OrderCancelReject with request details + ocr.sender(cancelAll.target()) + .target(cancelAll.sender()) + .symbol(cancelAll.symbol()) + .clOrdID(cancelAll.clOrdID()) + .sendingTime(cancelAll.sendingTime()) + .reason("No orders to cancel"); + + // Send order cancellation rejection + out.orderCancelReject(ocr); + } +} +.... + +Lines 121, Blanks 13, Tokens 1041 + +== File: src\main\java\town\lost\oms\OrderAdderMain.java +.... +/* + */ + +package town.lost.oms; + +import net.openhft.chronicle.core.pool.ClassAliasPool; +import net.openhft.chronicle.core.time.SystemTimeProvider; +import net.openhft.chronicle.core.util.Mocker; +import net.openhft.chronicle.queue.ChronicleQueue; +import net.openhft.chronicle.queue.ExcerptAppender; +import net.openhft.chronicle.queue.impl.single.SingleChronicleQueueBuilder; +import net.openhft.chronicle.queue.rollcycles.TestRollCycles; +import net.openhft.chronicle.wire.converter.ShortText; +import town.lost.oms.api.OMSIn; +import town.lost.oms.dto.*; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; + +/** + * The {@code OrderAdderMain} class is a utility application that allows users to add new orders to the Order Management System (OMS). + * + *

This class connects to a Chronicle Queue and writes {@link NewOrderSingle} messages to it. + * Users can interactively add orders by hitting the Enter key. The application will prompt the user + * and continue to accept orders until any non-empty input is entered. + */ +public class OrderAdderMain { + + /** + * The entry point of the application. + * + * @param args the input arguments (none expected) + */ + public static void main(String[] args) throws IOException { + // Add NewOrderSingle class to the alias pool + ClassAliasPool.CLASS_ALIASES.addAlias(NewOrderSingle.class); + + // Establish connection with the queue + try (ChronicleQueue q = SingleChronicleQueueBuilder.binary("in") + .rollCycle(TestRollCycles.TEST8_DAILY) + .build(); + ExcerptAppender appender = q.createAppender()) { + + // Acquire the appender and write methods for OMSIn + OMSIn in = appender.methodWriter(OMSIn.class); + + // Create a logging mock for OMSIn + OMSIn in2 = Mocker.logging(OMSIn.class, "in - ", System.out); + + // Create a new order single + NewOrderSingle nos = new NewOrderSingle() + .sender(fromShortText("sender")) + .target(fromShortText("target")) + .transactTime(now()) + .sendingTime(now()) + .account(1) + .timeInForce(TimeInForce.GTC) + .currency(Ccy.USD) + .orderQty(1) + .ordType(OrderType.MARKET) + .side(Side.BUY) + .symbol(fromShortText("EURUSD")); + + // Inform the user to add an order + System.out.println("\nHit blank line to add an order, anything else to exit"); + + // Initialize a BufferedReader to read user input + BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); + int orderCounter = 0; + while ("".equals(br.readLine())) { + // For each blank line read, add a new order + nos.clOrdID(Long.toString(orderCounter++)); + + // Send the new order + in.newOrderSingle(nos); + in2.newOrderSingle(nos); + } + } + // Inform the user that the program has finished running + System.out.println("Finished"); + + // Exit the program + System.exit(0); + } + + /** + * Returns the current system time in nanoseconds. + * + * @return the current time in nanoseconds + */ + static long now() { + return SystemTimeProvider.INSTANCE.currentTimeNanos(); + } + + /** + * Converts a string to a long using base85 encoding. + * + * @param s the string to convert + * @return the long representation of the string + */ + static long fromShortText(String s) { + return ShortText.INSTANCE.parse(s); + } +} +.... + +Lines 90, Blanks 15, Tokens 822 + +== File: src\main\java\town\lost\oms\OrderViewerMain.java +.... +/* + */ + +package town.lost.oms; + +import net.openhft.chronicle.bytes.MethodReader; +import net.openhft.chronicle.core.Jvm; +import net.openhft.chronicle.core.util.Mocker; +import net.openhft.chronicle.queue.ChronicleQueue; +import net.openhft.chronicle.queue.impl.single.SingleChronicleQueueBuilder; +import net.openhft.chronicle.queue.rollcycles.TestRollCycles; +import town.lost.oms.api.OMSIn; + +/** + * The {@code OrderViewerMain} class is a utility application that continuously reads and displays + * orders from a Chronicle Queue. It acts as a simple viewer for incoming orders, logging them to + * the console. + */ +public class OrderViewerMain { + + /** + * The entry point of the application. + * + * @param args the input arguments (none expected) + */ + public static void main(String[] args) { + // Inform the user that the program is waiting for messages + System.out.println("\nWaiting for messages..."); + + // Establish connection with the queue + try (ChronicleQueue q = SingleChronicleQueueBuilder.binary("in") + .rollCycle(TestRollCycles.TEST8_DAILY) + .build()) { + + // Create a logging mock for OMSIn + OMSIn logging = Mocker.logging(OMSIn.class, "read - ", System.out); + + // Create a MethodReader from the tail of the queue + MethodReader reader = q.createTailer().methodReader(logging); + + // Continuously read messages from the queue + while (true) { + // Read one message from the queue; pause if no message was read + if (!reader.readOne()) { + Jvm.pause(50); + } + } + } + } +} +.... + +Lines 42, Blanks 8, Tokens 378 + +== File: src\test\java\town\lost\oms\OMSImplTest.java +.... +/* + */ + +package town.lost.oms; + +import net.openhft.chronicle.core.time.SetTimeProvider; +import net.openhft.chronicle.core.time.SystemTimeProvider; +import org.junit.After; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import town.lost.oms.api.OMSOut; + +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.assertEquals; + +/** + * Test class for OMSImpl. + * The OMSImplTest runs tests for each method in OMSImpl class. + * The test data is read from specified files and the actual output is compared against expected output. + */ +@SuppressWarnings("deprecation") +@RunWith(Parameterized.class) +public class OMSImplTest { + // Defines the paths to the tests to run. + static final List paths = Arrays.asList(new String[]{ + "newOrderSingle", + "newOrderSingleEquity", + "cancelOrderRequest", + "cancelAll" + }); + + // The name of the test, and the tester that will run the test. + final String name; + final net.openhft.chronicle.wire.utils.YamlTester tester; + + // Constructor that sets the name and tester. + public OMSImplTest(String name, net.openhft.chronicle.wire.utils.YamlTester tester) { + this.name = name; + this.tester = tester; + } + + // Defines the parameters for the parameterized test runner. + @Parameterized.Parameters(name = "{0}") + public static List parameters() { + // Returns a list of test parameters to run the tests with. + // Each test will be run with an instance of AccountManagerImpl, + // and will be subjected to various agitations to ensure robustness. + return new net.openhft.chronicle.wire.utils.YamlTesterParametersBuilder<>(out -> new OMSImpl(out), OMSOut.class, paths) + .agitators( + net.openhft.chronicle.wire.utils.YamlAgitator.messageMissing(), + net.openhft.chronicle.wire.utils.YamlAgitator.duplicateMessage(), + net.openhft.chronicle.wire.utils.YamlAgitator.overrideFields("sendingTime: '', symbol: '', side: '', orderQty: NaN, orderQty: -1, price: NaN, price: -1, clOrdId: '', ordType: ''".split(", *")), + net.openhft.chronicle.wire.utils.YamlAgitator.missingFields("sender, target, sendingTime, symbol, transactTime, account, orderQty, price, side, clOrdID, ordType, timeInForce, currency".split(", *"))) + .exceptionHandlerFunction(out -> (log, msg, thrown) -> out.jvmError(thrown == null ? msg : (msg + " " + thrown))) + .exceptionHandlerFunctionAndLog(true) + .get(); + } + + // After each test, this method resets the system time provider. + @After + public void tearDown() { + SystemTimeProvider.CLOCK = SystemTimeProvider.INSTANCE; + } + + // This is the actual test method, which uses the provided tester + // to run the test and then compares the expected output to the actual output. + @Test + public void runTester() { + // Sets the system clock to a specific time for the purpose of testing. + SystemTimeProvider.CLOCK = new SetTimeProvider("2019-12-03T09:54:37.345679") + .autoIncrement(1, TimeUnit.SECONDS); + // Asserts that the expected output matches the actual output. + assertEquals(tester.expected(), tester.actual()); + } +} +.... + +Lines 69, Blanks 10, Tokens 791 + +== File: src\test\resources\cancelAll\in.yaml +.... +--- +# This is a test for the CancelOrderRequest operation. +# The CLIENT1 (sender) is sending a request to the Order Management System (OMS1, the target) +# to cancel an order that was previously placed. The order is identified by its unique ID (clOrdID). + +cancelOrderRequest: { + sender: CLIENT1, + target: OMS1, + sendingTime: 2019-12-03T09:54:37.134475, + symbol: EURUSD, + account: ACC-12345, + clOrdID: dkj4378fwh, + origClOrdID: ABC1234567, + side: BUY +} +... +--- +# This is a test for the CancelAll operation. +# The CLIENT1 (sender) sends a request to the OMS1 (target) to cancel all orders. +cancelAll: { + sender: CLIENT1, + target: OMS1, + sendingTime: 2019-12-03T09:54:37.134475, + symbol: EURUSD, + clOrdID: CANCEL_ALL, +} +... +.... + +Lines 26, Blanks 1, Tokens 229 + +== File: src\test\resources\cancelAll\out.yaml +.... +# This is a test for the CancelOrderRequest operation. +# The CLIENT1 (sender) is sending a request to the Order Management System (OMS1, the target) +# to cancel an order that was previously placed. The order is identified by its unique ID (clOrdID). +--- +orderCancelReject: { + sender: OMS1, + target: CLIENT1, + sendingTime: 2019-12-03T09:54:37.134475, + symbol: EURUSD, + clOrdID: dkj4378fwh, + reason: No such order +} +... +# This is a test for the CancelAll operation. +# The CLIENT1 (sender) sends a request to the OMS1 (target) to cancel all orders. +--- +orderCancelReject: { + sender: OMS1, + target: CLIENT1, + sendingTime: 2019-12-03T09:54:37.134475, + symbol: EURUSD, + clOrdID: CANCEL_ALL, + reason: No orders to cancel +} +... +.... + +Lines 25, Blanks 0, Tokens 220 + +== File: src\test\resources\cancelOrderRequest\in.yaml +.... +--- +# This is a test for the CancelOrderRequest operation. +# The CLIENT1 (sender) is sending a request to the Order Management System (OMS1, the target) +# to cancel an order that was previously placed. The order is identified by its unique ID (clOrdID). + +cancelOrderRequest: { + sender: CLIENT1, + target: OMS1, + sendingTime: 2019-12-03T09:54:37.134475, + symbol: EURUSD, + account: ACC-12345, + clOrdID: dkj4378fwh, + origClOrdID: ABC1234567, + side: BUY +} +... +.... + +Lines 15, Blanks 1, Tokens 142 + +== File: src\test\resources\cancelOrderRequest\out.yaml +.... +# This is a test for the CancelOrderRequest operation. +# The CLIENT1 (sender) is sending a request to the Order Management System (OMS1, the target) +# to cancel an order that was previously placed. The order is identified by its unique ID (clOrdID). +--- +orderCancelReject: { + sender: OMS1, + target: CLIENT1, + sendingTime: 2019-12-03T09:54:37.134475, + symbol: EURUSD, + clOrdID: dkj4378fwh, + reason: No such order +} +... +.... + +Lines 13, Blanks 0, Tokens 124 + +== File: src\test\resources\newOrderSingle\in.yaml +.... +--- +# This is a test for the NewOrderSingle operation. +# The client (sender) is sending a new order to the Order Management System (OMS, the target). +# The order details such as the trading symbol, order quantity, price, and the order type are specified. + +newOrderSingle: { + sender: CLIENT, + target: OMS, + sendingTime: 2019-12-03T09:54:37.134475, + symbol: EURUSD, + transactTime: 2019-12-03T09:54:37.344751, + account: ACC-12345, + orderQty: 10E6, + price: 1.2123, + side: BUY, + clOrdID: dkj4378fwh, + ordType: LIMIT, + timeInForce: GTC, + currency: USD +} +... + +.... + +Lines 20, Blanks 2, Tokens 186 + +== File: src\test\resources\newOrderSingle\out.yaml +.... +# This is a test for the NewOrderSingle operation. +# The client (sender) is sending a new order to the Order Management System (OMS, the target). +# The order details such as the trading symbol, order quantity, price, and the order type are specified. +--- +executionReport: { + sender: OMS, + target: CLIENT, + sendingTime: 2019-12-03T09:54:37.134475, + symbol: EURUSD, + transactTime: 2019-12-03T09:54:37.344751, + orderQty: 10E6, + price: 1.2123, + orderID: 2019-12-03T09:54:37.345679, + lastPx: 0.0, + leavesQty: 0.0, + cumQty: 0.0, + avgPx: 0.0, + side: BUY, + ordType: LIMIT, + clOrdID: dkj4378fwh, + text: Not ready +} +... +.... + +Lines 23, Blanks 0, Tokens 225 + +== File: src\test\resources\newOrderSingleEquity\in.yaml +.... +--- +# Sample `NewOrderSingle` message representing a client's new order sent to the Order Management System (OMS). +newOrderSingle: { + sender: CLIENT1, + target: OMS1, + sendingTime: 2023-10-20T14:25:37.134475Z, + symbol: AAPL, + transactTime: 2023-10-20T14:25:37.134475Z, + account: ACC-12345, + orderQty: 1000, + price: 150.25, + side: buy, + clOrdID: ABC1234567, + ordType: LIMIT, + timeInForce: DAY, + currency: USD +} +... +--- +# Sample `NewOrderSingle` message representing another client's new order sent to the Order Management System (OMS). +newOrderSingle: { + sender: CLIENT2, + target: OMS1, + sendingTime: 2023-10-20T14:27:15.987654Z, + symbol: GOOGL, + transactTime: 2023-10-20T14:27:15.987654Z, + account: ACC-67890, + orderQty: 500, + price: 2750.50, + side: sell, + clOrdID: XYZ7654321, + ordType: MARKET, + timeInForce: DAY, + currency: USD +} +... +.... + +Lines 36, Blanks 0, Tokens 299 + +== File: src\test\resources\newOrderSingleEquity\out.yaml +.... +# Sample `NewOrderSingle` message representing a client's new order sent to the Order Management System (OMS). +--- +executionReport: { + sender: OMS1, + target: CLIENT1, + sendingTime: 2023-10-20T14:25:37.134475, + symbol: AAPL, + transactTime: 2023-10-20T14:25:37.134475, + orderQty: 1E3, + price: 150.25, + orderID: 2019-12-03T09:54:37.345679, + lastPx: 0.0, + leavesQty: 0.0, + cumQty: 0.0, + avgPx: 0.0, + side: BUY, + ordType: LIMIT, + clOrdID: ABC1234567, + text: Not ready +} +... +# Sample `NewOrderSingle` message representing another client's new order sent to the Order Management System (OMS). +--- +executionReport: { + sender: OMS1, + target: CLIENT2, + sendingTime: 2023-10-20T14:27:15.987654, + symbol: GOOGL, + transactTime: 2023-10-20T14:27:15.987654, + orderQty: 500.0, + price: 2750.5, + orderID: 2019-12-03T09:54:38.345679, + lastPx: 0.0, + leavesQty: 0.0, + cumQty: 0.0, + avgPx: 0.0, + side: SELL, + ordType: MARKET, + clOrdID: XYZ7654321, + text: Not ready +} +... +.... + +Lines 42, Blanks 0, Tokens 382 + +== Summary == +Total Lines: 2833 +Total Blanks: 298 +Total Tokens: 25169 +Tokens/Line: 8.9 From a3bdf1384a707b3d8b8f4f37fb65c60931017d99 Mon Sep 17 00:00:00 2001 From: Peter Lawrey Date: Thu, 2 Jan 2025 09:20:51 +0000 Subject: [PATCH 22/31] Updated requirements and adoc --- .gitignore | 4 + README.adoc | 62 +++- account.ad | 63 ++++ {order-processor/aide => aide}/README.adoc | 0 .../aide => aide}/aide-glossary.adoc | 0 .../aide => aide}/aide-style-guide.adoc | 0 .../aide => aide}/aide-workflow.adoc | 0 .../aide => aide}/notes-on-using-aide.adoc | 0 architecture.adoc | 119 +++++++ .../main/adoc/event-routing-requirements.adoc | 110 ++++++ .../chronicle/routing/inout/ViaThreeFive.java | 10 +- .../chronicle/routing/inout/api/Value.java | 18 +- .../routing/inout/api/ValueMessage.java | 6 +- .../chronicle/routing/inout/api/ViaIn.java | 7 +- .../chronicle/routing/inout/api/ViaOut.java | 7 +- .../routing/inout/api/package-info.java | 8 +- .../run/chronicle/routing/out/SifterImpl.java | 7 +- .../chronicle/routing/out/package-info.java | 4 +- .../routing/inout/ViaThreeFiveTest.java | 13 +- .../test/resources/three-five-values/in.yaml | 20 ++ .../test/resources/three-five-values/out.yaml | 33 ++ hello-world/README.adoc | 170 ++++++---- .../main/adoc/hello-world-requirements.adoc | 124 +++++++ .../src/main/hello-world-requirements.adoc | 0 images/{Clone.PNG => Clone.png} | Bin ...wo-hop-latency.PNG => Two-hop-latency.png} | Bin images/{directory.PNG => directory.png} | Bin order-processor.ad | 88 +++++ order-processor/aide/examples.adoc | 316 ------------------ reference.adoc | 98 ++++++ usage-and-tests.adoc | 216 ++++++++++++ 31 files changed, 1087 insertions(+), 416 deletions(-) create mode 100644 account.ad rename {order-processor/aide => aide}/README.adoc (100%) rename {order-processor/aide => aide}/aide-glossary.adoc (100%) rename {order-processor/aide => aide}/aide-style-guide.adoc (100%) rename {order-processor/aide => aide}/aide-workflow.adoc (100%) rename {order-processor/aide => aide}/notes-on-using-aide.adoc (100%) create mode 100644 architecture.adoc create mode 100644 event-routing/src/main/adoc/event-routing-requirements.adoc mode change 100755 => 100644 event-routing/src/test/java/run/chronicle/routing/inout/ViaThreeFiveTest.java create mode 100644 event-routing/src/test/resources/three-five-values/in.yaml create mode 100644 event-routing/src/test/resources/three-five-values/out.yaml create mode 100644 hello-world/src/main/adoc/hello-world-requirements.adoc create mode 100644 hello-world/src/main/hello-world-requirements.adoc rename images/{Clone.PNG => Clone.png} (100%) rename images/{Two-hop-latency.PNG => Two-hop-latency.png} (100%) rename images/{directory.PNG => directory.png} (100%) create mode 100644 order-processor.ad delete mode 100644 order-processor/aide/examples.adoc create mode 100644 reference.adoc create mode 100644 usage-and-tests.adoc diff --git a/.gitignore b/.gitignore index 811b518..de77efc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ +# Temporarily exclude these files from aide to make the token size manageable +account/ +order-processor/ + ### How to update # This is copied from OpenHFT/.gitignore # update the original and run OpenHFT/update_gitignore.sh diff --git a/README.adoc b/README.adoc index 0754899..a0c1621 100644 --- a/README.adoc +++ b/README.adoc @@ -1,12 +1,56 @@ -= Chronicle-Queue-Demo -Peter Lawrey -:imagesdir: images += Chronicle Queue Demo - README +:toc: +:toclevels: 3 -This is a tutorial demonstrating the usage of Chronicle Queue with simple demo programs. +This repository demonstrates the usage of Chronicle Queue (and related Chronicle libraries) through multiple examples, including an order processor, event routing, and more. Below is a quick start and pointers to further documentation. + +== Quick Start + +.Basic Steps +---- +git clone https://github.com/OpenHFT/Chronicle-Queue-Demo.git +cd Chronicle-Queue-Demo +mvn clean install +---- + +To run a simple example, like the hello-world module: + +---- +mvn install exec:java@RecordInputToConsoleMain +---- + +== Documentation + +The project’s documentation is consolidated in five AsciiDoc files: + +1. xref:architecture.adoc[Architecture] +2. xref:usage-and-tests.adoc[Usage & Tests] +3. xref:reference.adoc[Reference, Style & Glossary] +4. xref:account/README.adoc[Account Management System (AMS)] +5. xref:order-processor/README.adoc[Order Processor] + +Refer to them for details on architecture, usage instructions, testing approaches, style guides, and advanced references. + +== Repository Overview + +The modules in this repository include: + +* **account**: An Account Management System (AMS) example using Chronicle Queue event-driven logic. +* **benchmarks**: Scripts and classes demonstrating throughput/latency benchmarks (e.g., LatencyDistributionMain, ThroughputMain). +* **event-routing**: Showcases how messages can be routed via Chronicle Queues with interfaces like `ViaIn`, `ViaOut`. +* **hello-world**: A simple introduction to an event-driven microservice using Chronicle Queue (input, exclamation addition, output). +* **md-pipeline**: A Market Data pipeline example with aggregator, strategy, and an exchange simulator. +* **message-history-demo**: Demonstrates Chronicle’s `MessageHistory` in bridging or event processing scenarios. +* **messages-with-text**: An example of writing/reading textual content with minimal garbage creation. +* **order-processor**: A submodule that acts as a simple OMS (Order Management System), referencing FIX 4.2 concepts. + +For deeper details on each, see xref:architecture.adoc[Architecture]. == Order Processor -image::Two-hop-latency.PNG[] +This is a tutorial demonstrating the usage of Chronicle Queue with simple demo programs. + +image::images/Two-hop-latency.png[] You can find the source code for the order processor example https://github.com/OpenHFT/Chronicle-Queue-Demo/tree/master/order-processor[here]. @@ -18,13 +62,13 @@ This allows you to install Linux packages that aren't already on Windows. When this asks you which packages you want to install, search for and add `git`. This is under `Development` and you need to click `skip` so it says to `install`. -image::gitpack.png[] +image::images/gitpack.png[] Open IntelliJ or your favorite https://en.wikipedia.org/wiki/Integrated_development_environment[Integrated Development Environment (IDE)]. If you haven't installed an IDE, we recommend https://www.jetbrains.com/idea/download/#section=windows[IntelliJ], which we'll use for this tutorial. In IntelliJ, select `Get from VCS` to clone the Chronicle Queue Sample code. -image::homegit.png[] +image::images/homegit.png[] Then, copy the following URL into the `Git Repository URL` field and remember the `Parent Directory`. Click `Clone` to get the code. @@ -33,11 +77,11 @@ Then, copy the following URL into the `Git Repository URL` field and remember th https://github.com/OpenHFT/Chronicle-Queue-Demo.git ---- -image::Clone.PNG[] +image::images/Clone.png[] If you close the project, you can reopen it by going to `File -> Open`. You'll find the repository in the directory where you saved it. -image::directory.PNG[] +image::images/directory.png[] Now you're ready to run the example programs! You can start with https://github.com/OpenHFT/Chronicle-Queue-Demo/tree/master/simple-input[Simple Input]. diff --git a/account.ad b/account.ad new file mode 100644 index 0000000..0ebbea3 --- /dev/null +++ b/account.ad @@ -0,0 +1,63 @@ += Project Summary + +== Overview + +The project is an Account Management System (AMS) leveraging the Chronicle libraries for high-performance, low-latency event-driven operations. It emphasizes documentation-driven development with AI integration for streamlined engineering. The AMS manages account creation, transfers, and state snapshots through a comprehensive suite of YAML-based tests, detailed functional requirements, and a modular architecture. + +== Key Features + +* **Low-Latency and High-Throughput**: Designed for operations with microsecond-level latencies and over 1M messages per second throughput. +* **Event-Driven Design**: Utilizes Chronicle Queue for durable message persistence and asynchronous operations. +* **Rich Documentation**: Implements AsciiDoc for capturing requirements, workflows, and style guides to align development with AI tools. +* **Testing Framework**: YAML-driven scenarios test various operations, including edge cases and invalid inputs, ensuring robust functionality. + +== Functional Components + +=== Documentation +* **AIDE Glossary**: Defines project-specific terminology, such as AIDE (Artificial Intelligence Development Environment) and tokens/line ratios. +* **Style Guide**: Enforces British English conventions and standardizes coding/documentation practices. +* **Workflow**: Outlines iterative development—document, test, code, review, and repeat—keeping code and requirements synchronized. + +=== Code +* **Core Classes**: +* `AccountManagerImpl`: Orchestrates incoming events and delegates logic to `AccountService`. +* `AccountService`: Validates accounts, manages balances, and processes transfers. +* DTOs (e.g., `CreateAccount`, `Transfer`): Represent operations with fluent setter methods for chaining. +* **Utilities**: +* `LogsAccountManagerOut`: A mock implementation for testing event outputs. +* `ErrorListener`: Handles JVM-level errors gracefully. + +=== Testing +* **Parameterized Tests**: YAML scenarios validate the AMS against predefined inputs and outputs. +* **Coverage**: +* Account creation, transfers, checkpoints. +* Edge cases, including invalid currencies, missing fields, and insufficient funds. +* Performance benchmarks using JLBH for latency testing. + +=== Benchmark Results +Demonstrated latencies: + +* Shared Memory: ~1.5 µs +* TCP: ~20 µs +* End-to-End Variance: Minimal under high load conditions. + +== Directories and Files + +All directories here are under `account` directory + +=== Code +* `src/main/java`: Implementation of AMS. +* `AccountManagerImpl.java`: Main orchestration logic. +* `AccountService.java`: Domain operations. + +=== Tests +* `src/test/java`: Unit and integration tests. +* YAML files: Input (`in.yaml`), expected output (`out.yaml`), and setup (`_setup.yaml`) configurations. + +=== Utilities +* `list_files_asciidoc.sh`: Script to generate directory content summaries. +* `benchmark-results.txt`: Performance insights. + +== Summary + +The AMS is a modular, high-performance system underpinned by comprehensive documentation, rigorous testing, and cutting-edge Chronicle technologies. This structure ensures maintainability, scalability, and alignment with evolving requirements. diff --git a/order-processor/aide/README.adoc b/aide/README.adoc similarity index 100% rename from order-processor/aide/README.adoc rename to aide/README.adoc diff --git a/order-processor/aide/aide-glossary.adoc b/aide/aide-glossary.adoc similarity index 100% rename from order-processor/aide/aide-glossary.adoc rename to aide/aide-glossary.adoc diff --git a/order-processor/aide/aide-style-guide.adoc b/aide/aide-style-guide.adoc similarity index 100% rename from order-processor/aide/aide-style-guide.adoc rename to aide/aide-style-guide.adoc diff --git a/order-processor/aide/aide-workflow.adoc b/aide/aide-workflow.adoc similarity index 100% rename from order-processor/aide/aide-workflow.adoc rename to aide/aide-workflow.adoc diff --git a/order-processor/aide/notes-on-using-aide.adoc b/aide/notes-on-using-aide.adoc similarity index 100% rename from order-processor/aide/notes-on-using-aide.adoc rename to aide/notes-on-using-aide.adoc diff --git a/architecture.adoc b/architecture.adoc new file mode 100644 index 0000000..26a565c --- /dev/null +++ b/architecture.adoc @@ -0,0 +1,119 @@ += Architecture +:toc: +:toclevels: 3 + +This document describes the overall system architecture, focusing on the event-driven design powered by Chronicle Queue, how each module connects, and the core concurrency and data-flow patterns. + +== Design Principles + +1. **Event-Driven** + Each service or module produces or consumes events via Chronicle Queue. We avoid tight coupling or synchronous calls, letting queues mediate communication. + +2. **Low Latency & High Throughput** + Chronicle Queue’s in-memory, append-only design gives us sub-microsecond latencies and can handle millions of events per second on commodity hardware. + +3. **Replay & Durability** + Every message is persisted to a queue file. We can replay historical data for debugging or auditing, or even reconstruct an entire system state by replaying logs from the start. + +4. **Modularity** + Each submodule (e.g., `event-routing`, `md-pipeline`, `order-processor`) addresses a distinct domain scenario or pattern while adhering to a consistent queue-based approach. + +== Core Architecture Overview + +At a high level: + +``` + [Producer/Simulator] --> [Chronicle Queue] --> [Consumer/Processor] --> [Potentially Another Queue] --> ... +``` + +- **Producer**: Writes method calls or messages into a queue. Could be an external service, command-line input, or an exchange simulator. +- **Chronicle Queue**: A durable, zero-GC logging of all events (append-only). Another process or thread can read from it with minimal blocking. +- **Consumer**: Subscribes via a `MethodReader`, processes the data (e.g., aggregates, transforms), and may write responses or derived events into another queue. + +Because each queue is independent, you can run each module in a separate process—enabling easy scaling or horizontal distribution. + +== Concurrency Model + +1. **Single-Threaded Readers** +Many demos use a single-threaded approach: a single method reader loop that blocks on the queue for new events. This eliminates complex synchronization and yields consistent micro-latencies. + +2. **Multi-Process** +For pipeline-like flows (`md-pipeline` or `event-routing`), each stage is typically a separate process (or separate main class) reading from one queue and writing to the next. This separation ensures that no single process becomes a bottleneck. + +3. **MethodWriter/MethodReader** +Chronicle’s code generation approach: +- **MethodWriter** automatically serializes method calls (e.g., `in.mdi(...)`) into queue entries. +- **MethodReader** decodes those entries, calling the actual consumer logic in real time. +This allows your code to look like normal method invocations while behind the scenes it’s writing or reading from a queue. + +4. **Message History & Replay** +Some modules (`message-history-demo`) add a small overhead for each message: a `MessageHistory` that tracks where/when it was produced, how many hops it took. If a consumer fails, we can replay the queue from a safe checkpoint. + +== Module-by-Module Breakdown + +=== Hello World +A minimal example: +- Reads lines from console (`SaysInput`). +- Possibly modifies them (e.g., adds an exclamation). +- Outputs to the console (`SaysOutput`). + +The entire flow can happen in a single process or across multiple if you store input lines in a queue for another process to read. +xref:usage-and-tests.adoc#hello-world[See usage instructions →] + +=== Event Routing +A scenario where messages (like `Value` objects) get routed or filtered in different ways: +- **ViaThreeFive**: Demonstrates branching logic (divisible by 3, 5). +- **SifterImpl**: Checks if a value is even or divisible by 3, sending them to different outputs. + +By abstracting reading/writing behind interfaces (`ViaIn`, `ViaOut`, `ValueMessage`), you decouple the logic from the underlying queue, making it easy to swap in new routing conditions. +xref:usage-and-tests.adoc#event-routing[See usage instructions →] + +=== Market Data Pipeline (md-pipeline) +A multi-process pipeline common in trading or real-time data systems: +1. **ExchangeSimulator** (Generate) – Publishes simulated market increments to `agg-in` queue. +2. **Aggregator** (AggregatorImpl) – Reads increments, aggregates them into a snapshot, outputs to `agg-out`. +3. **Strategy** (StrategyImpl) – Reads aggregated snapshots from `agg-out`, decides on trades, writes them to `strat-out`. +4. **OMS** (OMSImpl) – Optionally reads from `strat-out` to process new orders. + +This chain can run in multiple terminals or containers, each tailing one queue and writing to the next. +xref:usage-and-tests.adoc#md-pipeline[See usage instructions →] + +=== Order Processor (OMS) +Implements a small Order Management System referencing some FIX 4.2 tags: +- Processes inbound `NewOrderSingle`, `CancelOrderRequest` events. +- Publishes `ExecutionReport` or `OrderCancelReject`. + +Because the logic is single-threaded, we can handle thousands of orders per second with minimal overhead. For usage and benchmark commands, see xref:usage-and-tests.adoc#order-processor[Order Processor usage →] + +=== Benchmarks +Various classes in `benchmarks/` measure raw throughput, latency distribution, or stress tests. +- **ThroughputMain** blasts tens of millions of messages into a queue and reads them back, measuring speed. +- **LatencyDistributionMain** measures per-message latency. + +For how to run these tests, see xref:usage-and-tests.adoc#benchmarks[Benchmarking →]. + +== Advanced Chronicle Features + +1. **Shared Memory & Replication** +By default, Chronicle queues store data in memory-mapped files. This is nearly as fast as raw memory access. For advanced setups, Chronicle can replicate data across processes or even machines in near real-time. + +2. **Chronicle Map** +Some submodules (like account-based demos) may store large key-value data in an off-heap map (Chronicle Map). This is not extensively shown in the main modules but can be integrated in a similar pattern. + +3. **Custom Converters & Interceptors** +Chronicle supports user-defined converters (e.g., Base85 encoding for IDs), as well as interceptors to log or transform data during reads/writes. + +4. **Message History** +The `message-history-demo` shows how each queue hop can track timing and source IDs, enabling advanced debugging or performance audits. + +== Deployment & Scaling Notes + +- **Single Host**: You can run multiple modules on the same machine, each pointing to different queue directories, achieving near-zero-latency hops. +- **Distributed**: Place each module on a separate host or container. Latencies may increase with network overhead, but you still preserve asynchronous queue-based flows. +- **Auto-Restart**: Because the queue is durable, if a consumer process is restarted, it can resume from the last confirmed index, ensuring no data loss or duplication. + +== Conclusion + +This architecture leverages Chronicle’s strengths—durability, minimal GC, sub-microsecond latencies—while illustrating an event-driven approach across multiple domain demos (Hello World, Event Routing, MD Pipeline, Order Processor, etc.). The result is a flexible yet high-performance platform for real-time event processing, easily extended or replicated into production-ready systems. + +For instructions on **running** these modules or **testing** them with YAML scenarios, see xref:usage-and-tests.adoc[Usage & Tests]. For **style guidelines** and domain references (e.g., FIX 4.2), see xref:reference.adoc[Reference]. diff --git a/event-routing/src/main/adoc/event-routing-requirements.adoc b/event-routing/src/main/adoc/event-routing-requirements.adoc new file mode 100644 index 0000000..78201f2 --- /dev/null +++ b/event-routing/src/main/adoc/event-routing-requirements.adoc @@ -0,0 +1,110 @@ += Event Routing Submodule – Functional Requirements +:toc: +:toclevels: 3 +:sectnums: + +The *event-routing* submodule is a small system that demonstrates how to **route** (or direct) messages from an input source to one or more outputs, based on simple rules like “divisible by 3” or “even number.” By using Chronicle Queue, these messages are passed around in an **asynchronous, event-driven** way. + +== 1. Purpose and Goals + +. **Showcase how messages can be routed** in a low-latency, asynchronous style using Chronicle Queue. +. **Separate routing logic from the rest of the system** by defining easy-to-use interfaces (like `ViaIn`, `ViaOut`, and `ValueMessage`). +. **Provide simple examples** (e.g., messages that get sent to a "three" channel if divisible by 3) to illustrate the concept. + +=== 1.1 Who Is This For? +* **New developers** who want a straightforward example of event-driven routing. +* **Anyone** interested in seeing how Chronicle Queue can be used to build small, modular routing flows. + +== 2. Key Concepts + +* **Value**: A simple data object (DTO) containing a numeric field (e.g., `val`). +* **ValueMessage**: An interface that defines one method: `value(Value value)`. This method is where you handle incoming `Value` objects. +* **ViaIn** and **ViaOut**: Interfaces that let you specify how to **name** a route (`via("someName")`) and choose an **input** (`in()`) or **output** (`out()`). + +NOTE: +Using `via("...")` explicitly sets the “route name” or “channel name” for messages. Any subsequent logic (e.g., filtering by divisibility) can branch messages to one or more output channels based on that route. Think of `via` as the address you stamp on each message so the system knows where to send it. + +== 3. Functional Requirements + +=== 3.1 Basic Message Routing + +. **FR-1** +The system **must** accept messages with at least a numeric field called `val` (inside a `Value` object). + +. **FR-2** +Each incoming message **must** go through a **rule-based check** (like “Is `val % 3 == 0`?”) to decide what output channel(s) it belongs to. + +. **FR-3** +If a message meets **multiple** conditions (for example, divisible by both 3 and 5), it should be sent to all relevant channels (e.g., "three" and "five"). + +=== 3.2 Interfaces and Interaction + +. **FR-4** +The submodule **must** provide a `ValueMessage` interface so developers can call `value(Value v)` when they have a new message. + +. **FR-5** +A `ViaIn` interface **must** let developers specify a route name (`via(String name)`) and then call `in()` to start reading or receiving messages on that route. +Using `via("nameOfRoute")` indicates *how* (or *where*) messages should be tagged for subsequent filtering or distribution. + +. **FR-6** +A `ViaOut` interface **must** let developers specify a route name (`via(String name)`) and then call `out()` to direct messages to that output route. +Similarly, `via("someOutput")` ensures the system appends or publishes messages to a queue (or channel) labeled `"someOutput"`. + +**Why?** +Using these `via(...)` methods keeps routing flexible. You can easily swap out how you decide routes or what queue to write to without changing the rest of the system. + +=== 3.3 Chronicle Queue Integration + +. **FR-7** +The submodule **should** use Chronicle Queue’s `MethodWriter` to turn your interface calls (like `value(...)`) into actual queue entries automatically. + +. **FR-8** +Another part of the system (e.g., a consumer) **must** use a `MethodReader` to listen for these entries and invoke your routing logic, which might write to a second queue or log the result somewhere. + +This ensures **asynchronous** communication: one part writes events, another reads them whenever it’s ready. + +=== 3.4 Error Handling + +. **FR-9** +If a message doesn’t meet basic requirements (like `val` is missing), or the routing fails, the submodule **should** at least log an error and skip the message. + +. **FR-10** +For a message that doesn’t match any rule, the code can still forward it to a default output or do nothing, as determined by the developer’s preference. + +== 4. Testing + +=== 4.1 YAML Tests + +. **FR-11** +The submodule **must** support simple YAML tests in which `in.yaml` lists the messages (like `{ val: 6 }`), and `out.yaml` shows where each message ends up. + +. **FR-12** +If `val=6`, for example, the expected `out.yaml` should indicate it was routed to a `three` channel. Tests compare actual vs. expected results. + +=== 4.2 Unit & Integration Tests + +. **FR-13** +Developers **should** create unit tests for any custom routing logic (like Sifter or Divisible). + +. **FR-14** +If multiple routes or outputs can be triggered at once (e.g., `15` goes to "three" and "five"), test that the system handles multi-route messages without collisions or missed routes. + +== 5. Examples + +=== 5.1 Divisible by Three or Five +If `val = 15`, the routing logic might send it to: +- `via("three")` +- `via("five")` + +=== 5.2 Even-Odd Filtering +A `SifterImpl` could check if `val` is even, then direct it to an “evens” channel. Odd values could remain unmodified or be sent to another path. + +== 6. Non-Functional Considerations + +* **Performance**: Typically microseconds-level, but you’re free to optimize or benchmark if your scenarios require it. +* **Simplicity**: This module is intentionally straightforward—one or two conditions to illustrate the concept. +* **Extensibility**: You can easily add more rules or multiple routing layers without changing core interfaces. + +== 7. Summary + +The event-routing submodule aims to **demonstrate how messages can flow** through **simple, event-driven rules** using Chronicle Queue. With `via(String name)` acting as the explicit route for each message, it’s easy to define or change routing behavior. Simply implement or extend `ValueMessage` and `ViaIn`/`ViaOut` rules, then create matching tests (YAML or JUnit) to confirm your routing works as intended. diff --git a/event-routing/src/main/java/run/chronicle/routing/inout/ViaThreeFive.java b/event-routing/src/main/java/run/chronicle/routing/inout/ViaThreeFive.java index c34dc1b..3351920 100755 --- a/event-routing/src/main/java/run/chronicle/routing/inout/ViaThreeFive.java +++ b/event-routing/src/main/java/run/chronicle/routing/inout/ViaThreeFive.java @@ -6,7 +6,10 @@ import run.chronicle.routing.inout.api.ViaOut; /** - * Routes {@link Value} messages from multiple sources to multiple destinations based on specific criteria. + * A simple router that checks if a Value's 'val' is divisible by 3 and/or 5. + * - If val % 3 == 0 -> route to "three" + * - If val % 5 == 0 -> route to "five" + * If negative or zero doesn't match, we either skip or log an error. * *

This class implements {@link ViaIn} and {@link ValueMessage} to process incoming {@code Value} messages. * It checks if the name contains the character 'e', and if so, routes the value to different @@ -61,7 +64,10 @@ public ValueMessage in() { */ @Override public void value(Value value) { - if (name.contains("e")) { + if (value.val() < 0) + out.via("error") + .value(value); + else if (name.contains("e")) { if (value.val() % 3 == 0) out.via("three") .value(value); diff --git a/event-routing/src/main/java/run/chronicle/routing/inout/api/Value.java b/event-routing/src/main/java/run/chronicle/routing/inout/api/Value.java index e86a02f..b2979c3 100755 --- a/event-routing/src/main/java/run/chronicle/routing/inout/api/Value.java +++ b/event-routing/src/main/java/run/chronicle/routing/inout/api/Value.java @@ -3,8 +3,9 @@ import net.openhft.chronicle.wire.SelfDescribingMarshallable; /** - * This is a Value class extending SelfDescribingMarshallable. - * It wraps a long value and provides methods for setting and retrieving this value. + * Represents a simple DTO with a numeric value 'val'. + * We can route these messages based on val's properties + * (e.g., divisible by 3, negative, etc.). */ public class Value extends SelfDescribingMarshallable { @@ -22,12 +23,17 @@ public Value(long val) { } /** - * This is a getter for the val instance variable. - * It returns the current long value of this Value object. - * - * @return The current long value of this Value object + * Returns the numeric value of this message. */ public long val() { return val; } + + /** + * (Optional) Allows updating the numeric value. + */ + public Value val(long newVal) { + this.val = newVal; + return this; + } } diff --git a/event-routing/src/main/java/run/chronicle/routing/inout/api/ValueMessage.java b/event-routing/src/main/java/run/chronicle/routing/inout/api/ValueMessage.java index 42ddc73..3f32a37 100755 --- a/event-routing/src/main/java/run/chronicle/routing/inout/api/ValueMessage.java +++ b/event-routing/src/main/java/run/chronicle/routing/inout/api/ValueMessage.java @@ -1,9 +1,9 @@ package run.chronicle.routing.inout.api; /** - * This is a ValueMessage interface. - * It provides a contract for any class that wants to send a {@link Value} message. - * The purpose of this interface is to encapsulate the process of sending a value in a message. + * Describes the contract for any class that handles + * a 'Value' message. The system will call `value(...)` + * whenever a new message is received or processed. */ public interface ValueMessage { diff --git a/event-routing/src/main/java/run/chronicle/routing/inout/api/ViaIn.java b/event-routing/src/main/java/run/chronicle/routing/inout/api/ViaIn.java index b32cf07..ded91a1 100755 --- a/event-routing/src/main/java/run/chronicle/routing/inout/api/ViaIn.java +++ b/event-routing/src/main/java/run/chronicle/routing/inout/api/ViaIn.java @@ -1,8 +1,11 @@ package run.chronicle.routing.inout.api; /** - * This is a ViaIn interface. - * It provides a generic contract for classes that need to specify a route ("via") and an input source ("in"). + * An interface for specifying a route ("via") and obtaining + * an input source ("in"). + * + * Typically, you'd call via("someName") to define a route, + * then in() to start reading from that route as a ValueMessage. * * @param the type returned by the {@code via} method * @param the type returned by the {@code in} method diff --git a/event-routing/src/main/java/run/chronicle/routing/inout/api/ViaOut.java b/event-routing/src/main/java/run/chronicle/routing/inout/api/ViaOut.java index 464b04e..01445a6 100755 --- a/event-routing/src/main/java/run/chronicle/routing/inout/api/ViaOut.java +++ b/event-routing/src/main/java/run/chronicle/routing/inout/api/ViaOut.java @@ -1,8 +1,11 @@ package run.chronicle.routing.inout.api; /** - * This is a ViaOut interface. - * It provides a generic contract for classes that need to specify a route ("via") and an output destination ("out"). + * An interface for specifying a route ("via") and obtaining + * an output destination ("out"). + * + * Typically, you'd call via("someName") to define a route, + * then out() to get a ValueMessage for writing messages. * * @param the type returned by the {@code via} method * @param the type returned by the {@code out} method diff --git a/event-routing/src/main/java/run/chronicle/routing/inout/api/package-info.java b/event-routing/src/main/java/run/chronicle/routing/inout/api/package-info.java index 47aa408..ab413b9 100755 --- a/event-routing/src/main/java/run/chronicle/routing/inout/api/package-info.java +++ b/event-routing/src/main/java/run/chronicle/routing/inout/api/package-info.java @@ -3,10 +3,10 @@ * *

This package includes:

*
    - *
  • {@link Value}: A class that encapsulates a long value, providing methods for retrieval.
  • - *
  • {@link ValueMessage}: An interface defining a contract for sending {@code Value} messages.
  • - *
  • {@link ViaIn}: An interface for specifying a route ("via") and obtaining an input source ("in").
  • - *
  • {@link ViaOut}: An interface for specifying a route ("via") and obtaining an output destination ("out").
  • + *
  • {@link run.chronicle.routing.inout.api.Value}: A class that encapsulates a long value, providing methods for retrieval.
  • + *
  • {@link run.chronicle.routing.inout.api.ValueMessage}: An interface defining a contract for sending {@code Value} messages.
  • + *
  • {@link run.chronicle.routing.inout.api.ViaIn}: An interface for specifying a route ("via") and obtaining an input source ("in").
  • + *
  • {@link run.chronicle.routing.inout.api.ViaOut}: An interface for specifying a route ("via") and obtaining an output destination ("out").
  • *
* *

The goal of this package is to offer a flexible and extensible means of routing messages in various formats and protocols, supporting different input/output strategies.

diff --git a/event-routing/src/main/java/run/chronicle/routing/out/SifterImpl.java b/event-routing/src/main/java/run/chronicle/routing/out/SifterImpl.java index 80203ef..cee4133 100755 --- a/event-routing/src/main/java/run/chronicle/routing/out/SifterImpl.java +++ b/event-routing/src/main/java/run/chronicle/routing/out/SifterImpl.java @@ -7,7 +7,12 @@ import run.chronicle.routing.out.api.Triple; /** - * The SifterImpl class is an implementation of the SifterIn interface. + * SifterImpl checks if val is even or divisible by 3 (or both), + * then routes to different destinations. For example: + * - Even -> 'evens()' + * - Multiple of 3 -> 'triples()' + * + * * The SifterImpl class is an implementation of the SifterIn interface. * This class implements the logic for processing a Value object. * The Value object is processed based on whether its val property is an even number or divisible by 3. */ diff --git a/event-routing/src/main/java/run/chronicle/routing/out/package-info.java b/event-routing/src/main/java/run/chronicle/routing/out/package-info.java index bf3d50f..1191197 100755 --- a/event-routing/src/main/java/run/chronicle/routing/out/package-info.java +++ b/event-routing/src/main/java/run/chronicle/routing/out/package-info.java @@ -1,12 +1,12 @@ /** * This package, run.chronicle.routing.out, contains classes related to the output routing functionality. - * + *

* It includes: * * - SifterImpl: A class implementing the SifterIn interface. This class processes a Value object based on * whether its 'val' property is an even number or divisible by 3. The processing logic decides how the Value * object will be routed out. - * + *

* The aim of this package is to provide classes and logic for output operations. This includes taking * the processed input data, and routing it to the appropriate channels for further use. */ diff --git a/event-routing/src/test/java/run/chronicle/routing/inout/ViaThreeFiveTest.java b/event-routing/src/test/java/run/chronicle/routing/inout/ViaThreeFiveTest.java old mode 100755 new mode 100644 index cfd68ab..c1d7010 --- a/event-routing/src/test/java/run/chronicle/routing/inout/ViaThreeFiveTest.java +++ b/event-routing/src/test/java/run/chronicle/routing/inout/ViaThreeFiveTest.java @@ -11,8 +11,17 @@ public class ViaThreeFiveTest { // The `via` method is a unit test for the `via` method in the ViaThreeFive class. @Test public void via() { - // yt is an instance of YamlTester. The `runTest` method runs a test on the ViaThreeFive class with the input "three-five". - final net.openhft.chronicle.wire.utils.YamlTester yt = net.openhft.chronicle.wire.utils.YamlTester.runTest(ViaThreeFive.class, "three-five"); + doTest("three-five"); + } + + @Test + public void values() { + doTest("three-five-values"); + } + + private static void doTest(String path) { + // yt is an instance of YamlTester. The `runTest` method runs a test on the ViaThreeFive class with the input. + final net.openhft.chronicle.wire.utils.YamlTester yt = net.openhft.chronicle.wire.utils.YamlTester.runTest(ViaThreeFive.class, path); // Asserts that the expected result is equal to the actual result. // The `replace` method replaces any occurrences of "---\n---" in the actual result with "---". diff --git a/event-routing/src/test/resources/three-five-values/in.yaml b/event-routing/src/test/resources/three-five-values/in.yaml new file mode 100644 index 0000000..c763ef9 --- /dev/null +++ b/event-routing/src/test/resources/three-five-values/in.yaml @@ -0,0 +1,20 @@ +# Scenario: 6 -> expect route to "three" +--- +in: eff +value: { val: 6 } +... +# Scenario: 15 -> expect route to "three" and "five" +--- +in: eff +value: { val: 15 } +... +# Scenario: Negative -1 -> error +--- +in: eff +value: { val: -1 } +... +# Scenario: No match (like val=2) +--- +in: eff +value: { val: 2 } +... diff --git a/event-routing/src/test/resources/three-five-values/out.yaml b/event-routing/src/test/resources/three-five-values/out.yaml new file mode 100644 index 0000000..1186bb3 --- /dev/null +++ b/event-routing/src/test/resources/three-five-values/out.yaml @@ -0,0 +1,33 @@ +# Scenario: 6 -> expect route to "three" +--- +out: "" +value: { + val: 6 +} +... +# Scenario: 15 -> expect route to "three" and "five" +--- +out: "" +value: { + val: 15 +} +... +# Scenario: Negative -1 -> error +--- +via: error +value: { + val: -1 +} +... +out: "" +value: { + val: -1 +} +... +# Scenario: No match (like val=2) +--- +out: "" +value: { + val: 2 +} +... diff --git a/hello-world/README.adoc b/hello-world/README.adoc index 2166b81..9e7cc6e 100644 --- a/hello-world/README.adoc +++ b/hello-world/README.adoc @@ -1,23 +1,33 @@ -= Event Driven Program Demo += Event-Driven Program Demo +:toc: +:toclevels: 3 -This demo steps through event driven program development, testing and writing a simple microservice which operates a simple function on input events. -In this demo event driven systems have been modeled so that, the event driven core systems has input and output gateways connecting to external systems that might not be event driven and the business logic is placed in the core event driven system. -Figure 1 shows a general sketch of the introduced model with a simple business logic that adds exclamation mark to input messages (events) and then sends them to the output gateway. +This module demonstrates event-driven program development using Chronicle Queue. +It provides a series of small microservices that showcase how to read incoming events (strings), process or transform those events (e.g., by appending an exclamation mark), and then output the results. -.An event driven system based on the introduced model, the system adds exclamation mark to input messages and sends them to output -image::../docs/images/hello-world-fig1.png[] +image::../docs/images/hello-world-fig1.png[An event-driven system adds an exclamation mark to input messages and sends them to output, align="center"] -== Implementation +== Overview -In the following sections several event driven systems with the general structured shown in Figure 1 are implemented. -All the implementations use the same input and output gateways. +In this demo, we assume an *event-driven* architecture with: -=== Input and Output Gateways +* An **input gateway** that reads incoming lines from the console (or another source). +* An **output gateway** that writes lines to the console (or logs them). +* A **microservice** in between that applies any required business logic (e.g., adding an exclamation mark). -The input and output gateways used in this demo are implementations of the functional interface link:../hello-world/src/main/java/event/driven/program/api/Says.java[Says]. -The input gateway reads each input line and calls say(input line) method. +The figure above gives a high-level sketch of how these components connect. +This design can be extended to larger, more complex systems with multiple inputs and outputs. + +== 1. Implementation Walkthrough + +All implementations in this module rely on the same input and output gateways. Below, we describe each example in detail. + +=== 1.1 Input and Output Gateways + +The gateways used here are built on the link:../hello-world/src/main/java/event/driven/program/api/Says.java[Says] interface. +- **Input Gateway (`SaysInput`)** reads lines from `System.in` and calls `says.say(line)` for each line. +- **Output Gateway (`SaysOutput`)** receives text via `say(String words)` and prints it to `System.out`. -.Input gateway [source,java] ---- public class SaysInput { @@ -28,10 +38,8 @@ public class SaysInput { } } ---- +^ Input gateway: reads from the console and invokes `say(...)` on each line. -The output gateway prints out the received messages. - -.Output gateway [source,java] ---- public class SaysOutput implements Says { @@ -40,28 +48,25 @@ public class SaysOutput implements Says { } } ---- +^ Output gateway: prints each received message to the console. -In the following section several event driven examples are implemented using the above gateways. - -=== Relaying Input Events to Output +=== 1.2 Relaying Input Events to Output -This system simply relays input events to output thus it can be implemented easily as the output of one gateway is wired to the input of the other. -This is implemented in `RecordInputToConsoleMain` class. +The simplest approach wires the input gateway directly to the output gateway: each incoming line is immediately relayed to the console. This is implemented in `RecordInputToConsoleMain`. -.RecordInputToConsoleMain [source,java] ---- public class RecordInputToConsoleMain { public static void main(String[] args) throws IOException { - // Writes text in each call to say(line) to the console + // Create an output gateway final Says says = new SaysOutput(); - // Takes each line input and calls say(line) each time + // Pass it to the input gateway so that each line is echoed SaysInput.input(says); } } ---- -By running `RecordInputToConsoleMain` (right click on the class and select Run), each line of input will be echoed on the console: +When you run `RecordInputToConsoleMain`, each line of input is echoed back: [source,text] ---- @@ -73,58 +78,65 @@ Three // input event Three // output event ---- -=== Log Input Events in Yaml Format +=== 1.3 Logging Input Events as YAML -`RecordInputAsYamlMain` implements an event driven system so that input messages are logged as Yaml on the console. `Wires.recordAsYaml` creates and returns a proxy of `Says` interface which writes inputs into the specified PrintStream in Yaml format. -//TODO add link to Wires.recordAsYaml when it approved and merged +`RecordInputAsYamlMain` demonstrates how to transform input lines into YAML output. +Chronicle Wire’s `Wires.recordAsYaml` creates a proxy of the `Says` interface that writes method calls in YAML format to a specified `PrintStream`. -.RecordInputAsYamlMain [source,java] ---- public class RecordInputAsYamlMain { public static void main(String[] args) throws IOException { - // obtains a proxy that writes to the PrintStream the method calls and their arguments + // Creates a proxy that logs calls to System.out in YAML format final Says says = Wires.recordAsYaml(Says.class, System.out); - // Takes each line input and calls say(theLine) each time + // Reads lines from the console and logs them SaysInput.input(says); } } ---- -To run `RecordInputAsYamlMain` right click on `RecordInputAsYamlMain` and select "Modify Run Configuration ...". -In the pop-up window in the "program arguments" field write the input file name (ie says.yaml) then click on OK and run the program, the following can be observed on console. +.Run RecordInputAsYamlMain +1. Right-click on `RecordInputAsYamlMain` in your IDE. +2. Modify run configuration if needed. +3. Provide an argument for the input file name (e.g., `says.yaml`), though optional if you only want console logs. +Example console output: [source,text] ---- -One // input event -say: One // output event +One // input +say: One // logged as YAML ... -Two // input event -say: Two // output event +Two // input +say: Two // logged as YAML ... -Three // input event -say: Three // output event +Three // input +say: Three // logged as YAML ... ---- -=== Replaying Logged Outputs from a Yaml File +=== 1.4 Replaying Logged Outputs from a YAML File -For this example the outputs from the above section have been logged into resources/says.yaml file. -The file is read and replayed to `SaysOutput` by Wire.replay method. -//TODO add link to Wires.replay method. +We can replay the recorded YAML lines with `ReplayOutputMain`. This reads a YAML file (e.g., `says.yaml`) and invokes the `SaysOutput` gateway to print each event. -.ReplayOutputMain [source,java] ---- public class ReplayOutputMain { public static void main(String[] args) throws IOException { - // Reads the content of a Yaml file specified in args[0] and feeds it to SaysOutput. - Wires.replay(args[0], new SaysOutput()); + // The first argument is the YAML file path + Wires.replay(args[0], new SaysOutput()); } } ---- -By running `ReplayOutputMain` the following output will be printed out on the console. +If your YAML file (`says.yaml`) has lines like: +[source,text] +---- +say: One +say: Two +say: Three +... +---- +`ReplayOutputMain` prints them out as: [source,text] ---- @@ -133,22 +145,25 @@ Two Three ---- -.DirectWithExclamationMain +=== 1.5 Adding an Exclamation Mark + +`DirectWithExclamationMain` shows how to insert a small piece of logic between reading and printing: +It uses `AddsExclamation` to append `"!"` to each line before passing it to the output gateway. + [source,java] ---- public class DirectWithExclamationMain { public static void main(String[] args) throws IOException { - // Adds exclamation mark to input messages + // Decorate the output with an exclamation SaysInput.input(new AddsExclamation(new SaysOutput())); } } ---- -=== Adding a Microservice +=== 1.6 The AddsExclamation Microservice -Now the intermediate block in Figure 1 is implemented using a microservice that appends an exclamation mark to input messages and passes it to the output gateway. +This microservice is a straightforward implementation of `Says`, wrapping another `Says` object: -.AddsExclamation microservice [source,java] ---- public class AddsExclamation implements Says { @@ -158,29 +173,30 @@ public class AddsExclamation implements Says { this.out = out; } + @Override public void say(String words) { - this.out.say(words + "!"); + out.say(words + "!"); } } ---- - -`DirectWithExclamationMain` integrates the three components (Input/Output gateway and `AddsExclalation`) in Figure 1. By running `DirectWithExclamationMain`, an exclamation mark is appended to each input and then will be printed out on the console. +When combined with the input and output gateways (e.g., in `DirectWithExclamationMain`), each input line is printed with an exclamation mark: [source,text] ---- -One // input event -One! // output event -Two // input event -Two! // output event -Three // input event -Three! // output event +One // input +One! // output +Two // input +Two! // output +Three // input +Three! // output ---- -=== Testing the Microservice +== 2. Testing the Microservice + +The link:https://github.com/OpenHFT/Chronicle-Wire/blob/ea/src/main/java/net/openhft/chronicle/wire/TextMethodTester.java[TextMethodTester] allows you to define input (`in.yaml`) and expected output (`out.yaml`) for your microservice, then compare them automatically. -To test the microservice, link:https://github.com/OpenHFT/Chronicle-Wire/blob/ea/src/main/java/net/openhft/chronicle/wire/TextMethodTester.java[TextMethodTester] is used that can read input (resources/in.yaml) and expected output (resources/out.yaml) files of the microservice and compares it to the actual output. +For example: -.Testing the microservice [source,java] ---- public class AddsExclamationTest { @@ -190,12 +206,15 @@ public class AddsExclamationTest { "says/in.yaml", AddsExclamation::new, Says.class, - "says/out.yaml"); + "says/out.yaml" + ); tester.run(); assertEquals(tester.expected(), tester.actual()); } } ---- +If the test fails, you can see the difference between expected and actual outputs in your IDE’s comparison viewer. + Running the test should log "Process finished with exit code 0" on the console which shows the test has passed. Change the second input in in.yaml to Hello World and run the test again, to see the test fails. @@ -210,9 +229,10 @@ By clicking on the link, you will see the difference b .Difference between expected and actual test result image::../docs/images/hello-world-fig3.png[] -== Running the Demo from Maven -Open a shell and from the example directory, hello-world, run the following. commands. +== 3. Running the Demo from Maven + +Below are common commands for building and running each class: .Building the code [source,sh] @@ -234,3 +254,19 @@ mvn install exec:java@ReplayOutputMain [source,sh] mvn install exec:java@DirectWithExclamationMark +== Summary + +The *hello-world* example illustrates a simple event-driven pipeline: + +- **Read** lines (input gateway). +- **Process** them if needed (e.g., add punctuation). +- **Output** them (console or YAML). + +Experiment with each main class: + +* **RecordInputToConsoleMain**: echoes input. +* **RecordInputAsYamlMain**: logs input as YAML. +* **ReplayOutputMain**: replays a YAML file. +* **DirectWithExclamationMain**: adds an exclamation mark. + +This small example demonstrates how Chronicle Queue–based code can handle real-time data flow in a concise, modular way. Once comfortable with these basics, you can explore more advanced modules and features within the repository. diff --git a/hello-world/src/main/adoc/hello-world-requirements.adoc b/hello-world/src/main/adoc/hello-world-requirements.adoc new file mode 100644 index 0000000..e28df7f --- /dev/null +++ b/hello-world/src/main/adoc/hello-world-requirements.adoc @@ -0,0 +1,124 @@ += Hello World Example – Functional Requirements +:toc: +:toclevels: 3 +:sectnums: + +This document specifies the functional requirements for the *hello-world* module in the Chronicle Queue Demo project. It is designed to help new developers understand the basic goals, structure, and usage of the "Hello World" style program that demonstrates how Chronicle Queue can process simple text input and output. + +== 1. Introduction + +=== 1.1 Purpose +The *hello-world* module illustrates a simple, event-driven workflow using Chronicle Queue: +* Reading lines of text (e.g., user input). +* Processing or transforming that text (e.g., adding punctuation). +* Outputting the transformed text (e.g., printing to the console). + +By exploring this module, beginners can learn: +* How to handle **input** in a Chronicle Queue–based microservice. +* How to **process** or modify the text (e.g., by adding an exclamation point). +* How to **output** data back to a console or store it in a queue for later use. + +=== 1.2 Scope +This document only covers the *hello-world* module’s functionality and basic usage steps. It does not address other modules or advanced Chronicle Queue topics like high-throughput event routing, specialized data models, or advanced performance testing. + +== 2. Functional Overview + +=== 2.1 Key Components +The *hello-world* module comprises several main classes. Each class demonstrates a slightly different workflow, but all follow the same general pattern of reading input, optionally transforming it, and printing or logging the result. + +1. **RecordInputToConsoleMain** + - Reads lines from standard input (the keyboard or a piped file). + - Immediately prints each line back to the console. + +2. **RecordInputAsYamlMain** + - Reads lines from standard input. + - Logs or prints each line in YAML format to standard output (useful for structured logging or replay). + +3. **ReplayOutputMain** + - Reads previously saved YAML lines from a file (or queue). + - “Replays” them by printing to standard output. + - Ideal for verifying or demonstrating how recorded input can be replayed later. + +4. **DirectWithExclamationMain** + - Reads lines from standard input. + - Appends an exclamation mark (`!`) to each line. + - Prints the modified line to the console. + +=== 2.2 Data Flow Overview + +[source] +---- +Input (Console) ---> [Hello-World Microservice] ---> Output (Console/YAML/File) +---- +1. The user (or a script) **enters text** in the console. +2. The microservice **captures** that text and can optionally modify it. +3. The microservice **outputs** the original or modified text back to the console or logs it in YAML. + +== 3. Functional Requirements + +=== 3.1 Basic Operations +. **FR-1**: The system **must** read user input from the console or a text file (one line at a time). +. **FR-2**: The system **must** allow for simple text processing or transformation (e.g., adding punctuation). +. **FR-3**: The system **must** output or log each processed line to either the console or a YAML stream. + +=== 3.2 Input Handling +. **FR-4**: On startup, the program **must** prompt the user to type a line. +. **FR-5**: The program **must** recognize an empty line as an instruction to stop reading further input and terminate. + +=== 3.3 Processing Requirements +. **FR-6**: (In direct “echo” mode) The system **must** reprint the exact line without changes. +. **FR-7**: (In exclamation mode) The system **must** append a “!” to the user’s text before printing. +. **FR-8**: (In YAML mode) The system **must** record or display the user input in valid YAML format, enabling replay or inspection. + +=== 3.4 Output and Logging +. **FR-9**: The system **must** show each processed line on the console or standard output. +. **FR-10**: (Optional) The system **may** write lines to a log file or a Chronicle Queue for further inspection or replay. + +=== 3.5 Replay Functionality +. **FR-11**: The system **may** read lines from a pre-recorded YAML file. +. **FR-12**: For each read line, the system **must** print it to the console, simulating “replaying” past inputs. + +== 4. Non-Functional Requirements + +=== 4.1 User Experience +* The software **should** provide a simple, beginner-friendly environment. +* The console output **should** contain short messages guiding the user (e.g., `"Type something"` or `"Press Enter on a blank line to exit"`). + +=== 4.2 Performance +* This demonstration is for **educational** purposes; it does **not** require high-volume or low-latency optimizations. +* Performance is **not** a primary focus, but typical run times for reading and printing a few lines should be near-instant on standard hardware. + +=== 4.3 Dependencies and Environment +* Requires **Java 8** (or later) and **Maven** to build. +* No special hardware or OS constraints. Windows, Linux, and macOS are all supported. + +== 5. User Interactions + +1. The user starts one of the main classes (e.g., `RecordInputToConsoleMain`). +2. The program displays a message: “Type something” or “Input> ”. +3. The user types a line of text and presses Enter. +4. The program processes (or logs) that text and prints the result. +5. The user may type more lines; each is processed in the same manner. +6. If the user presses Enter on an empty line, the program exits gracefully. + +== 6. Testing and Validation + +=== 6.1 Manual Testing +Because the hello-world module is interactive, testers can: +1. Launch one of the main classes (e.g., `DirectWithExclamationMain`). +2. Type sample lines to see whether exclamation marks are appended. +3. Verify the console output matches expectations. + +=== 6.2 Automated Testing +While primarily a demonstration, you can still create simple unit tests for: +* Console I/O (mocking input streams). +* Validation of exclamation appending logic or YAML formatting logic. + +=== 6.3 Potential Edge Cases +* Empty input lines (should exit). +* Very long lines (should still be echoed or appended). +* Special characters and punctuation. + +== 7. Conclusion + +The *hello-world* example program serves as a gentle introduction to Chronicle Queue’s capabilities. By satisfying the above requirements—reading user input, optionally transforming or logging it, and replaying stored data—this module helps beginners understand the foundational concepts of event-driven applications. Further exploration can include queue-based message passing, advanced transformations, and integration with other modules in the Chronicle Queue Demo project. diff --git a/hello-world/src/main/hello-world-requirements.adoc b/hello-world/src/main/hello-world-requirements.adoc new file mode 100644 index 0000000..e69de29 diff --git a/images/Clone.PNG b/images/Clone.png similarity index 100% rename from images/Clone.PNG rename to images/Clone.png diff --git a/images/Two-hop-latency.PNG b/images/Two-hop-latency.png similarity index 100% rename from images/Two-hop-latency.PNG rename to images/Two-hop-latency.png diff --git a/images/directory.PNG b/images/directory.png similarity index 100% rename from images/directory.PNG rename to images/directory.png diff --git a/order-processor.ad b/order-processor.ad new file mode 100644 index 0000000..9345fc9 --- /dev/null +++ b/order-processor.ad @@ -0,0 +1,88 @@ += Order Processor Submodule Documentation +:toc: +:toclevels: 3 + +== Overview + +The **order-processor** submodule is a demonstration of an Order Management System (OMS) implementation. It integrates Chronicle Queue for high-performance message handling and leverages the FIX protocol (4.2) for financial message semantics. The submodule includes: + +- Functional implementations for creating, modifying, and canceling orders. +- Test coverage using unit tests and YAML-based Behavior-Driven Development (BDD) scenarios. +- Utilities for benchmarking and order viewing. + +== Directory Structure + +All directories here are under `order-processor` directory. + +The `order-processor` submodule consists of: + +- **src/main/java**: Source code for OMS interfaces and implementations. +- **src/main/adoc**: AsciiDoc documentation of requirements and FIX mappings. +- **src/test**: Unit tests and YAML-based test scenarios. + +== Key Components + +=== OMS API +Defines inbound (`OMSIn`) and outbound (`OMSOut`) interfaces: + +- `OMSIn`: Handles incoming order-related events such as `NewOrderSingle` and `CancelOrderRequest`. +- `OMSOut`: Publishes responses like `ExecutionReport` and `OrderCancelReject`. + +=== OMS Implementation +- **OMSImpl**: Implements `OMSIn` and processes messages. + - Generates unique `orderID` using a nanosecond timestamp. + - Outputs responses via `OMSOut`. + - Designed for single-threaded operation. + +=== Data Transfer Objects (DTOs) +Encapsulates order-related data: + +- Examples: `NewOrderSingle`, `CancelOrderRequest`, `ExecutionReport`. +- Validated before processing using `validate()` methods. + +=== Utilities + +- **OrderAdderMain**: Simulates order creation. +- **OrderViewerMain**: Displays order messages from the queue. +- **OMSBenchmarkMain**: Benchmarks message throughput and latency. + +== Documentation + +=== Requirements +AsciiDoc files outline the OMS requirements: + +- `oms-requirements.adoc`: Functional requirements for `OMSImpl`. +- `oms-api-requirements.adoc`: API specifications. +- `test-requirements.adoc`: Guidelines for unit and YAML-based tests. + +=== FIX 4.2 Mappings +- `selected-fix-4.2.adoc`: Maps DTO fields to FIX tags. + +== Testing + +- **Unit Tests**: Cover edge cases (e.g., negative quantities, missing fields). +- **BDD Tests**: YAML files specify inputs and expected outputs: + - `newOrderSingle`: Test for `NewOrderSingle` events. + - `cancelOrderRequest`: Test for cancel requests. + - `cancelAll`: Test for mass cancel scenarios. + +== Performance + +- Benchmarking with **OMSBenchmarkMain**. +- Handles 100,000 messages/second with sub-microsecond latencies. + +== Usage + +1. Clone the repository. +2. Build the project with Maven: + + mvn clean install + +3. Run examples: + - `OrderAdderMain` to create orders. + - `OrderViewerMain` to view queue messages. + - `OMSBenchmarkMain` for performance testing. + +== Conclusion + +The `order-processor` submodule demonstrates a robust, high-performance OMS using Chronicle libraries. It ensures alignment with FIX 4.2 and provides comprehensive testing and benchmarking capabilities. diff --git a/order-processor/aide/examples.adoc b/order-processor/aide/examples.adoc deleted file mode 100644 index be8dde1..0000000 --- a/order-processor/aide/examples.adoc +++ /dev/null @@ -1,316 +0,0 @@ -[#aide-examples] -= AIDE Examples - -== Prompt: Event Driven Example - ----- -Give a high performance sample program for event logging and event processing. ----- - -**Result** - -Here’s an example that utilises Chronicle Queue's **Method Readers** and **Method Writers** for event logging and processing. -This approach leverages Chronicle Queue's built-in serialization and method invocation capabilities, offering cleaner and more efficient code. - -''' -=== Maven Dependency - -Add the following dependency to your `pom.xml`: - -[source,xml] ----- - - - net.openhft - chronicle-queue - 3.27ea0 - - ----- - -=== Interface Definitions - -Define the interfaces for the events to be logged and processed. - -==== EventInterface.java - -[source,java] ----- -public interface EventInterface { - void onEvent(String event); -} ----- - -''' - -=== EventLogger.java - -The `EventLogger` writes events to the queue using a **Method Writer**. - -[source,java] ----- -import net.openhft.chronicle.queue.ChronicleQueue; - -public class EventLogger implements AutoCloseable { - - private final ChronicleQueue queue; - private final EventInterface eventWriter; - - public EventLogger(String path) { - queue = ChronicleQueue.singleBuilder(path).build(); - eventWriter = queue.methodWriter(EventInterface.class); - } - - public void logEvent(String event) { - eventWriter.onEvent(event); - } - - public void close() { - queue.close(); - } - - public static void main(String[] args) { - try (EventLogger logger = new EventLogger("events-queue")) { - logger.logEvent("UserLogin: user123"); - logger.logEvent("FileUpload: fileX.pdf"); - } - } -} ----- - -''' - -=== EventProcessor.java - -The `EventProcessor` reads and processes events using a **Method Reader**. - -[source,java] ----- -import net.openhft.chronicle.queue.ChronicleQueue; -import net.openhft.chronicle.bytes.MethodReader; -public class EventProcessor { - - private final ChronicleQueue queue; - private final MethodReader reader; - - public EventProcessor(String path) { - queue = ChronicleQueue.singleBuilder(path).build(); - reader = queue.createTailer().methodReader((EventInterface) this::processEvent); - } - - private void processEvent(String event) { - System.out.println("Processing event: " + event); - } - - public void startProcessing() { - while (true) { - if (!reader.readOne()) { - try { - Thread.sleep(10); // Prevent busy spinning - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - break; - } - } - } - } - - public static void main(String[] args) { - EventProcessor processor = new EventProcessor("events-queue"); - processor.startProcessing(); - } -} ----- - -''' - -=== Key Changes - -1. **Event Writer**: -- Uses `ChronicleQueue.methodWriter(EventInterface.class)` to generate a proxy implementation of `EventInterface`. -- Simplifies the logging of events by invoking interface methods. - -2. **Event Reader**: -- Uses `ChronicleQueue.methodReader(...)` to process events by calling the specified interface method. - -3. **Event Processing Logic**: -- The `processEvent` method is invoked directly for each event by the `MethodReader`. - -''' - -=== Execution - -1. Run `EventLogger` to log events. -2. Start `EventProcessor` to process the logged events in real-time. - -''' - -=== Benefits of Using Method Readers and Writers - -- **Clean API**: Method calls directly represent events, reducing boilerplate. -- **Type Safety**: Ensures compile-time validation of event definitions. -- **High Performance**: Optimized serialization and deserialization with minimal latency. - -This approach streamlines event-driven systems while maintaining high performance and simplicity. - -== Prompt: Key Value Store Example - ----- -Given a java example of a high performance key value store ----- - -**Result** - -Here’s an example of a high-performance key-value store implemented in Java using **Chronicle Map**, a low-latency, in-memory, persisted key-value store suitable for high-throughput applications. - -''' - -=== Maven Dependency - -Add the following dependency to your `pom.xml`: - -[source,xml] ----- - - - net.openhft - chronicle-map - 3.27ea0 - - ----- - -''' - -=== Implementation - -==== KeyValueStore.java - -This class provides a simple interface to interact with a Chronicle Map. - -[source,java] ----- -package build.chronicle.aide; - -import net.openhft.chronicle.map.ChronicleMap; - -import java.io.File; -import java.io.IOException; - -public class KeyValueStore implements AutoCloseable { - - private final ChronicleMap map; - - public KeyValueStore(String filePath) throws IOException { - // Create or load a persisted Chronicle Map - map = ChronicleMap - .of(String.class, String.class) - .averageKey("exampleKey") - .averageValue("exampleValue") - .entries(1_000_000) // Number of expected entries - .createPersistedTo(new File(filePath)); - } - - public void put(String key, String value) { - map.put(key, value); - } - - public String get(String key) { - return map.get(key); - } - - public void remove(String key) { - map.remove(key); - } - - public void close() { - map.close(); - } - - public static void main(String[] args) throws IOException { - String filePath = "key-value-store.dat"; - - try (KeyValueStore store = new KeyValueStore(filePath)) { - // Insert key-value pairs - store.put("user:1", "Alice"); - store.put("user:2", "Bob"); - - // Retrieve and print values - System.out.println("user:1 -> " + store.get("user:1")); - System.out.println("user:2 -> " + store.get("user:2")); - - // Remove a key - store.remove("user:1"); - System.out.println("user:1 -> " + store.get("user:1")); // Should print null - } - } -} ----- - -''' - -=== Features - -1. **Persistence**: -- The data is stored in a file (`key-value-store.dat`) and survives application restarts. -- Use `.createOrRecoverPersistedTo(filePath)` to create or recover a persisted map. - -2. **Performance**: -- Optimized for low-latency reads and writes. -- Suitable for applications requiring millions of operations per second. - -3. **Scalability**: -- Supports billions of entries with minimal memory overhead. - -4. **Ease of Use**: -- Simple API for `put`, `get`, and `remove` operations. - -''' - -=== How It Works - -1. **Initialization**: -- The Chronicle Map is configured with average key/value sizes and the expected number of entries. - -2. **Operations**: -- Keys and values are stored as `String`. -This can be replaced with any serializable class for more complex use cases. - -3. **Persistence**: -- The map persists to a file, enabling recovery after application restarts. - -4. **Cleanup**: -- The `close` method ensures all resources are released and the map is safely persisted. - -''' - -=== Example Output - -[source,text] ----- -user:1 -> Alice -user:2 -> Bob -user:1 -> null ----- - -''' - -=== Advanced Usage - -1. **Custom Key/Value Types**: -Replace `String.class` with custom types for both keys and values. - -[source,java] ----- -ChronicleMap.of(MyKey.class, MyValue.class) - .averageKey(new MyKey()) - .averageValue(new MyValue()) - .entries(1_000_000) - .createOrRecoverPersistedTo("custom-map.dat"); ----- - -2. **Concurrency**: -- Chronicle Map supports concurrent access across threads and processes. -- It’s thread-safe without additional locking mechanisms. - -This example demonstrates a robust and efficient key-value store suitable for high-performance applications like caching, distributed systems, or analytics. diff --git a/reference.adoc b/reference.adoc new file mode 100644 index 0000000..cd4215a --- /dev/null +++ b/reference.adoc @@ -0,0 +1,98 @@ += Reference +:toc: +:toclevels: 3 + +This document compiles various reference materials relevant to this project, such as coding style guidelines, development workflow steps, terminology, and domain-specific mappings (e.g., FIX 4.2 tags). + +== Style Guide + +This section outlines the coding conventions and documentation standards the team follows. + +=== Language and Wording +* **British English** for code comments, doc text, and commit messages. +* Maintain a concise, professional tone. Avoid slang or overly informal expressions. + +=== Code Formatting +* **Indentation**: 4 spaces, no tabs. +* **Line Length**: Typically 128 characters or fewer if possible. +* **Braces**: +** +- Open brace on the same line (`if (condition) {`). +- Close brace on a new line. +* **Blank Lines**: +- One blank line between method definitions. +- Group related lines logically for readability. + +=== Naming Conventions +* **Classes**: PascalCase (e.g., `OrderService`). +* **Methods and Variables**: camelCase (e.g., `processOrder`, `userName`). +* **Constants**: ALL_CAPS_WITH_UNDERSCORES (e.g., `MAX_BUFFER_SIZE`). +* **Packages**: all lowercase, possibly with dots (`com.example.myproject`). + +=== Comments and Documentation +* Use Javadoc (`/** ... */`) for public classes/methods, describing purpose, parameters, return values, and exceptions. +* Avoid excessive inline comments; code should largely speak for itself. +* Keep commit messages in present tense, referencing issue IDs where relevant. + +=== Additional Points +* Some modules rely on `.ad` or `.adoc` files for large code or doc summaries, especially if token constraints apply for AI usage. +* Use xref where possible to link sections in AsciiDoc. + +== Workflow + +This project embraces an AI-assisted, documentation-first process known as AIDE (Artificial Intelligence Development Environment). + +=== Iterative Stages +1. **Document Requirements** +- Write or update `.adoc` content reflecting new features or changes. +- This serves as the “single source of truth.” +2. **Write or Update Tests** +- Create or refine unit, integration, or YAML-based scenario tests, mirroring the documented requirements. +3. **Write or Update Code** +- Let AI or standard IDE tools generate stubs, boilerplate, or scaffolding based on the docs and tests. +- Manually refine logic where needed. +4. **Review** +- AI highlights discrepancies between docs/tests/code. Humans validate correctness. +5. **Repeat** +- Each change in requirements triggers this loop again, keeping everything in sync. + +=== Document-First Benefits + +* **Minimal Drift**: Fewer mismatches between code and docs. +* **Faster Onboarding**: Updated documentation clarifies system design. +* **Better Quality**: Frequent iteration on specs → more thorough testing. + +== FIX 4.2 Mapping + +For modules referencing FIX protocol (like the Order Processor), the following table maps relevant fields to standard FIX 4.2 tags. Adjust the table to your actual usage: + +|FIX Tag |Field/DTO Property |Description +|-------:|-------------------|------------------------------------------- +|35=D |`NewOrderSingle` |Message type for new orders +|11 |`clOrdID` |Unique client order ID +|54 |`side` |Side of the trade (`buy`/`sell`) +|38 |`orderQty` |Quantity of the order +|44 |`price` |Price for limit orders +|39 |`ordStatus` |Order status (e.g., `new`, `filled`) +|150 |`execType` |Execution type (partial fill, fill, canceled) +|**...** |**(others as needed)**|Expand based on your system’s data + +== Additional References + +Below are some advanced or miscellaneous notes not covered elsewhere. + +=== Large File Summaries +- Certain `.ad` or `.adoc` summaries exist for very large classes or resources that rarely change. +- This practice keeps token usage low for AI processes, while preserving essential context in a succinct form. + +=== Performance Insights +- Typical latencies can reach ~1.5µs (shared memory) or ~20µs (TCP) under load. +- For more detailed throughput or latency data, see xref:usage-and-tests.adoc#benchmarks[Usage & Tests → Benchmarking]. + +=== AI Prompt Templates +- If your build includes scripts or templated prompts for the AI, store them in version control as well. +- Keep track of prompt changes to maintain consistency across feature expansions. + +== Closing Notes + +Refer back to xref:architecture.adoc[Architecture] for design overviews or xref:usage-and-tests.adoc[Usage & Tests] for instructions on running modules and tests. By maintaining these references, the project stays coherent and approachable to both new and returning contributors. diff --git a/usage-and-tests.adoc b/usage-and-tests.adoc new file mode 100644 index 0000000..478eeb2 --- /dev/null +++ b/usage-and-tests.adoc @@ -0,0 +1,216 @@ += Usage & Tests +:toc: +:toclevels: 3 + +This document explains how to build, run, and test the various modules and demos in this project. It also covers the basic steps for performance benchmarking and troubleshooting common issues. + +[#building-the-project] +== Building the Project + +Use Maven (or your preferred build tool) to compile all modules: + +---- +mvn clean install +---- + +If you want to skip tests (for faster iteration): +---- +mvn clean install -DskipTests +---- + +[#running-examples] +== Running Examples by Module + +Below are instructions for starting each primary module or demo. In most cases, you will run a main class via Maven’s `exec:java` plugin, or by launching it directly from an IDE. + +[#hello-world] +=== Hello World + +The simplest demonstration of reading input, optionally modifying it, and printing results. +Commands (examples, might vary slightly depending on your `pom.xml` executions): + +1. **RecordInputToConsoleMain** + ---- + mvn exec:java@RecordInputToConsoleMain + ---- + - Reads input lines, echoes them back to the console. + +2. **RecordInputAsYamlMain** + ---- + mvn exec:java@RecordInputAsYamlMain + ---- + - Logs each line in YAML format to standard output. + +3. **ReplayOutputMain** + ---- + mvn exec:java@ReplayOutputMain -Dexec.args="someFile.yaml" + ---- + - Replays previously logged YAML lines from a file or queue. + +4. **DirectWithExclamationMain** + ---- + mvn exec:java@DirectWithExclamationMain + ---- + - Inserts an exclamation mark into each line before printing it. + +[#event-routing] +=== Event Routing + +Demonstrates the `ViaIn`/`ViaOut` pattern, or advanced filters like `ViaThreeFive`: + +1. **Set Up Input**: Provide input YAML files (`in.yaml`) describing messages. +2. **Run**: + ---- + mvn exec:java -Dexec.mainClass=run.chronicle.routing.inout.ViaThreeFive + ---- + (Adjust to your actual main classes or different test runners.) + +3. **Check Output**: Compare the resulting queue or console logs with expected YAML (`out.yaml`). + +[#md-pipeline] +=== Market Data Pipeline (md-pipeline) + +A multi-service pipeline: + +1. **ExchangeSimulator** – Writes incremental market data. + ---- + mvn exec:java@generate + ---- +2. **Aggregator** – Consumes data increments, aggregates them, and outputs snapshots. + ---- + mvn exec:java@aggregator + ---- +3. **Strategy** – Reads aggregated data, decides on trades, writes out orders. + ---- + mvn exec:java@strategy + ---- + +Optionally, **OMS** can consume the strategy's orders. +In separate terminals, watch each queue with: + +[source] +---- +mvn exec:java@tailf -Dqueue=agg-in +mvn exec:java@tailf -Dqueue=agg-out +mvn exec:java@tailf -Dqueue=strat-out +---- + +[#order-processor] +=== Order Processor (OMS) + +Implements a basic Order Management System referencing FIX 4.2. Typical usage: + +1. **Add Orders** + ---- + mvn exec:java@OrderAdderMain + ---- + - Generates `NewOrderSingle` events. + +2. **View Orders** + ---- + mvn exec:java@OrderViewerMain + ---- + - Reads the queue to display order or execution events. + +3. **Benchmark OMS** + ---- + mvn exec:java@OMSBenchmarkMain + ---- + - Tests throughput or latency for order submission and responses. + +''' + +[#testing] +== Testing + +This project uses a mix of **unit tests**, **integration tests**, and **YAML-based scenario tests**. Most are in `src/test/java` or `src/test/resources`. + +[#unit-tests] +=== Unit Tests + +- Classes named `XxxTest` (e.g., `AddsExclamationTest`, `SifterImplTest`). +- Typical JUnit or JUnit5 style. +- Run all at once with: +---- +mvn test +---- + +[#yaml-tests] +=== YAML-Based Tests + +- Many modules use BDD-like YAML files (`in.yaml`, `out.yaml`, `_setup.yaml`) to define inputs and expected outputs. +- Tools like `YamlTester` or `TextMethodTester` parse these files and compare actual vs. expected queue events. +- **Example**: `ViaThreeFive2Test` references `three-five/in.yaml` and `three-five/out.yaml`. +- To update or regress outputs: +---- +mvn test -Dregress.tests +---- +This can overwrite `out.yaml` if you’re intentionally changing expected outputs. + +[#integration-tests] +=== Integration Tests + +- Some scenarios span multiple processes (e.g., the market data pipeline). +- Start each submodule or main class in a separate terminal so they pass data via Chronicle Queues. +- Verify logs or queue outputs match expectations (YAML out files or console prints). + +[#coverage] +=== Coverage & Tools + +- (Optional) Use Jacoco or another coverage plugin to measure how thoroughly your tests cover the code. + +''' + +[#benchmarks] +== Benchmarking + +Several classes in `benchmarks/` measure throughput and latency: + +* **LatencyDistributionMain** +- Usage: +---- +mvn exec:java -Dexec.mainClass=chronicle.queue.benchmark.LatencyDistributionMain \ +-Dsize=60 -Dthroughput=100000 +---- +- Parameterize `size`, `throughput`, etc. +- Checks microsecond-level round-trip latencies. + +* **ThroughputMain** +- Writes and reads millions of messages to measure raw throughput. +- Example: +---- +mvn exec:java -Dexec.mainClass=chronicle.queue.benchmark.ThroughputMain \ +-Dtime=10 -Dsize=60 +---- + +Keep an eye on CPU scaling, GC logs, and whether you’re using shared memory or TCP. Results typically show 1+ million msgs/sec or sub-micro latencies, depending on hardware. + +''' + +[#troubleshooting] +== Troubleshooting + +- **No Output?** +Double-check your queue path. Maybe specify `-Dpath=queue` or ensure each module points to the correct folder. + +- **File Locks or Collisions** +On Windows, ensure no leftover processes hold open file handles. + +- **YAML Tester Mismatch** +If actual vs. expected outputs differ unexpectedly, see if your scenario changed. Use `-Dregress.tests` only if you deliberately want to update your baseline. + +- **Excessive GC** +Chronicle aims for minimal allocations, so check if you introduced object churn in custom code. Potentially switch to direct Bytes usage or off-heap. + +''' + +[#summary] +== Summary + +You now have clear instructions on: + +1. **How to build and run** each module or submodule example (hello-world, event-routing, md-pipeline, order-processor). +2. **How to test** using either standard JUnit or advanced YAML-based scenario tests. +3. **How to benchmark** with throughput and latency tools provided in `benchmarks/`. + +For deeper architectural background, see xref:architecture.adoc[Architecture]. For style guides and advanced references, see xref:reference.adoc[Reference]. \ No newline at end of file From 69cfe7a68979994641ae63948d14700349019e49 Mon Sep 17 00:00:00 2001 From: Peter Lawrey Date: Thu, 2 Jan 2025 09:21:41 +0000 Subject: [PATCH 23/31] Removed AIDE --- aide/README.adoc | 29 ------- aide/aide-glossary.adoc | 53 ------------ aide/aide-style-guide.adoc | 98 --------------------- aide/aide-workflow.adoc | 155 ---------------------------------- aide/notes-on-using-aide.adoc | 91 -------------------- 5 files changed, 426 deletions(-) delete mode 100644 aide/README.adoc delete mode 100644 aide/aide-glossary.adoc delete mode 100644 aide/aide-style-guide.adoc delete mode 100644 aide/aide-workflow.adoc delete mode 100644 aide/notes-on-using-aide.adoc diff --git a/aide/README.adoc b/aide/README.adoc deleted file mode 100644 index ee54b2d..0000000 --- a/aide/README.adoc +++ /dev/null @@ -1,29 +0,0 @@ -= Documents Supporting AI -:doctype: index -:author: Peter Lawrey -:lang: en-GB -:toc: - -Copied from https://github.com/peter-lawrey/aide[Peter Lawrey's AIDE Project] - -This folder contains supplemental AsciiDoc files that guide usage, workflows, or style conventions for AI-based projects. - -== Overview - ----- -aide/ -├─ aide-glossary.adoc → Terminology definitions -├─ aide-style-guide.adoc → Project coding style guidelines -├─ aide-workflow.adoc → AI workflow explanation -└─ notes-on-using-aide.adoc → Notes on using AIDE ----- - -Refer to each `.adoc` for details: - -* link:aide-glossary.adoc[AIDE Glossary]: Definitions of key terms (tokens, AIDE, tokens/line, etc.). -* link:aide-style-guide.adoc[AIDE Style Guide: Conventions for naming, coding, and documentation. -* link:aide-workflow.adoc[AIDE Workflow]: The AIDE approach to iterative, documentation-driven engineering. -* link:aide/notes-on-using-aide.adoc[Notes on Using AIDE]: Notes on previous revisions the AI can draw on for further suggestions. -* link:examples.adoc[Examples using AIDE]: Sample AI queries using AIDE. - -These documents are stored in version control alongside the code, ensuring they evolve together. diff --git a/aide/aide-glossary.adoc b/aide/aide-glossary.adoc deleted file mode 100644 index 3da4097..0000000 --- a/aide/aide-glossary.adoc +++ /dev/null @@ -1,53 +0,0 @@ -[#aide-glossary] -= AIDE Glossary -:doctype: glossary -:author: Peter Lawrey -:lang: en-GB -:toc: - -This glossary is a list of key terms used in the project, along with their definitions. - -[quote, Oxford English Dictionary] -aide (noun) an assistant to an important person, especially a political leader. Pronounced _ayd_. - -== Terms in this Project - -* **AIDE**: Artificial Intelligence Development Environment. -* **AsciiDoc**: A lightweight markup language for authoring technical documentation. -* **Context Mode**: The process of generating a `context.asciidoc` file that captures the entire project’s documentation, requirements, and code. -* **Incremental Mode**: The process of generating an `increment.asciidoc` which captures only the newly or recently updated files since the last full `context.asciidoc` build, as described in the “Next Level AIDE” post. -* **Token**: Underlying words or subword units used by AI. -* **Tokens/Line**: A ratio indicating how many tokens appear per non-blank line of text (calculated as total tokens / total lines). -Used as a quick measure of text “density”. -* **Blank Line**: A line containing only whitespace characters (spaces, tabs, etc.). - -== Different Asciidoctor extensions - -Below is a simple table clarifying when to create a shorter `.ad` summary or use a `.asciidoc` file for larger documents. - -[cols="1,1,4",options="header"] -|=== -| File Type -| Typical Size/Usage -| When to Use - -| `.ad` -| Short Summaries -| Generated or manually curated summaries of large docs. Use these when you have huge files that rarely change. Keeps token counts low for AI-based processes without losing essential context. - -| `.ad` -| Alt Text for Binary Files -| A description of an image or binary file can be stored in a `.ad` file for inclusion in the `context.asciidoc` file. - -| `.adoc.ad` -| Short Summary of an AsciiDoc File -| A short summary of a larger AsciiDoc file that is too large to include in the `context.asciidoc` file. - -| `.adoc` -| Medium -| Full documentation files that change relatively often or are critical references. It is useful when your readers or your AI need complete details. - -| `.asciidoc` -| Large -| A long-form extension for AsciiDoc files for larger documents that the tools will ignore as too large. -|=== \ No newline at end of file diff --git a/aide/aide-style-guide.adoc b/aide/aide-style-guide.adoc deleted file mode 100644 index 98de857..0000000 --- a/aide/aide-style-guide.adoc +++ /dev/null @@ -1,98 +0,0 @@ -[#aide-style-guide] -= AIDE Style Guide -:doctype: style-guide -:author: Peter Lawrey -:lang: en-GB -:toc: - -This guide outlines the core conventions for naming, coding, and documentation. By following these practices, the team maintains clarity, consistency, and a high-quality codebase overall. - -== 1. Language and Wording - -1. Use **British English** spelling (e.g., `colour`, `initialise`) in code comments, documentation, and commit messages. -2. Maintain a **professional, concise** tone; avoid slang or overly informal expressions. -3. Define all project-specific or domain-specific terms in a link:aide-glossary.adoc[AIDE Glossary]. -4. Use straight apostrophes and single and double quotes in code and documentation. -5. Code identifiers may use British or American spellings as needed; consistency within a project is key. -6. When adding suggested improvements to `.adoc` files, use comment syntax like `// consider:` -7. **Document-First Approach**: -* As recommended in the "Next Level AIDE" post, write or update `.adoc` content as early as possible, either in parallel with feature development or before coding (similar to TDD). -* This ensures that the single source of truth is always current and that AI-driven code suggestions align with the actual requirements. - -== 2. Directory and Package Structure - -1. **Directory Names**: all lowercase; use hyphens or underscores if necessary (e.g., `my-project-utils/`). -2. **File Names**: -* Java: match the class name (e.g., `MyService.java`). -* Documentation: use `.adoc` (e.g., `project-overview.adoc`) with lowercase, hyphenated file names. -* Configuration: `.yaml` or `.properties`, describing purpose (e.g., `app-config.yaml`). -3. **Packages**: all lowercase, reflecting the maven `groupId` and `artifactId` and subsystem (e.g., `groupId.artifactId.service`). - -== 3. Java Code Style - -=== 3.1 Formatting -1. **Indentation**: 4 spaces; no tabs. -2. **Line Length**: Keep to 128 characters or fewer if possible. -3. **Braces**: -* Open brace on the same line (e.g., `if (condition) {`). -* Close brace on a new line. -4. **Blank Lines**: Separate method definitions with one blank line; group related lines logically. - -=== 3.2 Naming Conventions -1. **Classes**: PascalCase (e.g., `OrderService`). -2. **Methods and Variables**: camelCase (e.g., `processOrder`, `userName`). -3. **Constants**: ALL_CAPS_WITH_UNDERSCORES (e.g., `MAX_BUFFER_SIZE`). - -=== 3.3 Comments and Documentation -1. Use Javadoc for **public** classes and methods, explaining purpose, parameters, and returns. -2. Only add inline comments (`//`) if the logic isn't apparent. -3. Maintain descriptive commit messages in the present tense (e.g., "Add new caching layer"). - -== 4. Testing - -=== 4.1 Test Naming Conventions -1. Test classes should be named by appending `Test` to the class they test (e.g., `OrderServiceTest` for `OrderService`). -2. Test method names should describe the test scenario and expected behavior, using camelCase (e.g., `shouldReturnOrderDetailsWhenOrderExists`). - -=== 4.2 Directory Structure -1. Mirror the main package structure under `src/test/java/...`. -2. Use a logical grouping of tests, such as: -- Unit tests under `src/test/java` -- Integration tests under `src/integrationTest/java` (if applicable). - -=== 4.3 Test Coverage -1. Ensure at least **80% test coverage** for critical business logic. -2. Focus on covering edge cases, particularly for methods with complex branching. -3. Write integration tests for new features or significant architectural changes. - -=== 4.4 Writing Effective Tests -1. Each test should focus on a single behavior or scenario. -2. Use parameterized tests for scenarios with repetitive logic. -3. Avoid hardcoding data when reusable fixtures or factories can be applied. - -=== 4.5 Mocking and Stubbing -1. Use mocking frameworks (e.g., Mockito) to isolate the unit under test. -2. Stub external dependencies to ensure consistent and repeatable test outcomes. -3. Prefer fakes or in-memory implementations for lightweight data operations (e.g., an in-memory database for integration tests). - -=== 4.6 Test Documentation -1. Include comments to clarify the intent of complex test cases. -2. Document preconditions, inputs, and expected outputs within the test itself or in its name. -3. Ensure test cases reference any associated requirements or bug IDs in the comments. - -== 5. Documentation (AsciiDoc) -1. Start each `.adoc` file with a title line (`= Title`) and optional metadata. -2. Organise content under headings: `==`, `===`, `====`. -3. Use `[source,java]` blocks (or similar) for code highlighting. -4. Keep lines concise, highlighting key points in bullet lists or admonitions (e.g., `NOTE:`). - -== 6. Version Control and Commit Messages -1. Write short, clear commit messages in the **present tense** (e.g., "Fix login bug in UserService"). -2. Reference issue IDs if applicable (e.g., "Fix #42: Add advanced search feature"). - -== 7. Additional Best Practices -1. **AutoCloseable Usage**: Where possible, consider implementing `AutoCloseable` to be used in a try-with-resources block, ensuring deterministic cleanup. -2. **Template Management**: Use templates for AI prompts and text blocks to ensure consistency and reduce duplication. - -== 8. Summary -By following these concise guidelines—especially consistent, clear Java formatting and straightforward documentation practices—teams ensure a streamlined, maintainable codebase. Remember to evolve this guide as the project grows and needs change. diff --git a/aide/aide-workflow.adoc b/aide/aide-workflow.adoc deleted file mode 100644 index 55a593d..0000000 --- a/aide/aide-workflow.adoc +++ /dev/null @@ -1,155 +0,0 @@ -[#aide-workflow] -= AIDE Workflow -:doctype: workflow -:author: Peter Lawrey -:lang: en-GB -:toc: - -AIDE combines AI-powered code generation's agility with the reliability of Documentation-Driven Development. -AIDE is an artificial intelligence development environment that relies on AsciiDoc for rich documentation and document context uploads to align your team with AI tooling. - -AIDE focuses on two principles: - -1. Use AI for repetitive code tasks such as keeping adoc, code, and tests in sync. -2. Keep requirements, tests, and code in sync with AsciiDoc. - -== Iterative Workflow - -The development follows a simple loop: - -1. **Document Requirements** – Write or update features in AsciiDoc so everyone understands goals. -These can be derived and updated from code and tests as these change. -This can be as much or as little detail as you have. -You can start by asking the AI to generate a detailed functional requirements document from the code you have or an idea. -2. **Write or Update Tests** – Create tests that reflect documented requirements. -AI can suggest skeletons; you refine domain logic. -3. **Write or Update Code** – Let AI produce a boilerplate or refactor existing code. -You verify its validity. -4. **Continuous Review** – AI flags discrepancies; humans resolve them. -5. **Repeat** – Each new or changed requirement triggers another loop. - -== AI <-> Human Collaboration - -[mermaid] ----- -flowchart LR - A((Write/Update
Requirements)) <--> B((Write/Update
Tests)) - B <--> C((Write/Refactor
Code)) - C <--> A ----- - -- **AI**: Proposes repetitive code, test stubs, or refactors. -- **Human**: Reviews correctness, ensures domain fit, and finalises decisions. -- **Outcome**: Software matches documented requirements and passes tests. - -== Elevating the Workflow with Real-Time Documentation - -Building upon our existing Iterative Workflow, the newest recommendation is to emphasise *real-time updates* to documentation. -Ensure the relevant `.adoc` files are updated as soon as features, requirements, or tests change. -This tight loop informs the AI and creates immediate clarity for all team members. - -=== Benefits - -- **Confidence in Documentation**: As the newest post highlights, accurate docs keep you from "miscommunications" that derail real-world outcomes. -- **Better Onboarding**: An up-to-date AsciiDoc set means new developers grasp the system's design and requirements more quickly. -- **Incremental Changes**: Thanks to the incremental mode, AIDE flags any newly updated files so you can keep the documentation in sync. - -== Benefits of Keeping Requirements, Tests, and Code In Sync - -- **Reduced Drift**: Fewer gaps between documentation and iimplementation -- **Faster Feedback**: AI can quickly generate stubs and highlight inconsistencies. -- **Better Quality**: Frequent checks align the code with tests and docs. -- **Smoother Onboarding**: Up-to-date AsciiDoc clarifies the system for new developers. - -== Best Practices - -1. **Single Source of Truth** -Keep docs (AsciiDoc), tests, and code in version control. - -2. **Frequent Iterations** -Update documentation first, then let AI produce or refine code and tests. - -3. **Small Commits** -Each commit should relate to a single requirement or story, making reviewing changes easier. - -4. **Team Buy-In** -Encourage everyone to review AI outputs. -Use a consistent coding style guided by the style guide. - -== Summarising Large Files - -When a code or test file grows very large, it can complicate AI-based reviews or exceed token limits. -AIDE allows you to generate a condensed `.ad` summary for these files. -This step is handy for rarely updated classes or tests that do not need frequent in-depth reference. - -Here is the recommended process: - -1. **Identify Large Files** -Track files that exceed a certain token threshold (as noted in `notes-on-using-aide.adoc`), or are not actively changing. -2. **Use AI to Summarise** -Run the "Summarise this class as an adoc" command or an equivalent script/tool. -This produces a concise `.ad` file containing essential details. -3. **Commit** -Include a concise commit message like "Summarise large class as `.ad` to reduce token length." - -This approach keeps your AI-based tooling efficient and your repository lean while preserving a high-level understanding of classes that do not require frequent full-detail scrutiny. - -''' - -== Why Integrate Chronicle Products? - -Chronicle products are designed to handle high-performance requirements for latency-critical systems. By integrating them with this project: -- **Throughput**: Achieve message processing speeds exceeding 1M messages/second. -- **Latency**: Sub-microsecond latencies for in-memory operations. -- **Scalability**: Handle billions of records with minimal memory overhead. -- **Fault Tolerance**: Ensure robustness and reliability in distributed systems. - -[mermaid] ----- -flowchart TD - A[Your Application] -->|Queue Events| B[Chronicle Queue] - B -->|Shared State| C[Chronicle Map] - A -->|FIX Messages| D[Chronicle FIX Engine] ----- - -When building applications with AIDE, it's important to consider performance, especially for latency-sensitive or high-throughput systems. Chronicle libraries provide specialised tools to optimise various aspects of your architecture: - -=== Chronicle Queue - -https://github.com/OpenHFT/Chronicle-Queue[Chronicle Queue] is a persisted low-latency messaging framework for high-performance applications. Event `MethodReader` and `MethodWriter` can be used to read and write messages at sub-microsecond latencies in an effective high-level way. - -- **Best for:** High-throughput messaging or event-driven architectures. -- **Use Case:** Logging, replaying, or processing large message volumes. -- **Why Use It:** Microsecond-level latencies, append-only logging, and distributed processing. - -=== Chronicle Map - -https://github.com/OpenHFT/Chronicle-Map[Chronicle Map] is a fast, in-memory, non-blocking key-value store. - -- **Best for:** High-performance, persistent key-value stores. -- **Use Case:** Concurrent access to shared data across threads or processes. -- **Why Use It:** Supports billions of entries, low-latency reads/writes, and thread-safe operations. - -=== Chronicle Services - -https://chronicle.software/services/[Chronicle Services] is a framework for high-performance distributed microservices. - -- **Best for:** Scalable, low-latency microservices. -- **Use Case:** Event-driven architectures with robust inter-service communication. -- **Why Use It:** Simplifies development, supports fault tolerance, and optimises sub-millisecond interactions. - -=== Chronicle FIX Engine - -https://chronicle.software/fix-engine/[Chronicle FIX Engine] is a high-performance FIX protocol engine. - -- **Best for:** Financial systems needing low-latency FIX protocol support. -- **Use Case:** Trading platforms, market data systems, or FIX-based gateways. -- **Why Use It:** Deterministic latencies, FIX message handling, and minimal overhead. - -Choosing the correct Chronicle library ensures your applications meet modern performance demands while maintaining a streamlined AIDE-driven development workflow. - -== Conclusion - -AIDE streamlines development by pairing AI's efficiency with clear human guidance. -Document requirements, run AI-driven code generation, verify via tests, and repeat in small steps. -This keeps your project consistent, accurate, and easy to maintain. diff --git a/aide/notes-on-using-aide.adoc b/aide/notes-on-using-aide.adoc deleted file mode 100644 index 83b89e7..0000000 --- a/aide/notes-on-using-aide.adoc +++ /dev/null @@ -1,91 +0,0 @@ -[#notes-on-using-aide] -= Notes on using AIDE -:doctype: revision-notes -:author: Peter Lawrey -:lang: en-GB -:toc: - -Keeping notes on how the project is progressing and what changes you have made is helpful. -This can help you understand the project better and make the necessary changes at the end to complete it. - -NOTE: This document can be a template for recording notes on your own project and for helping the AI suggest improvements, as it is also a source of information between phases. - -== First Phase - Requirements - -Create a skeleton for the project and see what the AI can generate for you. -These are more a source of inspiration than a source of truth. - -NOTE: Using `o1` is usally better than `4o` as it is more likely to be understood by the AI and produce a better result. `4o` works better for converting images, or web pages to asciidoc. - -- `Generate a functional requirements asciidoc of OMSImpl detailed enough to recreate this implementation including a prompt to execute it.` (Paste the `context.asciidoc`) -- `suggest improvements to this requirements document` - -.Request to clarify assumptions to the requirements document. ----- -Add to the requirements document to note that this component is intended to be in an event driven framework. Each input event comes from a Chronicle Queue, and each output is written to a Chronicle Queue, providing traceability. Validation is performed by the framework calling validate() on the input DTO before calling the matching method, and validate() called on the output DTO before writing to the queue. -Also note; -- On receiving a NewOrderSingle, the system must create an ExecutionReport. If the request is invalid, the system must create and send an OrderCancelReject. -- When an exception is thrown, the framework captures the exception and calls jvmError. -- for a simple example such as this, the component should be able to consistently handle 100k msg/s with low jitter on the 99% latency or better. ----- - -The improved generated requirements document was written to `src/main/adoc/oms-requirements.adoc` - -- `generate a functional requirements document for the packages lost.town.oms.api and lost.town.oms.dto as an asciidoc with enough detail to recreate them` -- `suggest improvements to these requirements` - -.Request to clarify details ----- -add to the api requirements document that the frame work is responsible for calling validate() before passing the event to the component, and for an output DTO before an output is written. -There is an assumption of single threadedness -The YAML tests describe sample usage. see BDD tests, Given `_setup.yaml` events, When `in.yaml` events are tested and Then we expect `out.yaml` events -The `jvmError` is called for every Exception thrown. -This demo relies on Chronicle Wire however a production example would require Chronicle Services to support failover to a replicated system and restartability. -More YAML based tests can be added by including a new directory in the list of paths in the OMSImplTest class. ----- - -- `make the requirements document detailed enough to recreate the API and dto packages` - -Output written as `src/main/adoc/oms-api-requirements.adoc` - -== Second Phase - Unit Tests - -Reran `AppDocumentApp` to create a new context.asciidoc file. - -- `suggest improvements to the YAML based tests` -* Consistent case for `sell`, `ordType`, and `clOrdID` in the YAML files. -* Consistent absense of timezone ie removed `Z` from timestamps. -* `Following suggestion 2.1, add a Scenario headings to all the YAML based in.yaml files` -** (Reran `AppDocumentApp` to create a new increment.asciidoc file so it has the changes I just made) -** Merged using `Compare with Clipboard` -- `generate a unit test for OMSImpl that covers edge cases` -* Minor fixes to the tests to ensure they compile. -- Run tests with `-Dregress.tests` to update the out.yaml files. -* This created three new agitated tests after correcting the case of `clOrdID` in the YAML files. -- `Generate a details requirements asciidoc for unit tests and yaml based event driven tests including assumptions made by the framework` -- `improve and reduce duplication in src/main/adoc files` -* Ten reasonable suggestions to improve the requirements and test documents, but none compelling. - -== Third Phase - Code Improvements - -Review the project - -- `Suggest improvements to the classes in town.lost.oms.api` -* -- `based on the FIX 4.2 specification, extract a description of messages in town.lost.oms.dto and their fields in asciidoc` - -== Fourth Phase - -Restructure the project to make it easier to work with - -- Refactor the requirements, code, and/or tests to make them easier for the AI to work with and understand. - -== Fifth Phase - -Finalize the project - -- Review the changes made in the previous phases to ensure they are consistent and correct. -- Make any necessary corrections or adjustments once you feel you aren't advancing. - -NOTE: You must understand the project to make the necessary changes at the end to make it complete. -If you don't understand something, ask for clarification or skip it and possibly return to it later. From 8041968168419e779dd268d331dac5b875f694a1 Mon Sep 17 00:00:00 2001 From: Peter Lawrey Date: Thu, 2 Jan 2025 10:35:16 +0000 Subject: [PATCH 24/31] ch hello-world --- README.adoc | 1 + 1 file changed, 1 insertion(+) diff --git a/README.adoc b/README.adoc index a0c1621..1775817 100644 --- a/README.adoc +++ b/README.adoc @@ -16,6 +16,7 @@ mvn clean install To run a simple example, like the hello-world module: ---- +cd hello-world mvn install exec:java@RecordInputToConsoleMain ---- From 22840bc2828583cbef57c895c993f18d31b10749 Mon Sep 17 00:00:00 2001 From: Peter Lawrey Date: Thu, 2 Jan 2025 10:36:31 +0000 Subject: [PATCH 25/31] Uncomment --- .gitignore | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index de77efc..050b750 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ # Temporarily exclude these files from aide to make the token size manageable -account/ -order-processor/ +#account/ +#order-processor/ ### How to update # This is copied from OpenHFT/.gitignore From c01b96ad7c8cd1445ecae03b162cdbc91b1bdaa5 Mon Sep 17 00:00:00 2001 From: Peter Lawrey Date: Wed, 1 Jan 2025 08:39:05 +0000 Subject: [PATCH 26/31] First Phase - Generate requirements --- order-processor/aide/README.adoc | 28 ++++ order-processor/aide/aide-glossary.adoc | 53 ++++++ order-processor/aide/aide-style-guide.adoc | 98 +++++++++++ order-processor/aide/aide-workflow.adoc | 155 ++++++++++++++++++ order-processor/aide/notes-on-using-aide.adoc | 73 +++++++++ 5 files changed, 407 insertions(+) create mode 100644 order-processor/aide/README.adoc create mode 100644 order-processor/aide/aide-glossary.adoc create mode 100644 order-processor/aide/aide-style-guide.adoc create mode 100644 order-processor/aide/aide-workflow.adoc create mode 100644 order-processor/aide/notes-on-using-aide.adoc diff --git a/order-processor/aide/README.adoc b/order-processor/aide/README.adoc new file mode 100644 index 0000000..6bb259b --- /dev/null +++ b/order-processor/aide/README.adoc @@ -0,0 +1,28 @@ += Documents Supporting AI +:doctype: index +:author: Peter Lawrey +:lang: en-GB +:toc: + +This folder contains supplemental AsciiDoc files that guide usage, workflows, or style conventions for AI-based projects. + +== Overview + +---- +aide/ +├─ aide-glossary.adoc → Terminology definitions +├─ aide-style-guide.adoc → Project coding style guidelines +├─ aide-workflow.adoc → AI workflow explanation +├─ examples.adoc → Example using AI +└─ notes-on-using-aide.adoc → Notes on using AIDE +---- + +Refer to each `.adoc` for details: + +* link:aide-glossary.adoc[AIDE Glossary]: Definitions of key terms (tokens, AIDE, tokens/line, etc.). +* link:aide-style-guide.adoc[AIDE Style Guide: Conventions for naming, coding, and documentation. +* link:aide-workflow.adoc[AIDE Workflow]: The AIDE approach to iterative, documentation-driven engineering. +* link:aide/notes-on-using-aide.adoc[Notes on Using AIDE]: Notes on previous revisions the AI can draw on for further suggestions. +* link:examples.adoc[Examples using AIDE]: Sample AI queries using AIDE. + +These documents are stored in version control alongside the code, ensuring they evolve together. diff --git a/order-processor/aide/aide-glossary.adoc b/order-processor/aide/aide-glossary.adoc new file mode 100644 index 0000000..3da4097 --- /dev/null +++ b/order-processor/aide/aide-glossary.adoc @@ -0,0 +1,53 @@ +[#aide-glossary] += AIDE Glossary +:doctype: glossary +:author: Peter Lawrey +:lang: en-GB +:toc: + +This glossary is a list of key terms used in the project, along with their definitions. + +[quote, Oxford English Dictionary] +aide (noun) an assistant to an important person, especially a political leader. Pronounced _ayd_. + +== Terms in this Project + +* **AIDE**: Artificial Intelligence Development Environment. +* **AsciiDoc**: A lightweight markup language for authoring technical documentation. +* **Context Mode**: The process of generating a `context.asciidoc` file that captures the entire project’s documentation, requirements, and code. +* **Incremental Mode**: The process of generating an `increment.asciidoc` which captures only the newly or recently updated files since the last full `context.asciidoc` build, as described in the “Next Level AIDE” post. +* **Token**: Underlying words or subword units used by AI. +* **Tokens/Line**: A ratio indicating how many tokens appear per non-blank line of text (calculated as total tokens / total lines). +Used as a quick measure of text “density”. +* **Blank Line**: A line containing only whitespace characters (spaces, tabs, etc.). + +== Different Asciidoctor extensions + +Below is a simple table clarifying when to create a shorter `.ad` summary or use a `.asciidoc` file for larger documents. + +[cols="1,1,4",options="header"] +|=== +| File Type +| Typical Size/Usage +| When to Use + +| `.ad` +| Short Summaries +| Generated or manually curated summaries of large docs. Use these when you have huge files that rarely change. Keeps token counts low for AI-based processes without losing essential context. + +| `.ad` +| Alt Text for Binary Files +| A description of an image or binary file can be stored in a `.ad` file for inclusion in the `context.asciidoc` file. + +| `.adoc.ad` +| Short Summary of an AsciiDoc File +| A short summary of a larger AsciiDoc file that is too large to include in the `context.asciidoc` file. + +| `.adoc` +| Medium +| Full documentation files that change relatively often or are critical references. It is useful when your readers or your AI need complete details. + +| `.asciidoc` +| Large +| A long-form extension for AsciiDoc files for larger documents that the tools will ignore as too large. +|=== \ No newline at end of file diff --git a/order-processor/aide/aide-style-guide.adoc b/order-processor/aide/aide-style-guide.adoc new file mode 100644 index 0000000..98de857 --- /dev/null +++ b/order-processor/aide/aide-style-guide.adoc @@ -0,0 +1,98 @@ +[#aide-style-guide] += AIDE Style Guide +:doctype: style-guide +:author: Peter Lawrey +:lang: en-GB +:toc: + +This guide outlines the core conventions for naming, coding, and documentation. By following these practices, the team maintains clarity, consistency, and a high-quality codebase overall. + +== 1. Language and Wording + +1. Use **British English** spelling (e.g., `colour`, `initialise`) in code comments, documentation, and commit messages. +2. Maintain a **professional, concise** tone; avoid slang or overly informal expressions. +3. Define all project-specific or domain-specific terms in a link:aide-glossary.adoc[AIDE Glossary]. +4. Use straight apostrophes and single and double quotes in code and documentation. +5. Code identifiers may use British or American spellings as needed; consistency within a project is key. +6. When adding suggested improvements to `.adoc` files, use comment syntax like `// consider:` +7. **Document-First Approach**: +* As recommended in the "Next Level AIDE" post, write or update `.adoc` content as early as possible, either in parallel with feature development or before coding (similar to TDD). +* This ensures that the single source of truth is always current and that AI-driven code suggestions align with the actual requirements. + +== 2. Directory and Package Structure + +1. **Directory Names**: all lowercase; use hyphens or underscores if necessary (e.g., `my-project-utils/`). +2. **File Names**: +* Java: match the class name (e.g., `MyService.java`). +* Documentation: use `.adoc` (e.g., `project-overview.adoc`) with lowercase, hyphenated file names. +* Configuration: `.yaml` or `.properties`, describing purpose (e.g., `app-config.yaml`). +3. **Packages**: all lowercase, reflecting the maven `groupId` and `artifactId` and subsystem (e.g., `groupId.artifactId.service`). + +== 3. Java Code Style + +=== 3.1 Formatting +1. **Indentation**: 4 spaces; no tabs. +2. **Line Length**: Keep to 128 characters or fewer if possible. +3. **Braces**: +* Open brace on the same line (e.g., `if (condition) {`). +* Close brace on a new line. +4. **Blank Lines**: Separate method definitions with one blank line; group related lines logically. + +=== 3.2 Naming Conventions +1. **Classes**: PascalCase (e.g., `OrderService`). +2. **Methods and Variables**: camelCase (e.g., `processOrder`, `userName`). +3. **Constants**: ALL_CAPS_WITH_UNDERSCORES (e.g., `MAX_BUFFER_SIZE`). + +=== 3.3 Comments and Documentation +1. Use Javadoc for **public** classes and methods, explaining purpose, parameters, and returns. +2. Only add inline comments (`//`) if the logic isn't apparent. +3. Maintain descriptive commit messages in the present tense (e.g., "Add new caching layer"). + +== 4. Testing + +=== 4.1 Test Naming Conventions +1. Test classes should be named by appending `Test` to the class they test (e.g., `OrderServiceTest` for `OrderService`). +2. Test method names should describe the test scenario and expected behavior, using camelCase (e.g., `shouldReturnOrderDetailsWhenOrderExists`). + +=== 4.2 Directory Structure +1. Mirror the main package structure under `src/test/java/...`. +2. Use a logical grouping of tests, such as: +- Unit tests under `src/test/java` +- Integration tests under `src/integrationTest/java` (if applicable). + +=== 4.3 Test Coverage +1. Ensure at least **80% test coverage** for critical business logic. +2. Focus on covering edge cases, particularly for methods with complex branching. +3. Write integration tests for new features or significant architectural changes. + +=== 4.4 Writing Effective Tests +1. Each test should focus on a single behavior or scenario. +2. Use parameterized tests for scenarios with repetitive logic. +3. Avoid hardcoding data when reusable fixtures or factories can be applied. + +=== 4.5 Mocking and Stubbing +1. Use mocking frameworks (e.g., Mockito) to isolate the unit under test. +2. Stub external dependencies to ensure consistent and repeatable test outcomes. +3. Prefer fakes or in-memory implementations for lightweight data operations (e.g., an in-memory database for integration tests). + +=== 4.6 Test Documentation +1. Include comments to clarify the intent of complex test cases. +2. Document preconditions, inputs, and expected outputs within the test itself or in its name. +3. Ensure test cases reference any associated requirements or bug IDs in the comments. + +== 5. Documentation (AsciiDoc) +1. Start each `.adoc` file with a title line (`= Title`) and optional metadata. +2. Organise content under headings: `==`, `===`, `====`. +3. Use `[source,java]` blocks (or similar) for code highlighting. +4. Keep lines concise, highlighting key points in bullet lists or admonitions (e.g., `NOTE:`). + +== 6. Version Control and Commit Messages +1. Write short, clear commit messages in the **present tense** (e.g., "Fix login bug in UserService"). +2. Reference issue IDs if applicable (e.g., "Fix #42: Add advanced search feature"). + +== 7. Additional Best Practices +1. **AutoCloseable Usage**: Where possible, consider implementing `AutoCloseable` to be used in a try-with-resources block, ensuring deterministic cleanup. +2. **Template Management**: Use templates for AI prompts and text blocks to ensure consistency and reduce duplication. + +== 8. Summary +By following these concise guidelines—especially consistent, clear Java formatting and straightforward documentation practices—teams ensure a streamlined, maintainable codebase. Remember to evolve this guide as the project grows and needs change. diff --git a/order-processor/aide/aide-workflow.adoc b/order-processor/aide/aide-workflow.adoc new file mode 100644 index 0000000..55a593d --- /dev/null +++ b/order-processor/aide/aide-workflow.adoc @@ -0,0 +1,155 @@ +[#aide-workflow] += AIDE Workflow +:doctype: workflow +:author: Peter Lawrey +:lang: en-GB +:toc: + +AIDE combines AI-powered code generation's agility with the reliability of Documentation-Driven Development. +AIDE is an artificial intelligence development environment that relies on AsciiDoc for rich documentation and document context uploads to align your team with AI tooling. + +AIDE focuses on two principles: + +1. Use AI for repetitive code tasks such as keeping adoc, code, and tests in sync. +2. Keep requirements, tests, and code in sync with AsciiDoc. + +== Iterative Workflow + +The development follows a simple loop: + +1. **Document Requirements** – Write or update features in AsciiDoc so everyone understands goals. +These can be derived and updated from code and tests as these change. +This can be as much or as little detail as you have. +You can start by asking the AI to generate a detailed functional requirements document from the code you have or an idea. +2. **Write or Update Tests** – Create tests that reflect documented requirements. +AI can suggest skeletons; you refine domain logic. +3. **Write or Update Code** – Let AI produce a boilerplate or refactor existing code. +You verify its validity. +4. **Continuous Review** – AI flags discrepancies; humans resolve them. +5. **Repeat** – Each new or changed requirement triggers another loop. + +== AI <-> Human Collaboration + +[mermaid] +---- +flowchart LR + A((Write/Update
Requirements)) <--> B((Write/Update
Tests)) + B <--> C((Write/Refactor
Code)) + C <--> A +---- + +- **AI**: Proposes repetitive code, test stubs, or refactors. +- **Human**: Reviews correctness, ensures domain fit, and finalises decisions. +- **Outcome**: Software matches documented requirements and passes tests. + +== Elevating the Workflow with Real-Time Documentation + +Building upon our existing Iterative Workflow, the newest recommendation is to emphasise *real-time updates* to documentation. +Ensure the relevant `.adoc` files are updated as soon as features, requirements, or tests change. +This tight loop informs the AI and creates immediate clarity for all team members. + +=== Benefits + +- **Confidence in Documentation**: As the newest post highlights, accurate docs keep you from "miscommunications" that derail real-world outcomes. +- **Better Onboarding**: An up-to-date AsciiDoc set means new developers grasp the system's design and requirements more quickly. +- **Incremental Changes**: Thanks to the incremental mode, AIDE flags any newly updated files so you can keep the documentation in sync. + +== Benefits of Keeping Requirements, Tests, and Code In Sync + +- **Reduced Drift**: Fewer gaps between documentation and iimplementation +- **Faster Feedback**: AI can quickly generate stubs and highlight inconsistencies. +- **Better Quality**: Frequent checks align the code with tests and docs. +- **Smoother Onboarding**: Up-to-date AsciiDoc clarifies the system for new developers. + +== Best Practices + +1. **Single Source of Truth** +Keep docs (AsciiDoc), tests, and code in version control. + +2. **Frequent Iterations** +Update documentation first, then let AI produce or refine code and tests. + +3. **Small Commits** +Each commit should relate to a single requirement or story, making reviewing changes easier. + +4. **Team Buy-In** +Encourage everyone to review AI outputs. +Use a consistent coding style guided by the style guide. + +== Summarising Large Files + +When a code or test file grows very large, it can complicate AI-based reviews or exceed token limits. +AIDE allows you to generate a condensed `.ad` summary for these files. +This step is handy for rarely updated classes or tests that do not need frequent in-depth reference. + +Here is the recommended process: + +1. **Identify Large Files** +Track files that exceed a certain token threshold (as noted in `notes-on-using-aide.adoc`), or are not actively changing. +2. **Use AI to Summarise** +Run the "Summarise this class as an adoc" command or an equivalent script/tool. +This produces a concise `.ad` file containing essential details. +3. **Commit** +Include a concise commit message like "Summarise large class as `.ad` to reduce token length." + +This approach keeps your AI-based tooling efficient and your repository lean while preserving a high-level understanding of classes that do not require frequent full-detail scrutiny. + +''' + +== Why Integrate Chronicle Products? + +Chronicle products are designed to handle high-performance requirements for latency-critical systems. By integrating them with this project: +- **Throughput**: Achieve message processing speeds exceeding 1M messages/second. +- **Latency**: Sub-microsecond latencies for in-memory operations. +- **Scalability**: Handle billions of records with minimal memory overhead. +- **Fault Tolerance**: Ensure robustness and reliability in distributed systems. + +[mermaid] +---- +flowchart TD + A[Your Application] -->|Queue Events| B[Chronicle Queue] + B -->|Shared State| C[Chronicle Map] + A -->|FIX Messages| D[Chronicle FIX Engine] +---- + +When building applications with AIDE, it's important to consider performance, especially for latency-sensitive or high-throughput systems. Chronicle libraries provide specialised tools to optimise various aspects of your architecture: + +=== Chronicle Queue + +https://github.com/OpenHFT/Chronicle-Queue[Chronicle Queue] is a persisted low-latency messaging framework for high-performance applications. Event `MethodReader` and `MethodWriter` can be used to read and write messages at sub-microsecond latencies in an effective high-level way. + +- **Best for:** High-throughput messaging or event-driven architectures. +- **Use Case:** Logging, replaying, or processing large message volumes. +- **Why Use It:** Microsecond-level latencies, append-only logging, and distributed processing. + +=== Chronicle Map + +https://github.com/OpenHFT/Chronicle-Map[Chronicle Map] is a fast, in-memory, non-blocking key-value store. + +- **Best for:** High-performance, persistent key-value stores. +- **Use Case:** Concurrent access to shared data across threads or processes. +- **Why Use It:** Supports billions of entries, low-latency reads/writes, and thread-safe operations. + +=== Chronicle Services + +https://chronicle.software/services/[Chronicle Services] is a framework for high-performance distributed microservices. + +- **Best for:** Scalable, low-latency microservices. +- **Use Case:** Event-driven architectures with robust inter-service communication. +- **Why Use It:** Simplifies development, supports fault tolerance, and optimises sub-millisecond interactions. + +=== Chronicle FIX Engine + +https://chronicle.software/fix-engine/[Chronicle FIX Engine] is a high-performance FIX protocol engine. + +- **Best for:** Financial systems needing low-latency FIX protocol support. +- **Use Case:** Trading platforms, market data systems, or FIX-based gateways. +- **Why Use It:** Deterministic latencies, FIX message handling, and minimal overhead. + +Choosing the correct Chronicle library ensures your applications meet modern performance demands while maintaining a streamlined AIDE-driven development workflow. + +== Conclusion + +AIDE streamlines development by pairing AI's efficiency with clear human guidance. +Document requirements, run AI-driven code generation, verify via tests, and repeat in small steps. +This keeps your project consistent, accurate, and easy to maintain. diff --git a/order-processor/aide/notes-on-using-aide.adoc b/order-processor/aide/notes-on-using-aide.adoc new file mode 100644 index 0000000..b6fe6e0 --- /dev/null +++ b/order-processor/aide/notes-on-using-aide.adoc @@ -0,0 +1,73 @@ +[#notes-on-using-aide] += Notes on using AIDE +:doctype: revision-notes +:author: Peter Lawrey +:lang: en-GB +:toc: + + +Keeping notes on how the project is progressing and what changes you have made is helpful. This can help you understand the project better and make the necessary changes at the end to complete it. + +NOTE: This document can be a template for recording notes on your own project and for helping the AI suggest improvements, as it is also a source of information between phases. + +== First Phase +Create a skeleton for the project and see what the AI can generate for you. These are more a source of inspiration than a source of truth. + +NOTE: Using `o1` is usally better than `4o` as it is more likely to be understood by the AI and produce a better result. `4o` works better for converting images, or web pages to asciidoc. + +- `Generate a functional requirements asciidoc of OMSImpl detailed enough to recreate this implementation including a prompt to execute it.` (Paste the `context.asciidoc`) +- `suggest improvements to this requirements document` + +.Request to clarify assumptions to the requirements document. +---- +Add to the requirements document to note that this component is intended to be in an event driven framework. Each input event comes from a Chronicle Queue, and each output is written to a Chronicle Queue, providing traceability. Validation is performed by the framework calling validate() on the input DTO before calling the matching method, and validate() called on the output DTO before writing to the queue. +Also note; +- On receiving a NewOrderSingle, the system must create an ExecutionReport. If the request is invalid, the system must create and send an OrderCancelReject. +- When an exception is thrown, the framework captures the exception and calls jvmError. +- for a simple example such as this, the component should be able to consistently handle 100k msg/s with low jitter on the 99% latency or better. +---- + +The improved generated requirements document was written to `src/main/adoc/oms-requirements.adoc` + +- `generate a functional requirements document for the packages lost.town.oms.api and lost.town.oms.dto as an asciidoc with enough detail to recreate them` +- `suggest improvements to these requirements` + +.Request to clarify details +---- +add to the api requirements document that the frame work is responsible for calling validate() before passing the event to the component, and for an output DTO before an output is written. +There is an assumption of single threadedness +The YAML tests describe sample usage. see BDD tests, Given `_setup.yaml` events, When `in.yaml` events are tested and Then we expect `out.yaml` events +The `jvmError` is called for every Exception thrown. +This demo relies on Chronicle Wire however a production example would require Chronicle Services to support failover to a replicated system and restartability. +More YAML based tests can be added by including a new directory in the list of paths in the OMSImplTest class. +---- + +- `make the requirements document detailed enough to recreate the API and dto packages` + +Output written as `src/main/adoc/oms-api-requirements.adoc` + +== Second Phase +Generate requirements docs for each events and the API + +- Update the requirements from the code and tests +- Update the tests from the requirements and code +- Update the code from the requirements and tests + +== Third Phase +Review the project + +- Look at more specific improvements +- Make selective changes + +== Fourth Phase +Restructure the project to make it easier to work with + +- Refactor the requirements, code, and/or tests to make them easier for the AI to work with and understand. + +== Fifth Phase +Finalize the project + +- Review the changes made in the previous phases to ensure they are consistent and correct. +- Make any necessary corrections or adjustments once you feel you aren't advancing. + +NOTE: You must understand the project to make the necessary changes at the end to make it complete. If you don't understand something, ask for clarification or skip it and possibly return to it later. From 4de57f5640804e62c3d973790fbd5e2dc683d38b Mon Sep 17 00:00:00 2001 From: Peter Lawrey Date: Wed, 1 Jan 2025 08:32:55 +0000 Subject: [PATCH 27/31] First phase updates --- order-processor/aide/examples.adoc | 316 +++++++++++++++++++++++++++++ 1 file changed, 316 insertions(+) create mode 100644 order-processor/aide/examples.adoc diff --git a/order-processor/aide/examples.adoc b/order-processor/aide/examples.adoc new file mode 100644 index 0000000..be8dde1 --- /dev/null +++ b/order-processor/aide/examples.adoc @@ -0,0 +1,316 @@ +[#aide-examples] += AIDE Examples + +== Prompt: Event Driven Example + +---- +Give a high performance sample program for event logging and event processing. +---- + +**Result** + +Here’s an example that utilises Chronicle Queue's **Method Readers** and **Method Writers** for event logging and processing. +This approach leverages Chronicle Queue's built-in serialization and method invocation capabilities, offering cleaner and more efficient code. + +''' +=== Maven Dependency + +Add the following dependency to your `pom.xml`: + +[source,xml] +---- + + + net.openhft + chronicle-queue + 3.27ea0 + + +---- + +=== Interface Definitions + +Define the interfaces for the events to be logged and processed. + +==== EventInterface.java + +[source,java] +---- +public interface EventInterface { + void onEvent(String event); +} +---- + +''' + +=== EventLogger.java + +The `EventLogger` writes events to the queue using a **Method Writer**. + +[source,java] +---- +import net.openhft.chronicle.queue.ChronicleQueue; + +public class EventLogger implements AutoCloseable { + + private final ChronicleQueue queue; + private final EventInterface eventWriter; + + public EventLogger(String path) { + queue = ChronicleQueue.singleBuilder(path).build(); + eventWriter = queue.methodWriter(EventInterface.class); + } + + public void logEvent(String event) { + eventWriter.onEvent(event); + } + + public void close() { + queue.close(); + } + + public static void main(String[] args) { + try (EventLogger logger = new EventLogger("events-queue")) { + logger.logEvent("UserLogin: user123"); + logger.logEvent("FileUpload: fileX.pdf"); + } + } +} +---- + +''' + +=== EventProcessor.java + +The `EventProcessor` reads and processes events using a **Method Reader**. + +[source,java] +---- +import net.openhft.chronicle.queue.ChronicleQueue; +import net.openhft.chronicle.bytes.MethodReader; +public class EventProcessor { + + private final ChronicleQueue queue; + private final MethodReader reader; + + public EventProcessor(String path) { + queue = ChronicleQueue.singleBuilder(path).build(); + reader = queue.createTailer().methodReader((EventInterface) this::processEvent); + } + + private void processEvent(String event) { + System.out.println("Processing event: " + event); + } + + public void startProcessing() { + while (true) { + if (!reader.readOne()) { + try { + Thread.sleep(10); // Prevent busy spinning + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + break; + } + } + } + } + + public static void main(String[] args) { + EventProcessor processor = new EventProcessor("events-queue"); + processor.startProcessing(); + } +} +---- + +''' + +=== Key Changes + +1. **Event Writer**: +- Uses `ChronicleQueue.methodWriter(EventInterface.class)` to generate a proxy implementation of `EventInterface`. +- Simplifies the logging of events by invoking interface methods. + +2. **Event Reader**: +- Uses `ChronicleQueue.methodReader(...)` to process events by calling the specified interface method. + +3. **Event Processing Logic**: +- The `processEvent` method is invoked directly for each event by the `MethodReader`. + +''' + +=== Execution + +1. Run `EventLogger` to log events. +2. Start `EventProcessor` to process the logged events in real-time. + +''' + +=== Benefits of Using Method Readers and Writers + +- **Clean API**: Method calls directly represent events, reducing boilerplate. +- **Type Safety**: Ensures compile-time validation of event definitions. +- **High Performance**: Optimized serialization and deserialization with minimal latency. + +This approach streamlines event-driven systems while maintaining high performance and simplicity. + +== Prompt: Key Value Store Example + +---- +Given a java example of a high performance key value store +---- + +**Result** + +Here’s an example of a high-performance key-value store implemented in Java using **Chronicle Map**, a low-latency, in-memory, persisted key-value store suitable for high-throughput applications. + +''' + +=== Maven Dependency + +Add the following dependency to your `pom.xml`: + +[source,xml] +---- + + + net.openhft + chronicle-map + 3.27ea0 + + +---- + +''' + +=== Implementation + +==== KeyValueStore.java + +This class provides a simple interface to interact with a Chronicle Map. + +[source,java] +---- +package build.chronicle.aide; + +import net.openhft.chronicle.map.ChronicleMap; + +import java.io.File; +import java.io.IOException; + +public class KeyValueStore implements AutoCloseable { + + private final ChronicleMap map; + + public KeyValueStore(String filePath) throws IOException { + // Create or load a persisted Chronicle Map + map = ChronicleMap + .of(String.class, String.class) + .averageKey("exampleKey") + .averageValue("exampleValue") + .entries(1_000_000) // Number of expected entries + .createPersistedTo(new File(filePath)); + } + + public void put(String key, String value) { + map.put(key, value); + } + + public String get(String key) { + return map.get(key); + } + + public void remove(String key) { + map.remove(key); + } + + public void close() { + map.close(); + } + + public static void main(String[] args) throws IOException { + String filePath = "key-value-store.dat"; + + try (KeyValueStore store = new KeyValueStore(filePath)) { + // Insert key-value pairs + store.put("user:1", "Alice"); + store.put("user:2", "Bob"); + + // Retrieve and print values + System.out.println("user:1 -> " + store.get("user:1")); + System.out.println("user:2 -> " + store.get("user:2")); + + // Remove a key + store.remove("user:1"); + System.out.println("user:1 -> " + store.get("user:1")); // Should print null + } + } +} +---- + +''' + +=== Features + +1. **Persistence**: +- The data is stored in a file (`key-value-store.dat`) and survives application restarts. +- Use `.createOrRecoverPersistedTo(filePath)` to create or recover a persisted map. + +2. **Performance**: +- Optimized for low-latency reads and writes. +- Suitable for applications requiring millions of operations per second. + +3. **Scalability**: +- Supports billions of entries with minimal memory overhead. + +4. **Ease of Use**: +- Simple API for `put`, `get`, and `remove` operations. + +''' + +=== How It Works + +1. **Initialization**: +- The Chronicle Map is configured with average key/value sizes and the expected number of entries. + +2. **Operations**: +- Keys and values are stored as `String`. +This can be replaced with any serializable class for more complex use cases. + +3. **Persistence**: +- The map persists to a file, enabling recovery after application restarts. + +4. **Cleanup**: +- The `close` method ensures all resources are released and the map is safely persisted. + +''' + +=== Example Output + +[source,text] +---- +user:1 -> Alice +user:2 -> Bob +user:1 -> null +---- + +''' + +=== Advanced Usage + +1. **Custom Key/Value Types**: +Replace `String.class` with custom types for both keys and values. + +[source,java] +---- +ChronicleMap.of(MyKey.class, MyValue.class) + .averageKey(new MyKey()) + .averageValue(new MyValue()) + .entries(1_000_000) + .createOrRecoverPersistedTo("custom-map.dat"); +---- + +2. **Concurrency**: +- Chronicle Map supports concurrent access across threads and processes. +- It’s thread-safe without additional locking mechanisms. + +This example demonstrates a robust and efficient key-value store suitable for high-performance applications like caching, distributed systems, or analytics. From a1f89282f0430b1fc30a5fa23340d37b51ccda9e Mon Sep 17 00:00:00 2001 From: Peter Lawrey Date: Thu, 2 Jan 2025 10:40:10 +0000 Subject: [PATCH 28/31] Not needed --- account/list_files_asciidoc.sh | 20 -------------------- 1 file changed, 20 deletions(-) delete mode 100644 account/list_files_asciidoc.sh diff --git a/account/list_files_asciidoc.sh b/account/list_files_asciidoc.sh deleted file mode 100644 index c331765..0000000 --- a/account/list_files_asciidoc.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env bash - -# Usage: ./list_files_asciidoc.sh [directory] > output.adoc -# If no directory is provided, the script defaults to the current directory. - -DIR="${1:-.}" - -cd $DIR -echo "= Directory Content" -echo - -# Find all regular files recursively -find "pom.xml" "src" "*.adoc" -type f | grep -v out- | sort | while read -r file; do - echo "== File: $file" - echo "[source]" - echo "----" - cat "$file" - echo "----" - echo -done From a52b961ea7977712e55f2bbe410df20538253795 Mon Sep 17 00:00:00 2001 From: Peter Lawrey Date: Thu, 2 Jan 2025 10:43:53 +0000 Subject: [PATCH 29/31] Not needed --- order-processor/aide/README.adoc | 28 - order-processor/aide/aide-glossary.adoc | 53 - order-processor/aide/aide-style-guide.adoc | 98 - order-processor/aide/aide-workflow.adoc | 155 - order-processor/aide/examples.adoc | 316 -- order-processor/aide/notes-on-using-aide.adoc | 73 - order-processor/context.asciidoc | 3199 ----------------- 7 files changed, 3922 deletions(-) delete mode 100644 order-processor/aide/README.adoc delete mode 100644 order-processor/aide/aide-glossary.adoc delete mode 100644 order-processor/aide/aide-style-guide.adoc delete mode 100644 order-processor/aide/aide-workflow.adoc delete mode 100644 order-processor/aide/examples.adoc delete mode 100644 order-processor/aide/notes-on-using-aide.adoc delete mode 100644 order-processor/context.asciidoc diff --git a/order-processor/aide/README.adoc b/order-processor/aide/README.adoc deleted file mode 100644 index 6bb259b..0000000 --- a/order-processor/aide/README.adoc +++ /dev/null @@ -1,28 +0,0 @@ -= Documents Supporting AI -:doctype: index -:author: Peter Lawrey -:lang: en-GB -:toc: - -This folder contains supplemental AsciiDoc files that guide usage, workflows, or style conventions for AI-based projects. - -== Overview - ----- -aide/ -├─ aide-glossary.adoc → Terminology definitions -├─ aide-style-guide.adoc → Project coding style guidelines -├─ aide-workflow.adoc → AI workflow explanation -├─ examples.adoc → Example using AI -└─ notes-on-using-aide.adoc → Notes on using AIDE ----- - -Refer to each `.adoc` for details: - -* link:aide-glossary.adoc[AIDE Glossary]: Definitions of key terms (tokens, AIDE, tokens/line, etc.). -* link:aide-style-guide.adoc[AIDE Style Guide: Conventions for naming, coding, and documentation. -* link:aide-workflow.adoc[AIDE Workflow]: The AIDE approach to iterative, documentation-driven engineering. -* link:aide/notes-on-using-aide.adoc[Notes on Using AIDE]: Notes on previous revisions the AI can draw on for further suggestions. -* link:examples.adoc[Examples using AIDE]: Sample AI queries using AIDE. - -These documents are stored in version control alongside the code, ensuring they evolve together. diff --git a/order-processor/aide/aide-glossary.adoc b/order-processor/aide/aide-glossary.adoc deleted file mode 100644 index 3da4097..0000000 --- a/order-processor/aide/aide-glossary.adoc +++ /dev/null @@ -1,53 +0,0 @@ -[#aide-glossary] -= AIDE Glossary -:doctype: glossary -:author: Peter Lawrey -:lang: en-GB -:toc: - -This glossary is a list of key terms used in the project, along with their definitions. - -[quote, Oxford English Dictionary] -aide (noun) an assistant to an important person, especially a political leader. Pronounced _ayd_. - -== Terms in this Project - -* **AIDE**: Artificial Intelligence Development Environment. -* **AsciiDoc**: A lightweight markup language for authoring technical documentation. -* **Context Mode**: The process of generating a `context.asciidoc` file that captures the entire project’s documentation, requirements, and code. -* **Incremental Mode**: The process of generating an `increment.asciidoc` which captures only the newly or recently updated files since the last full `context.asciidoc` build, as described in the “Next Level AIDE” post. -* **Token**: Underlying words or subword units used by AI. -* **Tokens/Line**: A ratio indicating how many tokens appear per non-blank line of text (calculated as total tokens / total lines). -Used as a quick measure of text “density”. -* **Blank Line**: A line containing only whitespace characters (spaces, tabs, etc.). - -== Different Asciidoctor extensions - -Below is a simple table clarifying when to create a shorter `.ad` summary or use a `.asciidoc` file for larger documents. - -[cols="1,1,4",options="header"] -|=== -| File Type -| Typical Size/Usage -| When to Use - -| `.ad` -| Short Summaries -| Generated or manually curated summaries of large docs. Use these when you have huge files that rarely change. Keeps token counts low for AI-based processes without losing essential context. - -| `.ad` -| Alt Text for Binary Files -| A description of an image or binary file can be stored in a `.ad` file for inclusion in the `context.asciidoc` file. - -| `.adoc.ad` -| Short Summary of an AsciiDoc File -| A short summary of a larger AsciiDoc file that is too large to include in the `context.asciidoc` file. - -| `.adoc` -| Medium -| Full documentation files that change relatively often or are critical references. It is useful when your readers or your AI need complete details. - -| `.asciidoc` -| Large -| A long-form extension for AsciiDoc files for larger documents that the tools will ignore as too large. -|=== \ No newline at end of file diff --git a/order-processor/aide/aide-style-guide.adoc b/order-processor/aide/aide-style-guide.adoc deleted file mode 100644 index 98de857..0000000 --- a/order-processor/aide/aide-style-guide.adoc +++ /dev/null @@ -1,98 +0,0 @@ -[#aide-style-guide] -= AIDE Style Guide -:doctype: style-guide -:author: Peter Lawrey -:lang: en-GB -:toc: - -This guide outlines the core conventions for naming, coding, and documentation. By following these practices, the team maintains clarity, consistency, and a high-quality codebase overall. - -== 1. Language and Wording - -1. Use **British English** spelling (e.g., `colour`, `initialise`) in code comments, documentation, and commit messages. -2. Maintain a **professional, concise** tone; avoid slang or overly informal expressions. -3. Define all project-specific or domain-specific terms in a link:aide-glossary.adoc[AIDE Glossary]. -4. Use straight apostrophes and single and double quotes in code and documentation. -5. Code identifiers may use British or American spellings as needed; consistency within a project is key. -6. When adding suggested improvements to `.adoc` files, use comment syntax like `// consider:` -7. **Document-First Approach**: -* As recommended in the "Next Level AIDE" post, write or update `.adoc` content as early as possible, either in parallel with feature development or before coding (similar to TDD). -* This ensures that the single source of truth is always current and that AI-driven code suggestions align with the actual requirements. - -== 2. Directory and Package Structure - -1. **Directory Names**: all lowercase; use hyphens or underscores if necessary (e.g., `my-project-utils/`). -2. **File Names**: -* Java: match the class name (e.g., `MyService.java`). -* Documentation: use `.adoc` (e.g., `project-overview.adoc`) with lowercase, hyphenated file names. -* Configuration: `.yaml` or `.properties`, describing purpose (e.g., `app-config.yaml`). -3. **Packages**: all lowercase, reflecting the maven `groupId` and `artifactId` and subsystem (e.g., `groupId.artifactId.service`). - -== 3. Java Code Style - -=== 3.1 Formatting -1. **Indentation**: 4 spaces; no tabs. -2. **Line Length**: Keep to 128 characters or fewer if possible. -3. **Braces**: -* Open brace on the same line (e.g., `if (condition) {`). -* Close brace on a new line. -4. **Blank Lines**: Separate method definitions with one blank line; group related lines logically. - -=== 3.2 Naming Conventions -1. **Classes**: PascalCase (e.g., `OrderService`). -2. **Methods and Variables**: camelCase (e.g., `processOrder`, `userName`). -3. **Constants**: ALL_CAPS_WITH_UNDERSCORES (e.g., `MAX_BUFFER_SIZE`). - -=== 3.3 Comments and Documentation -1. Use Javadoc for **public** classes and methods, explaining purpose, parameters, and returns. -2. Only add inline comments (`//`) if the logic isn't apparent. -3. Maintain descriptive commit messages in the present tense (e.g., "Add new caching layer"). - -== 4. Testing - -=== 4.1 Test Naming Conventions -1. Test classes should be named by appending `Test` to the class they test (e.g., `OrderServiceTest` for `OrderService`). -2. Test method names should describe the test scenario and expected behavior, using camelCase (e.g., `shouldReturnOrderDetailsWhenOrderExists`). - -=== 4.2 Directory Structure -1. Mirror the main package structure under `src/test/java/...`. -2. Use a logical grouping of tests, such as: -- Unit tests under `src/test/java` -- Integration tests under `src/integrationTest/java` (if applicable). - -=== 4.3 Test Coverage -1. Ensure at least **80% test coverage** for critical business logic. -2. Focus on covering edge cases, particularly for methods with complex branching. -3. Write integration tests for new features or significant architectural changes. - -=== 4.4 Writing Effective Tests -1. Each test should focus on a single behavior or scenario. -2. Use parameterized tests for scenarios with repetitive logic. -3. Avoid hardcoding data when reusable fixtures or factories can be applied. - -=== 4.5 Mocking and Stubbing -1. Use mocking frameworks (e.g., Mockito) to isolate the unit under test. -2. Stub external dependencies to ensure consistent and repeatable test outcomes. -3. Prefer fakes or in-memory implementations for lightweight data operations (e.g., an in-memory database for integration tests). - -=== 4.6 Test Documentation -1. Include comments to clarify the intent of complex test cases. -2. Document preconditions, inputs, and expected outputs within the test itself or in its name. -3. Ensure test cases reference any associated requirements or bug IDs in the comments. - -== 5. Documentation (AsciiDoc) -1. Start each `.adoc` file with a title line (`= Title`) and optional metadata. -2. Organise content under headings: `==`, `===`, `====`. -3. Use `[source,java]` blocks (or similar) for code highlighting. -4. Keep lines concise, highlighting key points in bullet lists or admonitions (e.g., `NOTE:`). - -== 6. Version Control and Commit Messages -1. Write short, clear commit messages in the **present tense** (e.g., "Fix login bug in UserService"). -2. Reference issue IDs if applicable (e.g., "Fix #42: Add advanced search feature"). - -== 7. Additional Best Practices -1. **AutoCloseable Usage**: Where possible, consider implementing `AutoCloseable` to be used in a try-with-resources block, ensuring deterministic cleanup. -2. **Template Management**: Use templates for AI prompts and text blocks to ensure consistency and reduce duplication. - -== 8. Summary -By following these concise guidelines—especially consistent, clear Java formatting and straightforward documentation practices—teams ensure a streamlined, maintainable codebase. Remember to evolve this guide as the project grows and needs change. diff --git a/order-processor/aide/aide-workflow.adoc b/order-processor/aide/aide-workflow.adoc deleted file mode 100644 index 55a593d..0000000 --- a/order-processor/aide/aide-workflow.adoc +++ /dev/null @@ -1,155 +0,0 @@ -[#aide-workflow] -= AIDE Workflow -:doctype: workflow -:author: Peter Lawrey -:lang: en-GB -:toc: - -AIDE combines AI-powered code generation's agility with the reliability of Documentation-Driven Development. -AIDE is an artificial intelligence development environment that relies on AsciiDoc for rich documentation and document context uploads to align your team with AI tooling. - -AIDE focuses on two principles: - -1. Use AI for repetitive code tasks such as keeping adoc, code, and tests in sync. -2. Keep requirements, tests, and code in sync with AsciiDoc. - -== Iterative Workflow - -The development follows a simple loop: - -1. **Document Requirements** – Write or update features in AsciiDoc so everyone understands goals. -These can be derived and updated from code and tests as these change. -This can be as much or as little detail as you have. -You can start by asking the AI to generate a detailed functional requirements document from the code you have or an idea. -2. **Write or Update Tests** – Create tests that reflect documented requirements. -AI can suggest skeletons; you refine domain logic. -3. **Write or Update Code** – Let AI produce a boilerplate or refactor existing code. -You verify its validity. -4. **Continuous Review** – AI flags discrepancies; humans resolve them. -5. **Repeat** – Each new or changed requirement triggers another loop. - -== AI <-> Human Collaboration - -[mermaid] ----- -flowchart LR - A((Write/Update
Requirements)) <--> B((Write/Update
Tests)) - B <--> C((Write/Refactor
Code)) - C <--> A ----- - -- **AI**: Proposes repetitive code, test stubs, or refactors. -- **Human**: Reviews correctness, ensures domain fit, and finalises decisions. -- **Outcome**: Software matches documented requirements and passes tests. - -== Elevating the Workflow with Real-Time Documentation - -Building upon our existing Iterative Workflow, the newest recommendation is to emphasise *real-time updates* to documentation. -Ensure the relevant `.adoc` files are updated as soon as features, requirements, or tests change. -This tight loop informs the AI and creates immediate clarity for all team members. - -=== Benefits - -- **Confidence in Documentation**: As the newest post highlights, accurate docs keep you from "miscommunications" that derail real-world outcomes. -- **Better Onboarding**: An up-to-date AsciiDoc set means new developers grasp the system's design and requirements more quickly. -- **Incremental Changes**: Thanks to the incremental mode, AIDE flags any newly updated files so you can keep the documentation in sync. - -== Benefits of Keeping Requirements, Tests, and Code In Sync - -- **Reduced Drift**: Fewer gaps between documentation and iimplementation -- **Faster Feedback**: AI can quickly generate stubs and highlight inconsistencies. -- **Better Quality**: Frequent checks align the code with tests and docs. -- **Smoother Onboarding**: Up-to-date AsciiDoc clarifies the system for new developers. - -== Best Practices - -1. **Single Source of Truth** -Keep docs (AsciiDoc), tests, and code in version control. - -2. **Frequent Iterations** -Update documentation first, then let AI produce or refine code and tests. - -3. **Small Commits** -Each commit should relate to a single requirement or story, making reviewing changes easier. - -4. **Team Buy-In** -Encourage everyone to review AI outputs. -Use a consistent coding style guided by the style guide. - -== Summarising Large Files - -When a code or test file grows very large, it can complicate AI-based reviews or exceed token limits. -AIDE allows you to generate a condensed `.ad` summary for these files. -This step is handy for rarely updated classes or tests that do not need frequent in-depth reference. - -Here is the recommended process: - -1. **Identify Large Files** -Track files that exceed a certain token threshold (as noted in `notes-on-using-aide.adoc`), or are not actively changing. -2. **Use AI to Summarise** -Run the "Summarise this class as an adoc" command or an equivalent script/tool. -This produces a concise `.ad` file containing essential details. -3. **Commit** -Include a concise commit message like "Summarise large class as `.ad` to reduce token length." - -This approach keeps your AI-based tooling efficient and your repository lean while preserving a high-level understanding of classes that do not require frequent full-detail scrutiny. - -''' - -== Why Integrate Chronicle Products? - -Chronicle products are designed to handle high-performance requirements for latency-critical systems. By integrating them with this project: -- **Throughput**: Achieve message processing speeds exceeding 1M messages/second. -- **Latency**: Sub-microsecond latencies for in-memory operations. -- **Scalability**: Handle billions of records with minimal memory overhead. -- **Fault Tolerance**: Ensure robustness and reliability in distributed systems. - -[mermaid] ----- -flowchart TD - A[Your Application] -->|Queue Events| B[Chronicle Queue] - B -->|Shared State| C[Chronicle Map] - A -->|FIX Messages| D[Chronicle FIX Engine] ----- - -When building applications with AIDE, it's important to consider performance, especially for latency-sensitive or high-throughput systems. Chronicle libraries provide specialised tools to optimise various aspects of your architecture: - -=== Chronicle Queue - -https://github.com/OpenHFT/Chronicle-Queue[Chronicle Queue] is a persisted low-latency messaging framework for high-performance applications. Event `MethodReader` and `MethodWriter` can be used to read and write messages at sub-microsecond latencies in an effective high-level way. - -- **Best for:** High-throughput messaging or event-driven architectures. -- **Use Case:** Logging, replaying, or processing large message volumes. -- **Why Use It:** Microsecond-level latencies, append-only logging, and distributed processing. - -=== Chronicle Map - -https://github.com/OpenHFT/Chronicle-Map[Chronicle Map] is a fast, in-memory, non-blocking key-value store. - -- **Best for:** High-performance, persistent key-value stores. -- **Use Case:** Concurrent access to shared data across threads or processes. -- **Why Use It:** Supports billions of entries, low-latency reads/writes, and thread-safe operations. - -=== Chronicle Services - -https://chronicle.software/services/[Chronicle Services] is a framework for high-performance distributed microservices. - -- **Best for:** Scalable, low-latency microservices. -- **Use Case:** Event-driven architectures with robust inter-service communication. -- **Why Use It:** Simplifies development, supports fault tolerance, and optimises sub-millisecond interactions. - -=== Chronicle FIX Engine - -https://chronicle.software/fix-engine/[Chronicle FIX Engine] is a high-performance FIX protocol engine. - -- **Best for:** Financial systems needing low-latency FIX protocol support. -- **Use Case:** Trading platforms, market data systems, or FIX-based gateways. -- **Why Use It:** Deterministic latencies, FIX message handling, and minimal overhead. - -Choosing the correct Chronicle library ensures your applications meet modern performance demands while maintaining a streamlined AIDE-driven development workflow. - -== Conclusion - -AIDE streamlines development by pairing AI's efficiency with clear human guidance. -Document requirements, run AI-driven code generation, verify via tests, and repeat in small steps. -This keeps your project consistent, accurate, and easy to maintain. diff --git a/order-processor/aide/examples.adoc b/order-processor/aide/examples.adoc deleted file mode 100644 index be8dde1..0000000 --- a/order-processor/aide/examples.adoc +++ /dev/null @@ -1,316 +0,0 @@ -[#aide-examples] -= AIDE Examples - -== Prompt: Event Driven Example - ----- -Give a high performance sample program for event logging and event processing. ----- - -**Result** - -Here’s an example that utilises Chronicle Queue's **Method Readers** and **Method Writers** for event logging and processing. -This approach leverages Chronicle Queue's built-in serialization and method invocation capabilities, offering cleaner and more efficient code. - -''' -=== Maven Dependency - -Add the following dependency to your `pom.xml`: - -[source,xml] ----- - - - net.openhft - chronicle-queue - 3.27ea0 - - ----- - -=== Interface Definitions - -Define the interfaces for the events to be logged and processed. - -==== EventInterface.java - -[source,java] ----- -public interface EventInterface { - void onEvent(String event); -} ----- - -''' - -=== EventLogger.java - -The `EventLogger` writes events to the queue using a **Method Writer**. - -[source,java] ----- -import net.openhft.chronicle.queue.ChronicleQueue; - -public class EventLogger implements AutoCloseable { - - private final ChronicleQueue queue; - private final EventInterface eventWriter; - - public EventLogger(String path) { - queue = ChronicleQueue.singleBuilder(path).build(); - eventWriter = queue.methodWriter(EventInterface.class); - } - - public void logEvent(String event) { - eventWriter.onEvent(event); - } - - public void close() { - queue.close(); - } - - public static void main(String[] args) { - try (EventLogger logger = new EventLogger("events-queue")) { - logger.logEvent("UserLogin: user123"); - logger.logEvent("FileUpload: fileX.pdf"); - } - } -} ----- - -''' - -=== EventProcessor.java - -The `EventProcessor` reads and processes events using a **Method Reader**. - -[source,java] ----- -import net.openhft.chronicle.queue.ChronicleQueue; -import net.openhft.chronicle.bytes.MethodReader; -public class EventProcessor { - - private final ChronicleQueue queue; - private final MethodReader reader; - - public EventProcessor(String path) { - queue = ChronicleQueue.singleBuilder(path).build(); - reader = queue.createTailer().methodReader((EventInterface) this::processEvent); - } - - private void processEvent(String event) { - System.out.println("Processing event: " + event); - } - - public void startProcessing() { - while (true) { - if (!reader.readOne()) { - try { - Thread.sleep(10); // Prevent busy spinning - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - break; - } - } - } - } - - public static void main(String[] args) { - EventProcessor processor = new EventProcessor("events-queue"); - processor.startProcessing(); - } -} ----- - -''' - -=== Key Changes - -1. **Event Writer**: -- Uses `ChronicleQueue.methodWriter(EventInterface.class)` to generate a proxy implementation of `EventInterface`. -- Simplifies the logging of events by invoking interface methods. - -2. **Event Reader**: -- Uses `ChronicleQueue.methodReader(...)` to process events by calling the specified interface method. - -3. **Event Processing Logic**: -- The `processEvent` method is invoked directly for each event by the `MethodReader`. - -''' - -=== Execution - -1. Run `EventLogger` to log events. -2. Start `EventProcessor` to process the logged events in real-time. - -''' - -=== Benefits of Using Method Readers and Writers - -- **Clean API**: Method calls directly represent events, reducing boilerplate. -- **Type Safety**: Ensures compile-time validation of event definitions. -- **High Performance**: Optimized serialization and deserialization with minimal latency. - -This approach streamlines event-driven systems while maintaining high performance and simplicity. - -== Prompt: Key Value Store Example - ----- -Given a java example of a high performance key value store ----- - -**Result** - -Here’s an example of a high-performance key-value store implemented in Java using **Chronicle Map**, a low-latency, in-memory, persisted key-value store suitable for high-throughput applications. - -''' - -=== Maven Dependency - -Add the following dependency to your `pom.xml`: - -[source,xml] ----- - - - net.openhft - chronicle-map - 3.27ea0 - - ----- - -''' - -=== Implementation - -==== KeyValueStore.java - -This class provides a simple interface to interact with a Chronicle Map. - -[source,java] ----- -package build.chronicle.aide; - -import net.openhft.chronicle.map.ChronicleMap; - -import java.io.File; -import java.io.IOException; - -public class KeyValueStore implements AutoCloseable { - - private final ChronicleMap map; - - public KeyValueStore(String filePath) throws IOException { - // Create or load a persisted Chronicle Map - map = ChronicleMap - .of(String.class, String.class) - .averageKey("exampleKey") - .averageValue("exampleValue") - .entries(1_000_000) // Number of expected entries - .createPersistedTo(new File(filePath)); - } - - public void put(String key, String value) { - map.put(key, value); - } - - public String get(String key) { - return map.get(key); - } - - public void remove(String key) { - map.remove(key); - } - - public void close() { - map.close(); - } - - public static void main(String[] args) throws IOException { - String filePath = "key-value-store.dat"; - - try (KeyValueStore store = new KeyValueStore(filePath)) { - // Insert key-value pairs - store.put("user:1", "Alice"); - store.put("user:2", "Bob"); - - // Retrieve and print values - System.out.println("user:1 -> " + store.get("user:1")); - System.out.println("user:2 -> " + store.get("user:2")); - - // Remove a key - store.remove("user:1"); - System.out.println("user:1 -> " + store.get("user:1")); // Should print null - } - } -} ----- - -''' - -=== Features - -1. **Persistence**: -- The data is stored in a file (`key-value-store.dat`) and survives application restarts. -- Use `.createOrRecoverPersistedTo(filePath)` to create or recover a persisted map. - -2. **Performance**: -- Optimized for low-latency reads and writes. -- Suitable for applications requiring millions of operations per second. - -3. **Scalability**: -- Supports billions of entries with minimal memory overhead. - -4. **Ease of Use**: -- Simple API for `put`, `get`, and `remove` operations. - -''' - -=== How It Works - -1. **Initialization**: -- The Chronicle Map is configured with average key/value sizes and the expected number of entries. - -2. **Operations**: -- Keys and values are stored as `String`. -This can be replaced with any serializable class for more complex use cases. - -3. **Persistence**: -- The map persists to a file, enabling recovery after application restarts. - -4. **Cleanup**: -- The `close` method ensures all resources are released and the map is safely persisted. - -''' - -=== Example Output - -[source,text] ----- -user:1 -> Alice -user:2 -> Bob -user:1 -> null ----- - -''' - -=== Advanced Usage - -1. **Custom Key/Value Types**: -Replace `String.class` with custom types for both keys and values. - -[source,java] ----- -ChronicleMap.of(MyKey.class, MyValue.class) - .averageKey(new MyKey()) - .averageValue(new MyValue()) - .entries(1_000_000) - .createOrRecoverPersistedTo("custom-map.dat"); ----- - -2. **Concurrency**: -- Chronicle Map supports concurrent access across threads and processes. -- It’s thread-safe without additional locking mechanisms. - -This example demonstrates a robust and efficient key-value store suitable for high-performance applications like caching, distributed systems, or analytics. diff --git a/order-processor/aide/notes-on-using-aide.adoc b/order-processor/aide/notes-on-using-aide.adoc deleted file mode 100644 index b6fe6e0..0000000 --- a/order-processor/aide/notes-on-using-aide.adoc +++ /dev/null @@ -1,73 +0,0 @@ -[#notes-on-using-aide] -= Notes on using AIDE -:doctype: revision-notes -:author: Peter Lawrey -:lang: en-GB -:toc: - - -Keeping notes on how the project is progressing and what changes you have made is helpful. This can help you understand the project better and make the necessary changes at the end to complete it. - -NOTE: This document can be a template for recording notes on your own project and for helping the AI suggest improvements, as it is also a source of information between phases. - -== First Phase -Create a skeleton for the project and see what the AI can generate for you. These are more a source of inspiration than a source of truth. - -NOTE: Using `o1` is usally better than `4o` as it is more likely to be understood by the AI and produce a better result. `4o` works better for converting images, or web pages to asciidoc. - -- `Generate a functional requirements asciidoc of OMSImpl detailed enough to recreate this implementation including a prompt to execute it.` (Paste the `context.asciidoc`) -- `suggest improvements to this requirements document` - -.Request to clarify assumptions to the requirements document. ----- -Add to the requirements document to note that this component is intended to be in an event driven framework. Each input event comes from a Chronicle Queue, and each output is written to a Chronicle Queue, providing traceability. Validation is performed by the framework calling validate() on the input DTO before calling the matching method, and validate() called on the output DTO before writing to the queue. -Also note; -- On receiving a NewOrderSingle, the system must create an ExecutionReport. If the request is invalid, the system must create and send an OrderCancelReject. -- When an exception is thrown, the framework captures the exception and calls jvmError. -- for a simple example such as this, the component should be able to consistently handle 100k msg/s with low jitter on the 99% latency or better. ----- - -The improved generated requirements document was written to `src/main/adoc/oms-requirements.adoc` - -- `generate a functional requirements document for the packages lost.town.oms.api and lost.town.oms.dto as an asciidoc with enough detail to recreate them` -- `suggest improvements to these requirements` - -.Request to clarify details ----- -add to the api requirements document that the frame work is responsible for calling validate() before passing the event to the component, and for an output DTO before an output is written. -There is an assumption of single threadedness -The YAML tests describe sample usage. see BDD tests, Given `_setup.yaml` events, When `in.yaml` events are tested and Then we expect `out.yaml` events -The `jvmError` is called for every Exception thrown. -This demo relies on Chronicle Wire however a production example would require Chronicle Services to support failover to a replicated system and restartability. -More YAML based tests can be added by including a new directory in the list of paths in the OMSImplTest class. ----- - -- `make the requirements document detailed enough to recreate the API and dto packages` - -Output written as `src/main/adoc/oms-api-requirements.adoc` - -== Second Phase -Generate requirements docs for each events and the API - -- Update the requirements from the code and tests -- Update the tests from the requirements and code -- Update the code from the requirements and tests - -== Third Phase -Review the project - -- Look at more specific improvements -- Make selective changes - -== Fourth Phase -Restructure the project to make it easier to work with - -- Refactor the requirements, code, and/or tests to make them easier for the AI to work with and understand. - -== Fifth Phase -Finalize the project - -- Review the changes made in the previous phases to ensure they are consistent and correct. -- Make any necessary corrections or adjustments once you feel you aren't advancing. - -NOTE: You must understand the project to make the necessary changes at the end to make it complete. If you don't understand something, ask for clarification or skip it and possibly return to it later. diff --git a/order-processor/context.asciidoc b/order-processor/context.asciidoc deleted file mode 100644 index aedf202..0000000 --- a/order-processor/context.asciidoc +++ /dev/null @@ -1,3199 +0,0 @@ -= Directory Content - -== File: pom.xml -.... - - - 4.0.0 - - - run.chronicle - chronicle-queue-demo - 1.0-SNAPSHOT - ../pom.xml - - - order-processor - OpenHFT/${project.parent.artifactId}/${project.artifactId} - - - - net.openhft - jlbh - - - - - - - org.codehaus.mojo - exec-maven-plugin - - - bench - - exec - - - ${java.home}/bin/java - ${jvm.requiredArgs} -classpath %classpath town.lost.oms.OMSBenchmarkMain - - - - adder - - exec - - - ${java.home}/bin/java - ${jvm.requiredArgs} -classpath %classpath town.lost.oms.OrderAdderMain - - - - viewer - - exec - - - ${java.home}/bin/java - ${jvm.requiredArgs} -classpath %classpath town.lost.oms.OrderViewerMain - - - - dump - - exec - - - ${java.home}/bin/java - ${jvm.requiredArgs} -classpath %classpath net.openhft.chronicle.queue.DumpQueueMain in - - - - - - - -.... - -Lines 70, Blanks 4, Tokens 645 - -== File: README.adoc -.... -= Order Processor Demo -Peter Lawrey - -The Order Processor Demo showcases the implementation of services using messages based on the Financial Information Exchange (FIX) protocol. This project simulates an order processing system, including features such as creating new orders, cancelling specific orders, and cancelling all orders. The project is designed to mimic real-world financial trading systems, demonstrating how such a system can be built using Java. - -== Table of Contents - -* <> -* <> -* <> -* <> -* <> -* <> - -== Getting Started - -To get started with the Order Processor Demo, you need to clone the repository to your local machine. Once you have cloned the repository, you can open it in your IDE of choice (we recommend IntelliJ CE for this project). - -== Prerequisites - -Before you begin, ensure you have the following installed: - -- **Java Development Kit (JDK)**: Java 8 Update 202+, Java 11, Java 17, or Java 21 -- **Apache Maven**: Version 3.6.5 or higher -- **Integrated Development Environment (IDE)**: IntelliJ IDEA Community Edition (recommended) or any other IDE -- **Internet Access**: Required for Maven to download necessary dependencies - -== Installation - -Here are the steps to clone the repository and open it in IntelliJ: - -1. Clone the repository with `git clone ` -2. Open IntelliJ IDE and select 'Open' -3. Navigate to the cloned repository and click 'Open' -4. Wait for the IDE to import the project and download the necessary dependencies - -== Running the Project - -[source,mermaid] -.... -sequenceDiagram -autonumber -Client->>+OMS: NewOrderSingle -Note right of Client: via Chronicle Queue, persisted -Note right of OMS: processes event -OMS->>-Client: ExecutionReport -Note right of Client: via Chronicle Queue, persisted -.... - -To run the project, you need to compile the project and run the main classes for different features: - -- `OrderAdderMain`: This class simulates the creation of new orders. -- `OrderViewerMain`: This class simulates the viewing of created orders. -- `OrderBenchmarkMain`: This class benchmarks round trip time of writing a `NewOrderSingle` a queue, have the `OMSImpl` produce an `ExecutionReport` and read that report. -- `OMSImpl`: This class handles the processing of the orders, including cancelling orders. - -You can run these classes directly from your IDE, or you can use `mvn compile exec:exec@id` on the command line to run the compiled classes. - -== Contributing - -We appreciate any contributions to the Order Processor Demo. If you find any bugs or issues, please open an issue in the repository. If you want to contribute to the code, please fork the repository, make your changes, and open a pull request. - -== Acknowledgments - -- Inspired by real-world financial trading systems. -- Uses the FIX protocol for message exchange. -.... - -Lines 46, Blanks 20, Tokens 663 - -== File: src\main\java\town\lost\oms\api\ErrorListener.java -.... -package town.lost.oms.api; - -/** - * This interface provides a method to handle JVM errors. - * Implementations of this interface will define how these errors are handled. - */ -public interface ErrorListener { - - /** - * This method handles JVM errors. - * It takes a String message which provides details about the error. - * - * @param msg a String providing details about the JVM error. - */ - void jvmError(String msg); -} -.... - -Lines 14, Blanks 2, Tokens 93 - -== File: src\main\java\town\lost\oms\api\OMSIn.java -.... -/* - */ - -package town.lost.oms.api; - -import net.openhft.chronicle.bytes.MethodId; -import town.lost.oms.dto.CancelAll; -import town.lost.oms.dto.CancelOrderRequest; -import town.lost.oms.dto.NewOrderSingle; - -/** - * The {@code OMSIn} interface defines the operations that can be performed by an Order Management System (OMS). - *

- * It includes methods to handle new single orders, cancel order requests, and cancel all orders. - * Each method receives an instance of a data transfer object that represents the details of the operation. - * - * @see NewOrderSingle - * @see CancelOrderRequest - * @see CancelAll - */ -public interface OMSIn { - - /** - * Handles a new single order submission. - * - * @param nos The {@link NewOrderSingle} object representing the details of the new order. - */ - @MethodId(1) - void newOrderSingle(NewOrderSingle nos); - - /** - * Handles a cancel order request. - * - * @param cor The {@link CancelOrderRequest} object representing the details of the cancel order request. - */ - @MethodId(2) - void cancelOrderRequest(CancelOrderRequest cor); - - /** - * Handles a cancel all orders request. - * - * @param cancelAll The {@link CancelAll} object representing the details of the cancel all orders request. - */ - void cancelAll(CancelAll cancelAll); -} -.... - -Lines 39, Blanks 6, Tokens 309 - -== File: src\main\java\town\lost\oms\api\OMSOut.java -.... -/* - */ - -package town.lost.oms.api; - -import net.openhft.chronicle.bytes.MethodId; -import town.lost.oms.dto.ExecutionReport; -import town.lost.oms.dto.OrderCancelReject; - -/** - * The {@code OMSOut} interface defines the output operations that an Order Management System (OMS) can perform. - *

- * It includes methods to handle execution reports and order cancel rejections. - *

It extends the {@link ErrorListener} interface to handle any JVM errors that may occur during processing. - * Each method receives an instance of a data transfer object that represents the details of the operation. - * - * @see ExecutionReport - * @see OrderCancelReject - * @see ErrorListener - */ -public interface OMSOut extends ErrorListener{ - - /** - * Handles an execution report. - * - * @param er The {@link ExecutionReport} object representing the details of the execution report. - */ - @MethodId(11) - void executionReport(ExecutionReport er); - - /** - * Handles an order cancel reject. - * - * @param ocr The {@link OrderCancelReject} object representing the details of the order cancel reject. - */ - @MethodId(12) - void orderCancelReject(OrderCancelReject ocr); -} -.... - -Lines 33, Blanks 5, Tokens 267 - -== File: src\main\java\town\lost\oms\api\package-info.java -.... -/** - * Provides the API interfaces for interactions with the Order Management System (OMS) in the 'town.lost.oms' project. - * - *

This package includes the following interfaces: - * - *

    - *
  • {@link town.lost.oms.api.OMSIn}, which defines the methods for inbound operations that an OMS can perform, such as handling new single orders, cancel order requests, and cancel all orders.
  • - *
  • {@link town.lost.oms.api.OMSOut}, which defines the methods for outbound operations from the OMS, including handling execution reports and order cancel rejections.
  • - *
- * - *

Each interface includes methods that correspond to specific actions within the OMS, facilitating communication between clients and the OMS. - * - *

For more details, refer to the documentation for each individual interface. - */ -package town.lost.oms.api; - -.... - -Lines 15, Blanks 1, Tokens 187 - -== File: src\main\java\town\lost\oms\dto\AbstractEvent.java -.... -/* - */ - -package town.lost.oms.dto; - -import net.openhft.chronicle.bytes.BytesIn; -import net.openhft.chronicle.bytes.BytesOut; -import net.openhft.chronicle.core.Jvm; -import net.openhft.chronicle.core.io.InvalidMarshallableException; -import net.openhft.chronicle.core.io.Validatable; -import net.openhft.chronicle.wire.*; -import net.openhft.chronicle.wire.converter.NanoTime; -import net.openhft.chronicle.wire.converter.ShortText; - -/** - * The {@code AbstractEvent} class represents a generic event in the system. - * - *

This class provides common fields such as sender, target, and sendingTime which can be extended - * by more specific events. Fields sender and target are encoded as {@link ShortText} for efficient storage and transmission. - * - *

The class is generic, with the type parameter being a subclass of {@code AbstractEvent}. This allows - * methods in this class to return an instance of the subclass, enabling method chaining in the subclass. - * - *

The encoding of the marshalled bytes can be controlled via system properties "bytesInBinary" and "pregeneratedMarshallable". - */ -public class AbstractEvent> extends SelfDescribingMarshallable implements Validatable { - // Used to control the benchmark - public static final boolean BYTES_IN_BINARY = Jvm.getBoolean("bytesInBinary", true); - - // Used to control the benchmark - public static final boolean PREGENERATED_MARSHALLABLE = Jvm.getBoolean("pregeneratedMarshallable", true); - private static final int MARSHALLABLE_VERSION = 1; - - // The sender of the event - @ShortText - private long sender; - - // The target of the event - @ShortText - private long target; - - // The sending time of the event, in nanoseconds. - @NanoTime - private long sendingTime; - - @Override - public boolean usesSelfDescribingMessage() { - return !BYTES_IN_BINARY; - } - - /** - * Get the sender of the event. - * - * @return The sender's value as a long. - */ - public long sender() { - return sender; - } - - /** - * Set the sender of the event. - * - * @param sender The sender's value to set, as a long. - * @return This AbstractEvent instance, to facilitate method chaining. - */ - @SuppressWarnings("unchecked") - public E sender(long sender) { - this.sender = sender; - return (E) this; - } - - /** - * Get the target of the event. - * - * @return The target's value as a long. - */ - public long target() { - return target; - } - - /** - * Set the target of the event. - * - * @param target The target's value to set, as a long. - * @return This AbstractEvent instance, to facilitate method chaining. - */ - @SuppressWarnings("unchecked") - public E target(long target) { - this.target = target; - return (E) this; - } - - /** - * Get the sending time of the event. - * - * @return The sending time as a long. - */ - public long sendingTime() { - return sendingTime; - } - - /** - * Set the sending time of the event. - * - * @param sendingTime The sending time to set, as a long. - * @return This AbstractEvent instance, to facilitate method chaining. - */ - @SuppressWarnings("unchecked") - public E sendingTime(long sendingTime) { - this.sendingTime = sendingTime; - return (E) this; - } - - @Override - public void writeMarshallable(BytesOut out) { - if (PREGENERATED_MARSHALLABLE) { - out.writeStopBit(MARSHALLABLE_VERSION); - out.writeLong(sender); - out.writeLong(target); - out.writeLong(sendingTime); - } else { - super.writeMarshallable(out); - } - } - - @Override - public void readMarshallable(BytesIn in) { - if (PREGENERATED_MARSHALLABLE) { - int version = (int) in.readStopBit(); - if (version == MARSHALLABLE_VERSION) { - sender = in.readLong(); - target = in.readLong(); - sendingTime = in.readLong(); - } else { - throw new IllegalStateException("Unknown version " + version); - } - } else { - super.readMarshallable(in); - } - } - - @Override - public void writeMarshallable(WireOut out) { - if (PREGENERATED_MARSHALLABLE) { - out.write("sender").writeLong(ShortTextLongConverter.INSTANCE, sender); - out.write("target").writeLong(ShortTextLongConverter.INSTANCE, target); - out.write("sendingTime").writeLong(MicroTimestampLongConverter.INSTANCE, sendingTime); - } else { - super.writeMarshallable(out); - } - } - - @Override - public void readMarshallable(WireIn in) { - if (PREGENERATED_MARSHALLABLE) { - sender = in.read("sender").readLong(ShortTextLongConverter.INSTANCE); - target = in.read("target").readLong(ShortTextLongConverter.INSTANCE); - sendingTime = in.read("sendingTime").readLong(MicroTimestampLongConverter.INSTANCE); - } else { - super.readMarshallable(in); - } - } - - @Override - public void validate() throws InvalidMarshallableException { - if (sender == 0) - throw new InvalidMarshallableException("sender is required"); - if (target == 0) - throw new InvalidMarshallableException("target is required"); - if (sendingTime == 0) - throw new InvalidMarshallableException("sendingTime is required"); - } -} -.... - -Lines 154, Blanks 19, Tokens 1262 - -== File: src\main\java\town\lost\oms\dto\CancelAll.java -.... -/* - */ - -package town.lost.oms.dto; - -import net.openhft.chronicle.bytes.BytesIn; -import net.openhft.chronicle.bytes.BytesOut; -import net.openhft.chronicle.core.io.InvalidMarshallableException; -import net.openhft.chronicle.wire.ShortTextLongConverter; -import net.openhft.chronicle.wire.WireIn; -import net.openhft.chronicle.wire.WireOut; -import net.openhft.chronicle.wire.converter.ShortText; - -/** - * The {@code CancelAll} class represents an event that is used to cancel all active orders for a particular symbol. - * - *

This class extends the {@link AbstractEvent} class with the type parameter {@link CancelAll}, indicating that - * it is processed as a {@code CancelAll} event. - * - *

This class is typically used like this: - * - *

- * public void cancelAll(CancelAll cancelAll) {
- *     // Iterate over active orders and cancel those matching the symbol
- *     for (Order order : activeOrders) {
- *         if (order.symbol() == cancelAll.symbol()) {
- *             CancelOrderRequest request = new CancelOrderRequest()
- *                 .sender(cancelAll.target())
- *                 .target(cancelAll.sender())
- *                 .symbol(order.symbol())
- *                 .clOrdID(order.clOrdID())
- *                 .sendingTime(cancelAll.sendingTime())
- *                 .origClOrdID(order.origClOrdID())
- *                 .side(order.side());
- *             out.cancelOrderRequest(request);
- *         }
- *     }
- * }
- * }
- * - *

Note that the {@code symbol} field is encoded using {@link ShortTextLongConverter} to minimize storage and transmission size. - */ -public class CancelAll extends AbstractEvent { - private static final int MASHALLABLE_VERSION = 1; - // Symbol for which all orders are to be canceled. - @ShortText - private long symbol; - - // Client order ID. - private String clOrdID = ""; - - /** - * Get the symbol for which all orders are to be canceled. - * - * @return The symbol value as a long. - */ - public long symbol() { - return symbol; - } - - /** - * Set the symbol for which all orders are to be canceled. - * - * @param symbol The symbol value to set, as a long. - * @return This CancelAll instance, to facilitate method chaining. - */ - public CancelAll symbol(long symbol) { - this.symbol = symbol; - return this; - } - - /** - * Retrieves the client order ID. - * - * @return The client order ID as a {@code String}. - */ - public String clOrdID() { - return clOrdID; - } - - /** - * Sets the client order ID and returns the {@code CancelAll} object. This method allows for method chaining. - * - * @param clOrdID The client order ID. - * @return The {@code CancelAll} object with the client order ID set. - */ - public CancelAll clOrdID(String clOrdID) { - this.clOrdID = clOrdID; - return this; - } - - @Override - public void writeMarshallable(WireOut out) { - super.writeMarshallable(out); - if (PREGENERATED_MARSHALLABLE) { - out.write("symbol").writeLong(ShortTextLongConverter.INSTANCE, symbol); - out.write("clOrdID").object(String.class, clOrdID); - } - } - - @Override - public void readMarshallable(WireIn in) { - super.readMarshallable(in); - if (PREGENERATED_MARSHALLABLE) { - symbol = in.read("symbol").readLong(ShortTextLongConverter.INSTANCE); - clOrdID = in.read("clOrdID").object(clOrdID, String.class); - } - } - - @Override - public void writeMarshallable(BytesOut out) { - super.writeMarshallable(out); - if (PREGENERATED_MARSHALLABLE) { - out.writeStopBit(MASHALLABLE_VERSION); - out.writeLong(symbol); - out.writeObject(String.class, clOrdID); - } - } - - @Override - public void readMarshallable(BytesIn in) { - super.readMarshallable(in); - if (PREGENERATED_MARSHALLABLE) { - int version = (int) in.readStopBit(); - if (version == MASHALLABLE_VERSION) { - symbol = in.readLong(); - clOrdID = in.readObject(String.class); - } else { - throw new IllegalStateException("Unknown version " + version); - } - } - } - - /** - * Validates the fields of this {@code CancelAll} event. - * - * @throws InvalidMarshallableException if any required field is missing or invalid. - */ - @Override - public void validate() throws InvalidMarshallableException { - super.validate(); - if (symbol == 0) { - throw new InvalidMarshallableException("symbol is required"); - } - if (clOrdID == null || clOrdID.isEmpty()) { - throw new InvalidMarshallableException("clOrdID is required"); - } - } -} -.... - -Lines 136, Blanks 13, Tokens 1121 - -== File: src\main\java\town\lost\oms\dto\CancelOrderRequest.java -.... -/* - */ - -package town.lost.oms.dto; - -import net.openhft.chronicle.bytes.BytesIn; -import net.openhft.chronicle.bytes.BytesOut; -import net.openhft.chronicle.core.io.InvalidMarshallableException; -import net.openhft.chronicle.wire.ShortTextLongConverter; -import net.openhft.chronicle.wire.WireIn; -import net.openhft.chronicle.wire.WireOut; -import net.openhft.chronicle.wire.converter.ShortText; - -/** - * The {@code CancelOrderRequest} class represents a request to cancel an order in a trading system. - * - *

This class extends the {@link AbstractEvent} class, with the type parameter being {@link CancelOrderRequest}. - * This indicates that the event will be processed into a {@link CancelOrderRequest} that represents a request to cancel an order. - * - *

Each {@code CancelOrderRequest} contains the symbol of the financial instrument for the order that is being requested to be cancelled and the client order ID. - * - *

    - *
  • symbol: The symbol of the financial instrument for the order.
  • - *
  • account: The account associated with the order.
  • - *
  • clOrdID: The client order ID, a unique identifier for the order.
  • - *
  • origClOrdID: The original client order ID of the order to be canceled.
  • - *
  • side: The side of the order (e.g., buy or sell).
  • - *
- * - *

The {@code symbol} and {@code account} fields are encoded using {@link ShortTextLongConverter} - * to save space, while the client order IDs are strings for identification purposes. - */ -public class CancelOrderRequest extends AbstractEvent { - private static final int MASHALLABLE_VERSION = 1; - // Symbol of the financial instrument for the order. - @ShortText - private long symbol; - @ShortText - private long account; - - // Client order ID. - private String clOrdID = ""; - - // Original client order ID. - private String origClOrdID = ""; - - // Side of the order (e.g., buy or sell). - private Side side; - - - /** - * Retrieves the symbol of the financial instrument for the order. - * - * @return The symbol as a {@code long}. - */ - public long symbol() { - return symbol; - } - - /** - * Sets the symbol of the financial instrument for the order and returns the {@code CancelOrderRequest} object. This method allows for method chaining. - * - * @param symbol The symbol of the financial instrument for the order. - * @return The {@code CancelOrderRequest} object with the symbol set. - */ - public CancelOrderRequest symbol(long symbol) { - this.symbol = symbol; - return this; - } - - - /** - * Retrieves the account associated with the order. - * - * @return The account as a {@code long}. - */ - public long account() { - return account; - } - - /** - * Sets the account associated with the order. - * - * @param account The account to set. - * @return This {@code CancelOrderRequest} instance for method chaining. - */ - public CancelOrderRequest account(long account) { - this.account = account; - return this; - } - - /** - * Retrieves the client order ID. - * - * @return The client order ID as a {@code String}. - */ - public String clOrdID() { - return clOrdID; - } - - /** - * Sets the client order ID and returns the {@code CancelOrderRequest} object. This method allows for method chaining. - * - * @param clOrdID The client order ID. - * @return The {@code CancelOrderRequest} object with the client order ID set. - */ - public CancelOrderRequest clOrdID(String clOrdID) { - this.clOrdID = clOrdID; - return this; - } - - /** - * Retrieves the original client order ID. - * - * @return The original client order ID as a {@code String}. - */ - public String origClOrdID() { - return origClOrdID; - } - - /** - * Sets the original client order ID. - * - * @param origClOrdID The original client order ID to set. - * @return This {@code CancelOrderRequest} instance for method chaining. - */ - public CancelOrderRequest origClOrdID(String origClOrdID) { - this.origClOrdID = origClOrdID; - return this; - } - - /** - * Retrieves the side of the order. - * - * @return The side of the order as a {@link Side} enum. - */ - public Side side() { - return side; - } - - /** - * Sets the side of the order. - * - * @param side The side to set (e.g., buy or sell). - * @return This {@code CancelOrderRequest} instance for method chaining. - */ - public CancelOrderRequest side(Side side) { - this.side = side; - return this; - } - - @Override - public void writeMarshallable(WireOut out) { - super.writeMarshallable(out); - if (PREGENERATED_MARSHALLABLE) { - out.write("symbol").writeLong(ShortTextLongConverter.INSTANCE, symbol); - out.write("account").writeLong(ShortTextLongConverter.INSTANCE, account); - out.write("clOrdID").object(String.class, clOrdID); - out.write("origClOrdID").object(String.class, origClOrdID); - out.write("side").object(Side.class, side); - } - } - - @Override - public void readMarshallable(WireIn in) { - super.readMarshallable(in); - if (PREGENERATED_MARSHALLABLE) { - symbol = in.read("symbol").readLong(ShortTextLongConverter.INSTANCE); - account = in.read("account").readLong(ShortTextLongConverter.INSTANCE); - clOrdID = in.read("clOrdID").object(clOrdID, String.class); - origClOrdID = in.read("origClOrdID").object(origClOrdID, String.class); - side = in.read("side").object(Side.class); - } - } - - @Override - public void writeMarshallable(BytesOut out) { - super.writeMarshallable(out); - if (PREGENERATED_MARSHALLABLE) { - out.writeStopBit(MASHALLABLE_VERSION); - out.writeLong(symbol); - out.writeLong(account); - out.writeObject(String.class, clOrdID); - out.writeObject(String.class, origClOrdID); - out.writeObject(Side.class, side); - } - } - - @SuppressWarnings("unchecked") - @Override - public void readMarshallable(BytesIn in) { - super.readMarshallable(in); - if (PREGENERATED_MARSHALLABLE) { - int version = (int) in.readStopBit(); - if (version == MASHALLABLE_VERSION) { - symbol = in.readLong(); - account = in.readLong(); - clOrdID = in.readObject(String.class); - origClOrdID = in.readObject(String.class); - side = in.readObject(Side.class); - } else { - throw new IllegalStateException("Unknown version " + version); - } - } - } - - /** - * Validates the fields of this {@code CancelOrderRequest}. - * - * @throws InvalidMarshallableException if any required field is missing or invalid. - */ - @Override - public void validate() throws InvalidMarshallableException { - if (symbol == 0) { - throw new InvalidMarshallableException("symbol is required"); - } - if (account == 0) { - throw new InvalidMarshallableException("account is required"); - } - if (clOrdID == null || clOrdID.isEmpty()) { - throw new InvalidMarshallableException("clOrdID is required"); - } - if (origClOrdID == null || origClOrdID.isEmpty()) { - throw new InvalidMarshallableException("origClOrdID is required"); - } - if (side == null) { - throw new InvalidMarshallableException("side is required"); - } - } -} -.... - -Lines 207, Blanks 23, Tokens 1812 - -== File: src\main\java\town\lost\oms\dto\Ccy.java -.... -package town.lost.oms.dto; - -/** - * The {@code Ccy} enum represents international currency codes as per the ISO 4217 standard. - * - *

Each constant corresponds to a specific currency, identified by its three-letter code. - * Additional information such as the currency name, country, and symbol can be accessed through - * the provided methods. - * - *

Example usage: - *

{@code
- * Ccy currency = Ccy.USD;
- * System.out.println("Currency Code: " + currency.getCode());
- * System.out.println("Currency Name: " + currency.getCurrencyName());
- * System.out.println("Country: " + currency.getCountry());
- * System.out.println("Symbol: " + currency.getSymbol());
- * }
- * - *

This would output: - *

- * Currency Code: USD
- * Currency Name: United States Dollar
- * Country: United States
- * Symbol: $
- * 
- */ -public enum Ccy { - /** - * United States Dollar. - */ - USD("United States Dollar", "United States", "$"), - /** - * Euro. - */ - EUR("Euro", "Eurozone", "€"), - /** - * British Pound Sterling. - */ - GBP("Pound Sterling", "United Kingdom", "£"), - /** - * Japanese Yen. - */ - JPY("Japanese Yen", "Japan", "¥"), - /** - * Swiss Franc. - */ - CHF("Swiss Franc", "Switzerland", "CHF"), - /** - * Canadian Dollar. - */ - CAD("Canadian Dollar", "Canada", "C$"), - /** - * Australian Dollar. - */ - AUD("Australian Dollar", "Australia", "A$"), - /** - * New Zealand Dollar. - */ - NZD("New Zealand Dollar", "New Zealand", "NZ$"), - /** - * Chinese Yuan Renminbi. - */ - CNY("Chinese Yuan", "China", "¥"), - /** - * Swedish Krona. - */ - SEK("Swedish Krona", "Sweden", "kr"), - /** - * Norwegian Krone. - */ - NOK("Norwegian Krone", "Norway", "kr"), - /** - * Russian Ruble. - */ - RUB("Russian Ruble", "Russia", "₽"), - /** - * South African Rand. - */ - ZAR("South African Rand", "South Africa", "R"), - /** - * Singapore Dollar. - */ - SGD("Singapore Dollar", "Singapore", "S$"), - /** - * Hong Kong Dollar. - */ - HKD("Hong Kong Dollar", "Hong Kong", "HK$"); - - // Fields - private final String currencyName; - private final String country; - private final String symbol; - - /** - * Constructs a {@code Ccy} enum constant with the specified currency name, country, and symbol. - * - * @param currencyName the full name of the currency - * @param country the country or region where the currency is used - * @param symbol the symbol representing the currency - */ - Ccy(String currencyName, String country, String symbol) { - this.currencyName = currencyName; - this.country = country; - this.symbol = symbol; - } - - /** - * Gets the full name of the currency. - * - * @return the currency name - */ - public String getCurrencyName() { - return currencyName; - } - - /** - * Gets the country or region where the currency is used. - * - * @return the country or region - */ - public String getCountry() { - return country; - } - - /** - * Gets the symbol representing the currency. - * - * @return the currency symbol - */ - public String getSymbol() { - return symbol; - } - - /** - * Gets the three-letter currency code as per the ISO 4217 standard. - * - * @return the currency code - */ - public String getCode() { - return this.name(); - } -} - -.... - -Lines 135, Blanks 8, Tokens 853 - -== File: src\main\java\town\lost\oms\dto\ExecutionReport.java -.... -/* - */ - -package town.lost.oms.dto; - -import net.openhft.chronicle.bytes.BytesIn; -import net.openhft.chronicle.bytes.BytesOut; -import net.openhft.chronicle.core.io.InvalidMarshallableException; -import net.openhft.chronicle.wire.ShortTextLongConverter; -import net.openhft.chronicle.wire.NanoTimestampLongConverter; -import net.openhft.chronicle.wire.WireIn; -import net.openhft.chronicle.wire.WireOut; -import net.openhft.chronicle.wire.converter.NanoTime; -import net.openhft.chronicle.wire.converter.ShortText; - -import static town.lost.oms.dto.ValidateUtil.*; - -/** - * The {@code ExecutionReport} class represents the execution report of an order in a trading system. - * - *

This class extends the {@link AbstractEvent} class, with the type parameter being {@link ExecutionReport}. - * This indicates that the event will be processed into an {@link ExecutionReport} that represents the execution report of an order. - * - *

Each {@code ExecutionReport} contains various pieces of information about the order execution, including: - * - *

    - *
  • symbol: The identifier of the financial instrument.
  • - *
  • transactTime: The transaction time in nanoseconds.
  • - *
  • orderQty: The quantity of the order.
  • - *
  • price: The price of the order.
  • - *
  • orderID: The unique identifier of the order.
  • - *
  • lastPx: The last traded price of the order.
  • - *
  • leavesQty: The remaining quantity of the order.
  • - *
  • cumQty: The accumulated quantity of the order.
  • - *
  • avgPx: The average price of the order.
  • - *
  • side: The side of the order (buy or sell).
  • - *
  • ordType: The type of the order (e.g., market or limit).
  • - *
  • clOrdID: The client order ID.
  • - *
  • text: An optional text message about the order execution.
  • - *
- * - *

The {@code symbol} field is encoded using {@link ShortTextLongConverter}, and {@code transactTime} and {@code orderID} - * use {@link NanoTimestampLongConverter} to save space. - */ -public class ExecutionReport extends AbstractEvent { - private static final int MASHALLABLE_VERSION = 1; - // Symbol of the financial instrument. - @ShortText - private long symbol; - - // Transaction time in nanoseconds. - @NanoTime - private long transactTime; - - // Quantity of the order. - private double orderQty; - - // Price of the order. - private double price; - - // Order ID in nanoseconds. - @NanoTime - private long orderID; - - // Last traded price of the order. - private double lastPx; - - // Remaining quantity of the order. - private double leavesQty; - - // Accumulated quantity of the order. - private double cumQty; - - // Average price of the order. - private double avgPx; - - // Side of the order (buy or sell). - private Side side; - - // Type of the order (market or limit). - private OrderType ordType; - - // Client order ID. - private String clOrdID = ""; - - // Optional text message about the order execution. - private String text = null; - - /** - * Retrieves the client order ID. - * - * @return the client order ID as a {@code String} - */ - public String clOrdID() { - return clOrdID; - } - - /** - * Sets the client order ID. - * - * @param clOrdID the client order ID to set - * @return this {@code ExecutionReport} instance for method chaining - */ - public ExecutionReport clOrdID(String clOrdID) { - this.clOrdID = clOrdID; - return this; - } - - /** - * Retrieves the symbol of the financial instrument. - * - * @return the symbol as a {@code long} - */ - public long symbol() { - return symbol; - } - - /** - * Sets the symbol of the financial instrument. - * - * @param symbol the symbol to set - * @return this {@code ExecutionReport} instance for method chaining - */ - public ExecutionReport symbol(long symbol) { - this.symbol = symbol; - return this; - } - - /** - * Retrieves the side of the order (buy or sell). - * - * @return the side as a {@link Side} enum value - */ - public Side side() { - return side; - } - - /** - * Sets the side of the order (buy or sell). - * - * @param side the side to set - * @return this {@code ExecutionReport} instance for method chaining - */ - public ExecutionReport side(Side side) { - this.side = side; - return this; - } - - /** - * Retrieves the transaction time in nanoseconds. - * - * @return the transaction time as a {@code long} - */ - public long transactTime() { - return transactTime; - } - - /** - * Sets the transaction time in nanoseconds. - * - * @param transactTime the transaction time to set - * @return this {@code ExecutionReport} instance for method chaining - */ - public ExecutionReport transactTime(long transactTime) { - this.transactTime = transactTime; - return this; - } - - /** - * Retrieves the quantity of the order. - * - * @return the order quantity as a {@code double} - */ - public double orderQty() { - return orderQty; - } - - /** - * Sets the quantity of the order. - * - * @param orderQty the order quantity to set - * @return this {@code ExecutionReport} instance for method chaining - */ - public ExecutionReport orderQty(double orderQty) { - this.orderQty = orderQty; - return this; - } - - /** - * Retrieves the price of the order. - * - * @return the price as a {@code double} - */ - public double price() { - return price; - } - - /** - * Sets the price of the order. - * - * @param price the price to set - * @return this {@code ExecutionReport} instance for method chaining - */ - public ExecutionReport price(double price) { - this.price = price; - return this; - } - - /** - * Retrieves the order ID. - * - * @return the order ID as a {@code long} - */ - public long orderID() { - return orderID; - } - - /** - * Sets the order ID. - * - * @param orderID the order ID to set - * @return this {@code ExecutionReport} instance for method chaining - */ - public ExecutionReport orderID(long orderID) { - this.orderID = orderID; - return this; - } - /** - * Returns the order type. - * - * @return the order type - */ - public OrderType ordType() { - return ordType; - } - /** - * Sets the order type and returns this ExecutionReport instance. - * - * @param ordType the order type to set - * @return this ExecutionReport instance - */ - public ExecutionReport ordType(OrderType ordType) { - this.ordType = ordType; - return this; - } - - /** - * Retrieves the last traded price of the order. - * - * @return the last traded price as a {@code double} - */ - public double lastPx() { - return lastPx; - } - - /** - * Sets the last traded price of the order. - * - * @param lastPx the last price to set - * @return this {@code ExecutionReport} instance for method chaining - */ - public ExecutionReport lastPx(double lastPx) { - this.lastPx = lastPx; - return this; - } - - /** - * Retrieves the remaining quantity of the order. - * - * @return the leaves quantity as a {@code double} - */ - public double leavesQty() { - return leavesQty; - } - - /** - * Sets the remaining quantity of the order. - * - * @param leavesQty the leaves quantity to set - * @return this {@code ExecutionReport} instance for method chaining - */ - public ExecutionReport leavesQty(double leavesQty) { - this.leavesQty = leavesQty; - return this; - } - - /** - * Retrieves the accumulated quantity of the order. - * - * @return the cumulative quantity as a {@code double} - */ - public double cumQty() { - return cumQty; - } - - /** - * Sets the accumulated quantity of the order. - * - * @param cumQty the cumulative quantity to set - * @return this {@code ExecutionReport} instance for method chaining - */ - public ExecutionReport cumQty(double cumQty) { - this.cumQty = cumQty; - return this; - } - - /** - * Retrieves the average price of the order. - * - * @return the average price as a {@code double} - */ - public double avgPx() { - return avgPx; - } - - /** - * Sets the average price and returns this ExecutionReport instance. - * - * @param avgPx the average price to set - * @return this ExecutionReport instance - */ - public ExecutionReport avgPx(double avgPx) { - this.avgPx = avgPx; - return this; - } - - /** - * Retrieves the optional text message about the order execution. - * - * @return the text message as a {@code String} - */ - public String text() { - return text; - } - - /** - * Sets the text of the report and returns this ExecutionReport instance. - * - * @param text the text message to set - * @return this {@code ExecutionReport} instance for method chaining - */ - public ExecutionReport text(String text) { - this.text = text; - return this; - } - - @Override - public void writeMarshallable(BytesOut out) { - super.writeMarshallable(out); - if (PREGENERATED_MARSHALLABLE) { - out.writeStopBit(MASHALLABLE_VERSION); - out.writeLong(symbol); - out.writeLong(transactTime); - out.writeDouble(orderQty); - out.writeDouble(price); - out.writeLong(orderID); - out.writeDouble(lastPx); - out.writeDouble(leavesQty); - out.writeDouble(cumQty); - out.writeDouble(avgPx); - out.writeObject(Side.class, side); - out.writeObject(OrderType.class, ordType); - out.writeObject(String.class, clOrdID); - out.writeObject(String.class, text); - } - } - - @Override - public void readMarshallable(BytesIn in) { - super.readMarshallable(in); - if (PREGENERATED_MARSHALLABLE) { - int version = (int) in.readStopBit(); - if (version == MASHALLABLE_VERSION) { - symbol = in.readLong(); - transactTime = in.readLong(); - orderQty = in.readDouble(); - price = in.readDouble(); - orderID = in.readLong(); - lastPx = in.readDouble(); - leavesQty = in.readDouble(); - cumQty = in.readDouble(); - avgPx = in.readDouble(); - side = in.readObject(Side.class); - ordType = in.readObject(OrderType.class); - clOrdID = in.readObject(String.class); - text = in.readObject(String.class); - } else { - throw new IllegalStateException("Unknown version " + version); - } - } - } - - @Override - public void writeMarshallable(WireOut out) { - super.writeMarshallable(out); - if (PREGENERATED_MARSHALLABLE) { - out.write("symbol").writeLong(ShortTextLongConverter.INSTANCE, symbol); - out.write("transactTime").writeLong(NanoTimestampLongConverter.INSTANCE, transactTime); - out.write("orderQty").writeDouble(orderQty); - out.write("price").writeDouble(price); - out.write("orderID").writeLong(NanoTimestampLongConverter.INSTANCE, orderID); - out.write("lastPx").writeDouble(lastPx); - out.write("leavesQty").writeDouble(leavesQty); - out.write("cumQty").writeDouble(cumQty); - out.write("avgPx").writeDouble(avgPx); - out.write("side").object(Side.class, side); - out.write("ordType").object(OrderType.class, ordType); - out.write("clOrdID").object(String.class, clOrdID); - out.write("text").object(String.class, text); - } - } - - @Override - public void readMarshallable(WireIn in) { - super.readMarshallable(in); - if (PREGENERATED_MARSHALLABLE) { - symbol = in.read("symbol").readLong(ShortTextLongConverter.INSTANCE); - transactTime = in.read("transactTime").readLong(NanoTimestampLongConverter.INSTANCE); - orderQty = in.read("orderQty").readDouble(); - price = in.read("price").readDouble(); - orderID = in.read("orderID").readLong(NanoTimestampLongConverter.INSTANCE); - lastPx = in.read("lastPx").readDouble(); - leavesQty = in.read("leavesQty").readDouble(); - cumQty = in.read("cumQty").readDouble(); - avgPx = in.read("avgPx").readDouble(); - side = in.read("side").object(side, Side.class); - ordType = in.read("ordType").object(ordType, OrderType.class); - clOrdID = in.read("clOrdID").object(clOrdID, String.class); - text = in.read("text").object(text, String.class); - } - } - - /** - * Validates the fields of this {@code ExecutionReport} event. - * - * @throws InvalidMarshallableException if any required field is missing or invalid - */ - @Override - public void validate() throws InvalidMarshallableException { - super.validate(); - if (symbol == 0) - throw new InvalidMarshallableException("symbol is required"); - if (transactTime == 0) - throw new InvalidMarshallableException("transactTime is required"); - if (invalidQuantity(orderQty)) - throw new InvalidMarshallableException("orderQty is invalid"); - if (invalidPrice(price)) - throw new InvalidMarshallableException("price is invalid"); - if (orderID == 0) - throw new InvalidMarshallableException("orderID is required"); - if (lastPx != 0 && invalidPrice(lastPx)) - throw new InvalidMarshallableException("lastPx is invalid"); - if (invalidQuantity(leavesQty)) - throw new InvalidMarshallableException("leavesQty is invalid"); - if (invalidQuantity(cumQty)) - throw new InvalidMarshallableException("cumQty is invalid"); - if (avgPx != 0 && invalidPrice(avgPx)) - throw new InvalidMarshallableException("avgPx is invalid"); - if (side == null) - throw new InvalidMarshallableException("side is required"); - if (ordType == null) - throw new InvalidMarshallableException("ordType is required"); - if (clOrdID == null || clOrdID.isEmpty()) - throw new InvalidMarshallableException("clOrdID is required"); - } -} -.... - -Lines 421, Blanks 45, Tokens 3417 - -== File: src\main\java\town\lost\oms\dto\NewOrderSingle.java -.... -/* - */ - -package town.lost.oms.dto; - -import net.openhft.chronicle.bytes.BytesIn; -import net.openhft.chronicle.bytes.BytesOut; -import net.openhft.chronicle.core.io.InvalidMarshallableException; -import net.openhft.chronicle.wire.*; -import net.openhft.chronicle.wire.converter.NanoTime; -import net.openhft.chronicle.wire.converter.ShortText; - -import static town.lost.oms.dto.ValidateUtil.invalidPrice; -import static town.lost.oms.dto.ValidateUtil.invalidQuantity; - -/** - * The {@code NewOrderSingle} class represents a new single order in a trading system. - * - *

This class extends the {@link AbstractEvent} class, with the type parameter being {@link NewOrderSingle}. - * This indicates that the event will be processed into a {@link NewOrderSingle} that will be sent to the order - * system. - * - *

Each {@code NewOrderSingle} contains various pieces of information about the order, including: - * - *

    - *
  • symbol: The identifier of the financial instrument.
  • - *
  • transactTime: The transaction time in nanoseconds.
  • - *
  • account: The account associated with the order.
  • - *
  • orderQty: The quantity of the order.
  • - *
  • price: The price of the order.
  • - *
  • side: The side of the order (buy or sell).
  • - *
  • clOrdID: The client order ID.
  • - *
  • ordType: The type of the order (e.g., market or limit).
  • - *
  • timeInForce: The time-in-force instruction for the order.
  • - *
  • currency: The currency of the order.
  • - *
- * - *

The symbol is encoded using {@link ShortText} and the transaction time is in nanoseconds, both to save space. - * The client order ID is a string that identifies the order, the side indicates whether the order is to buy or sell, - * and the order type indicates whether the order is a market order or a limit order. - */ -public class NewOrderSingle extends AbstractEvent { - private static final int MASHALLABLE_VERSION = 1; - // Symbol of the financial instrument. - @ShortText - private long symbol; - // Transaction time in nanoseconds. - @NanoTime - private long transactTime; - @ShortText - private long account; - - // Quantity of the order. - private double orderQty; - - // Price of the order. - private double price; - - // Side of the order (buy or sell). - private Side side; - - // Client order ID. - private String clOrdID = ""; - - // Type of the order (market or limit). - private OrderType ordType; - - // Time-in-force instruction for the order. - private TimeInForce timeInForce; - - // Currency of the order. - private Ccy currency; - - /** - * Get the client order ID. - * - * @return The client order ID as a string. - */ - public String clOrdID() { - return clOrdID; - } - - /** - * Sets the client order ID. - * - * @param clOrdID The client order ID to set, as a string. - * @return This {@code NewOrderSingle} instance, to facilitate method chaining. - */ - public NewOrderSingle clOrdID(String clOrdID) { - this.clOrdID = clOrdID; - return this; - } - - /** - * Get the symbol of the financial instrument. - * - * @return The symbol as a long. - */ - public long symbol() { - return symbol; - } - - /** - * Sets the symbol of the financial instrument. - * - * @param symbol The symbol to set, as a long. - * @return This {@code NewOrderSingle} instance, to facilitate method chaining. - */ - public NewOrderSingle symbol(long symbol) { - this.symbol = symbol; - return this; - } - - /** - * Get the side of the order (buy or sell). - * - * @return The side as a {@link Side} enum value. - */ - public Side side() { - return side; - } - - /** - * Sets the side of the order (buy or sell). - * - * @param side The side to set, as a {@link Side} enum value. - * @return This {@code NewOrderSingle} instance, to facilitate method chaining. - */ - public NewOrderSingle side(Side side) { - this.side = side; - return this; - } - - /** - * Get the transaction time in nanoseconds. - * - * @return The transaction time as a long. - */ - public long transactTime() { - return transactTime; - } - - /** - * Sets the transaction time in nanoseconds. - * - * @param transactTime The transaction time to set, as a long. - * @return This {@code NewOrderSingle} instance, to facilitate method chaining. - */ - public NewOrderSingle transactTime(long transactTime) { - this.transactTime = transactTime; - return this; - } - - /** - * Get the quantity of the order. - * - * @return The order quantity as a double. - */ - public double orderQty() { - return orderQty; - } - - /** - * Sets the quantity of the order. - * - * @param orderQty The order quantity to set, as a double. - * @return This {@code NewOrderSingle} instance, to facilitate method chaining. - */ - public NewOrderSingle orderQty(double orderQty) { - this.orderQty = orderQty; - return this; - } - - /** - * Get the price of the order. - * - * @return The price as a double. - */ - public double price() { - return price; - } - - /** - * Sets the price of the order. - * - * @param price The price to set, as a double. - * @return This {@code NewOrderSingle} instance, to facilitate method chaining. - */ - public NewOrderSingle price(double price) { - this.price = price; - return this; - } - - /** - * Get the type of the order (market or limit). - * - * @return The order type as an {@link OrderType} enum value. - */ - public OrderType ordType() { - return ordType; - } - - /** - * Sets the type of the order (market or limit). - * - * @param ordType The order type to set, as an {@link OrderType} enum value. - * @return This {@code NewOrderSingle} instance, to facilitate method chaining. - */ - public NewOrderSingle ordType(OrderType ordType) { - this.ordType = ordType; - return this; - } - - /** - * Retrieves the time-in-force instruction for the order. - * - * @return The time-in-force as a {@link TimeInForce} enum value. - */ - public TimeInForce timeInForce() { - return timeInForce; - } - - /** - * Sets the time-in-force instruction for the order. - * - * @param timeInForce The time-in-force to set. - * @return This {@code NewOrderSingle} instance for method chaining. - */ - public NewOrderSingle timeInForce(TimeInForce timeInForce) { - this.timeInForce = timeInForce; - return this; - } - - /** - * Retrieves the account associated with the order. - * - * @return The account as a {@code long}. - */ - public long account() { - return account; - } - - /** - * Sets the account associated with the order. - * - * @param account The account to set. - * @return This {@code NewOrderSingle} instance for method chaining. - */ - public NewOrderSingle account(long account) { - this.account = account; - return this; - } - - /** - * Retrieves the currency of the order. - * - * @return The currency as a {@link Ccy} enum value. - */ - public Ccy currency() { - return currency; - } - - /** - * Sets the currency of the order. - * - * @param currency The currency to set. - * @return This {@code NewOrderSingle} instance for method chaining. - */ - public NewOrderSingle currency(Ccy currency) { - this.currency = currency; - return this; - } - - @Override - public void writeMarshallable(BytesOut out) { - super.writeMarshallable(out); - if (PREGENERATED_MARSHALLABLE) { - out.writeStopBit(MASHALLABLE_VERSION); - out.writeLong(symbol); - out.writeLong(transactTime); - out.writeLong(account); - out.writeDouble(orderQty); - out.writeDouble(price); - out.writeObject(Side.class, side); - out.writeObject(OrderType.class, ordType); - out.writeObject(String.class, clOrdID); - out.writeObject(TimeInForce.class, timeInForce); - out.writeObject(Ccy.class, currency); - } - } - - @Override - public void readMarshallable(BytesIn in) { - super.readMarshallable(in); - if (PREGENERATED_MARSHALLABLE) { - int version = (int) in.readStopBit(); - if (version == MASHALLABLE_VERSION) { - symbol = in.readLong(); - transactTime = in.readLong(); - account = in.readLong(); - orderQty = in.readDouble(); - price = in.readDouble(); - side = in.readObject(Side.class); - ordType = in.readObject(OrderType.class); - clOrdID = in.readObject(String.class); - timeInForce = in.readObject(TimeInForce.class); - currency = in.readObject(Ccy.class); - } - } - } - - @Override - public void writeMarshallable(WireOut out) { - super.writeMarshallable(out); - if (PREGENERATED_MARSHALLABLE) { - out.write("symbol").writeLong(ShortTextLongConverter.INSTANCE, symbol); - out.write("transactTime").writeLong(NanoTimestampLongConverter.INSTANCE, transactTime); - out.write("account").writeLong(ShortTextLongConverter.INSTANCE, account); - out.write("orderQty").writeDouble(orderQty); - out.write("price").writeDouble(price); - out.write("side").object(Side.class, side); - out.write("ordType").object(OrderType.class, ordType); - out.write("clOrdID").object(String.class, clOrdID); - out.write("timeInForce").object(TimeInForce.class, timeInForce); - out.write("currency").object(Ccy.class, currency); - } - } - - @Override - public void readMarshallable(WireIn in) { - super.readMarshallable(in); - if (PREGENERATED_MARSHALLABLE) { - symbol = in.read("symbol").readLong(ShortTextLongConverter.INSTANCE); - transactTime = in.read("transactTime").readLong(NanoTimestampLongConverter.INSTANCE); - account = in.read("account").readLong(ShortTextLongConverter.INSTANCE); - orderQty = in.read("orderQty").readDouble(); - price = in.read("price").readDouble(); - side = in.read("side").object(side, Side.class); - ordType = in.read("ordType").object(OrderType.class); - clOrdID = in.read("clOrdID").object(clOrdID, String.class); - timeInForce = in.read("timeInForce").object(TimeInForce.class); - currency = in.read("currency").object(Ccy.class); - } - } - - /** - * Validates the fields of this {@code NewOrderSingle} event. - * - * @throws InvalidMarshallableException if any required field is missing or invalid. - */ - @Override - public void validate() throws InvalidMarshallableException { - super.validate(); - if (symbol == 0) - throw new InvalidMarshallableException("symbol is required"); - if (transactTime == 0) - throw new InvalidMarshallableException("transactTime is required"); - if (account == 0) - throw new InvalidMarshallableException("account is required"); - if (orderQty == 0) - throw new InvalidMarshallableException("orderQty is required"); - if (invalidQuantity(orderQty)) - throw new InvalidMarshallableException("orderQty is invalid"); - if (invalidPrice(price)) - throw new InvalidMarshallableException("price is invalid"); - if (side == null) - throw new InvalidMarshallableException("side is required"); - if (ordType == null) - throw new InvalidMarshallableException("ordType is required"); - if (clOrdID == null || clOrdID.isEmpty()) - throw new InvalidMarshallableException("clOrdID is required"); - if (timeInForce == null) - throw new InvalidMarshallableException("timeInForce is required"); - if (currency == null) - throw new InvalidMarshallableException("currency is required"); - } -} -.... - -Lines 341, Blanks 36, Tokens 2886 - -== File: src\main\java\town\lost\oms\dto\OrderCancelReject.java -.... -/* - */ - -package town.lost.oms.dto; - -import net.openhft.chronicle.bytes.BytesIn; -import net.openhft.chronicle.bytes.BytesOut; -import net.openhft.chronicle.core.io.InvalidMarshallableException; -import net.openhft.chronicle.wire.ShortTextLongConverter; -import net.openhft.chronicle.wire.WireIn; -import net.openhft.chronicle.wire.WireOut; -import net.openhft.chronicle.wire.converter.ShortText; - -/** - * The {@code OrderCancelReject} class represents an event that is used to indicate that a request to cancel an order was rejected. - * - *

This class extends the {@link AbstractEvent} class, with the type parameter being {@link OrderCancelReject}. - * This indicates that the event will be processed into a {@link OrderCancelReject} that will be sent to the order - * system. - * - *

Each {@code OrderCancelReject} event contains: - *

    - *
  • symbol: The identifier for the financial instrument involved in the order.
  • - *
  • clOrdID: The client order ID of the order that was requested to be canceled.
  • - *
  • reason: A string explaining why the cancellation request was rejected.
  • - *
- * - *

Note that the symbol field is encoded using {@link ShortText} to minimize the space required for storage and transmission. - */ -public class OrderCancelReject extends AbstractEvent { - private static final int MASHALLABLE_VERSION = 1; - // Symbol for which the order cancellation was rejected. - @ShortText - private long symbol; - - // Client order ID of the order that was requested to be canceled. - private String clOrdID = ""; - - // Reason for the rejection of the order cancellation request. - private String reason = ""; - - /** - * Get the client order ID of the order that was requested to be canceled. - * - * @return The client order ID as a string. - */ - public String clOrdID() { - return clOrdID; - } - - /** - * Set the client order ID of the order that was requested to be canceled. - * - * @param clOrdID The client order ID to set, as a string. - * @return This OrderCancelReject instance, to facilitate method chaining. - */ - public OrderCancelReject clOrdID(String clOrdID) { - this.clOrdID = clOrdID; - return this; - } - - /** - * Get the symbol for which the order cancellation was rejected. - * - * @return The symbol value as a {@code long}. - */ - public long symbol() { - return symbol; - } - - /** - * Set the symbol for which the order cancellation was rejected. - * - * @param symbol The symbol value to set, as a long. - * @return This {@code OrderCancelReject} instance, to facilitate method chaining. - */ - public OrderCancelReject symbol(long symbol) { - this.symbol = symbol; - return this; - } - - /** - * Get the reason for the rejection of the order cancellation request. - * - * @return The reason as a string. - */ - public String reason() { - return reason; - } - - /** - * Set the reason for the rejection of the order cancellation request. - * - * @param reason The reason to set, as a string. - * @return This {@code OrderCancelReject} instance, to facilitate method chaining. - */ - public OrderCancelReject reason(String reason) { - this.reason = reason; - return this; - } - - @Override - public void writeMarshallable(WireOut out) { - validate(); - super.writeMarshallable(out); - if (PREGENERATED_MARSHALLABLE) { - out.write("symbol").writeLong(ShortTextLongConverter.INSTANCE, symbol); - out.write("clOrdID").object(String.class, clOrdID); - out.write("reason").object(String.class, reason); - } - } - - @Override - public void readMarshallable(WireIn in) { - super.readMarshallable(in); - if (PREGENERATED_MARSHALLABLE) { - symbol = in.read("symbol").readLong(ShortTextLongConverter.INSTANCE); - clOrdID = in.read("clOrdID").object(clOrdID, String.class); - reason = in.read("reason").object(reason, String.class); - } - } - - @Override - public void writeMarshallable(BytesOut out) { - validate(); - super.writeMarshallable(out); - if (PREGENERATED_MARSHALLABLE) { - out.writeStopBit(MASHALLABLE_VERSION); - out.writeLong(symbol); - out.writeObject(String.class, clOrdID); - out.writeObject(String.class, reason); - } - } - - @Override - public void readMarshallable(BytesIn in) { - super.readMarshallable(in); - if (PREGENERATED_MARSHALLABLE) { - int version = (int) in.readStopBit(); - if (version == MASHALLABLE_VERSION) { - symbol = in.readLong(); - clOrdID = in.readObject(String.class); - reason = in.readObject(String.class); - } else { - throw new IllegalStateException("Unknown version " + version); - } - } - } - - /** - * Validates the fields of this {@code OrderCancelReject} event. - * - * @throws InvalidMarshallableException if any required field is missing or invalid. - */ - @Override - public void validate() throws InvalidMarshallableException { - super.validate(); - if (symbol == 0) - throw new InvalidMarshallableException("symbol is required"); - if (clOrdID == null || clOrdID.isEmpty()) - throw new InvalidMarshallableException("clOrdID is required"); - if (reason == null || reason.isEmpty()) - throw new InvalidMarshallableException("reason is required"); - } -} -.... - -Lines 149, Blanks 16, Tokens 1294 - -== File: src\main\java\town\lost\oms\dto\OrderType.java -.... -/* - */ - -package town.lost.oms.dto; -/** - * Enumeration for the types of orders in a trading system. - * - *

It contains two values: MARKET and LIMIT, which represent the two main types of orders that can be placed - * in a trading system. A MARKET order is an order to buy or sell a security at the current market price, - * whereas a LIMIT order is an order to buy or sell a security at a specific price or better. - * - *

This enumeration can be used when creating trading orders, like so: - * - *

- * NewOrderSingle nos = new NewOrderSingle()
- *    .sender(ShortText.parse("client"))
- *    .target(ShortText.parse("OMS"))
- *    .clOrdID("clOrdId")
- *    .orderQty(1e6)
- *    .price(1.6)
- *    .symbol(ShortText.parse("AUDUSD"))
- *    .ordType(OrderType.LIMIT)
- *    .side(BuySell.BUY);
- * 
- * - *

Note that the order type is indicated by the {@link OrderType} used in the 'ordType' field of the order. - */ -public enum OrderType { - /** - * Market order type, which means the order should be executed at the current market price. - */ - MARKET, - - /** - * Limit order type, which means the order should be executed at a specific price or better. - */ - LIMIT, - - /** - * Pegged order type, where the price is pegged to a benchmark price, such as the best bid or ask. - */ - PEGGED, - - /** - * Fill or Kill order type, which must be executed immediately in its entirety or cancelled. - */ - FILL_OR_KILL, - - /** - * Immediate or Cancel order type, which executes all or part immediately and cancels any unfilled portion. - */ - IMMEDIATE_OR_CANCEL, -} - -.... - -Lines 48, Blanks 6, Tokens 409 - -== File: src\main\java\town\lost\oms\dto\package-info.java -.... -/** - * Provides the classes and interfaces necessary to create and manage Order Management System (OMS) data transfer objects (DTOs). - * - *

This package includes the following key components: - * - *

    - *
  • AbstractEvent and its subclasses, which represent different types of events that can occur in an OMS, such as: - *
      - *
    • {@link town.lost.oms.dto.NewOrderSingle}: Represents a new order submission.
    • - *
    • {@link town.lost.oms.dto.CancelOrderRequest}: Represents a request to cancel an existing order.
    • - *
    • {@link town.lost.oms.dto.ExecutionReport}: Represents the execution status of an order.
    • - *
    • {@link town.lost.oms.dto.OrderCancelReject}: Indicates that an order cancellation request was rejected.
    • - *
    • {@link town.lost.oms.dto.CancelAll}: Represents a request to cancel all orders for a specific symbol.
    • - *
    - *
  • - *
  • Enums that define constants for various order attributes: - *
      - *
    • {@link town.lost.oms.dto.Side}: Represents the side of an order (e.g., buy or sell).
    • - *
    • {@link town.lost.oms.dto.OrderType}: Represents the type of an order (e.g., market, limit).
    • - *
    • {@link town.lost.oms.dto.TimeInForce}: Represents the time-in-force instructions for an order.
    • - *
    • {@link town.lost.oms.dto.Ccy}: Represents currency codes as per ISO 4217.
    • - *
    - *
  • - *
  • Utility Classes: - *
      - *
    • {@link town.lost.oms.dto.ValidateUtil}: Provides utility methods for validating order parameters such as price and quantity.
    • - *
    - *
  • - *
- * - *

Each class is designed to be marshalled and unmarshalled efficiently for high-performance data transfer, leveraging serialization optimizations provided by Chronicle Wire. - * - *

For more details, refer to the documentation of each individual class. - */ -package town.lost.oms.dto; -.... - -Lines 35, Blanks 0, Tokens 493 - -== File: src\main\java\town\lost\oms\dto\Side.java -.... -/* - */ - -package town.lost.oms.dto; - -/** - * Enumeration for the direction of a trade order. - * - *

It contains two values: BUY and SELL, which represents the direction of the order. BUY (+1) means the order is to - * purchase, while SELL (-1) implies the order is to sell. - * - *

This enumeration can be used to create trading orders, like so: - * - *

{@code
- * NewOrderSingle nos = new NewOrderSingle()
- *    .sender(toLong("sender"))
- *    .target(toLong("target"))
- *    .transactTime(now())
- *    .sendingTime(now())
- *    .orderQty(1)
- *    .ordType(OrderType.MARKET)
- *    .side(Side.BUY)
- *    .symbol(toLong("EURUSD"));
- * }
- * - *

Note that the direction is indicated by the {@link Side} used in the 'side' field of the order. - */ -public enum Side { - /** - * Buy order direction, represented by an integer value of +1. - * Indicates an order to purchase. - */ - BUY(+1), - - /** - * Sell order direction, represented by an integer value of -1. - * Indicates an order to sell. - */ - SELL(-1); - - /** - * The direction of the order. - */ - public final int direction; - - /** - * Constructs a BuySell enum with the specified direction. - * - * @param direction the direction of the order (+1 for buy, -1 for sell) - */ - Side(int direction) { - this.direction = direction; - } - - /** - * Gets the direction indicator of the order. - * - * @return the direction indicator as an integer - */ - public int direction() { - return direction; - } - } -.... - -Lines 57, Blanks 6, Tokens 392 - -== File: src\main\java\town\lost\oms\dto\TimeInForce.java -.... -package town.lost.oms.dto; - -/** - * The {@code TimeInForce} enum represents the time conditions under which an order will be executed. - * - *

These conditions specify how long an order remains active in the market before it is executed or expires. - * This enum includes commonly used time-in-force instructions in trading systems, conforming to standard trading practices. - * - *

Example usage: - *

{@code
- * TimeInForce tif = TimeInForce.DAY;
- * System.out.println("Time in Force: " + tif);
- * }
- * - *

This would output: - *

- * Time in Force: DAY
- * 
- */ -public enum TimeInForce { - /** - * Indicates that the order should be valid for the current trading day only. - * If not executed by the end of the trading day, the order expires. - */ - DAY, - - /** - * "Good Till Cancelled" - The order remains active until it is either executed or explicitly cancelled by the trader. - * It may carry over to the next trading day. - */ - GTC, - - /** - * "Immediate or Cancel" - The order attempts to execute all or part immediately. - * Any portion not immediately executed is cancelled. - */ - IOC, - - /** - * "Fill or Kill" - The order must be executed immediately in its entirety, or it is cancelled. - * Partial fills are not allowed. - */ - FOK, - - /** - * "Good Till Date" - The order remains active until a specified date unless it is executed or cancelled before then. - */ - GTD, - - /** - * "Good Till Time" - The order remains active until a specified time on the current trading day. - * If not executed by that time, the order expires. - */ - GTT, - - /** - * "At the Opening" - The order is to be executed at the opening of the market. - * If not executed at the opening, it is cancelled. - */ - OPG, - - /** - * "At the Close" - The order is to be executed at the close of the market. - * If not executed at the close, it is cancelled. - */ - ATC, - - /** - * "Good for Auction" - The order is valid only during the auction period. - */ - GFA, - - /** - * "Good in Session" - The order is valid only during the specified trading session. - */ - GIS, - - /** - * "Good Through Crossing" - The order is valid through the crossing session. - */ - GTX, - - /** - * "Systematic Internaliser Only" - The order is to be executed only on a systematic internaliser. - */ - SIO -} -.... - -Lines 75, Blanks 12, Tokens 586 - -== File: src\main\java\town\lost\oms\dto\ValidateUtil.java -.... -/* - */ -package town.lost.oms.dto; - -/** - * The {@code ValidateUtil} class provides utility methods for validating numerical values such as price and quantity. - * - *

This class is final and cannot be instantiated. It contains static methods that check whether a given price or quantity - * is valid according to predefined business rules. - * - *

Example usage: - * - *

{@code
- * double price = 100.0;
- * if (ValidateUtil.invalidPrice(price)) {
- *     throw new IllegalArgumentException("Invalid price");
- * }
- * }
- */ -public final class ValidateUtil { - - /** - * Private constructor to prevent instantiation. - */ - private ValidateUtil() { - } - - /** - * Checks if the provided price is invalid. - * - *

A price is considered invalid if it is not a finite number or if it is less than or equal to zero. - * - * @param price the price to validate - * @return {@code true} if the price is invalid; {@code false} otherwise - */ - public static boolean invalidPrice(double price) { - return !Double.isFinite(price) || (price <= 0); - } - - /** - * Checks if the provided quantity is invalid. - * - *

A quantity is considered invalid if it is not a finite number or if it is less than zero. - * - * @param quantity the quantity to validate - * @return {@code true} if the quantity is invalid; {@code false} otherwise - */ - public static boolean invalidQuantity(double quantity) { - return !Double.isFinite(quantity) || (quantity < 0); - } -} -.... - -Lines 47, Blanks 4, Tokens 352 - -== File: src\main\java\town\lost\oms\OMSBenchmarkMain.java -.... -/* - */ - -package town.lost.oms; - -import net.openhft.affinity.AffinityLock; -import net.openhft.chronicle.bytes.MethodReader; -import net.openhft.chronicle.core.Jvm; -import net.openhft.chronicle.core.OS; -import net.openhft.chronicle.core.io.BackgroundResourceReleaser; -import net.openhft.chronicle.core.io.IOTools; -import net.openhft.chronicle.jlbh.JLBH; -import net.openhft.chronicle.jlbh.JLBHOptions; -import net.openhft.chronicle.jlbh.JLBHTask; -import net.openhft.chronicle.queue.ChronicleQueue; -import net.openhft.chronicle.threads.DiskSpaceMonitor; -import net.openhft.chronicle.wire.ShortTextLongConverter; -import town.lost.oms.api.OMSIn; -import town.lost.oms.api.OMSOut; -import town.lost.oms.dto.*; - -import static town.lost.oms.OrderAdderMain.now; -// isolcpus=5,6,7 set in grub.cfg -// sudo cpupower frequency-set -g performance -d 4.5g - -// -Xmx64m -XX:+UnlockCommercialFeatures -XX:+FlightRecorder -XX:StartFlightRecording=name=test,filename=test.jfr,dumponexit=true,settings=profile -XX:-UseTLAB -/* Run on a Ryzen 9 5950X, Ubuntu 20.10 --Xmx1g -DbyteInBinary=true -DpregeneratedMarshallable=true -Dthroughput=100000 -DrunTime=120 -Dpath=/tmp --------------------------------- SUMMARY (end to end) us ------------------------------------------- -Percentile run1 run2 run3 run4 run5 % Variation -50.0: 2.06 1.99 1.99 1.99 1.99 0.00 -90.0: 2.13 2.04 2.04 2.05 2.05 0.33 -99.0: 3.16 3.09 3.09 3.10 3.10 0.17 -99.7: 3.66 3.57 3.58 3.58 3.57 0.15 -99.9: 4.02 3.92 3.90 3.90 3.90 0.41 -99.97: 4.78 4.66 4.65 4.70 4.65 0.68 -99.99: 5.58 5.29 5.21 5.34 5.14 2.43 -99.997: 8.56 6.01 5.88 6.34 5.90 5.00 -99.999: 9.30 6.81 6.42 9.39 6.47 23.55 -worst: 50.11 74.11 10.58 13.30 11.15 80.02 ----------------------------------------------------------------------------------------------------- - --Xmx772m -DbyteInBinary=true -DpregeneratedMarshallable=true -Dthroughput=100000 -DrunTime=120 -Dpath=/nvme/tmp --------------------------------- SUMMARY (end to end) us ------------------------------------------- -Percentile run1 run2 run3 run4 run5 % Variation -50.0: 1.98 1.97 1.97 1.97 1.97 0.00 -90.0: 2.05 2.04 2.04 2.04 2.04 0.00 -99.0: 3.38 3.39 3.36 3.34 3.34 0.95 -99.7: 6.34 6.38 5.80 5.45 5.46 10.20 -99.9: 16.27 16.42 16.48 16.42 16.48 0.26 -99.97: 17.38 17.38 17.38 17.25 17.38 0.49 -99.99: 18.91 18.46 18.66 18.21 18.66 1.61 -99.997: 24.29 22.24 22.82 21.09 25.12 11.31 -99.999: 29.15 27.94 26.40 25.76 402.94 90.71 -worst: 214.27 51.78 537.60 50.24 1255.42 94.12 ----------------------------------------------------------------------------------------------------- - */ -public class OMSBenchmarkMain { - static { - System.setProperty("pauser.minProcessors", "2"); - System.setProperty("disableValidate", "true"); - } - - public static final int THROUGHPUT = Integer.getInteger("throughput", 100_000); - public static final int RUN_TIME = Integer.getInteger("runTime", 10); - public static final ShortTextLongConverter ShortText = ShortTextLongConverter.INSTANCE; - public static final String PATH = System.getProperty("path", OS.TMP); - public static final boolean ACCOUNT_FOR_COORDINATED_OMISSION = Jvm.getBoolean("accountForCoordinatedOmission"); - - @SuppressWarnings("try") - public static void main(String[] args) { - printProperties(); - - String tmpDir = PATH + "/bench-" + System.nanoTime(); - try (ChronicleQueue input = single(tmpDir, "/input"); - ChronicleQueue output = single(tmpDir, "/output")) { - - // processing thread - Thread processor = new Thread(() -> { - try (AffinityLock ignored = AffinityLock.acquireCore()) { - OMSOut out = output.createAppender().methodWriter(OMSOut.class); - OMSImpl oms = new OMSImpl(out); - MethodReader in = input.createTailer("test").methodReader(oms); - while (!Thread.currentThread().isInterrupted()) { - in.readOne(); - } - } - }, "processor"); - processor.start(); - - JLBH jlbh = new JLBH(new JLBHOptions() - .warmUpIterations(300_000) - .pauseAfterWarmupMS(500) - .throughput(THROUGHPUT) - .iterations(THROUGHPUT * RUN_TIME) - .runs(5) - .recordOSJitter(false) - .accountForCoordinatedOmission(ACCOUNT_FOR_COORDINATED_OMISSION) - .acquireLock(AffinityLock::acquireCore) - .jlbhTask(new MyJLBHTask(input))); - - Thread last = new Thread(() -> { - try (AffinityLock ignored = AffinityLock.acquireCore()) { - final MethodReader reader = output.createTailer().methodReader(new OMSOut() { - @Override - public void executionReport(ExecutionReport er) { - jlbh.sampleNanos(System.nanoTime() - er.sendingTime()); - } - - @Override - public void orderCancelReject(OrderCancelReject ocr) { - } - - @Override - public void jvmError(String msg) { - } - }); - while (!Thread.currentThread().isInterrupted()) - reader.readOne(); - - } catch (Throwable t) { - t.printStackTrace(); - } - }, "last"); - last.start(); - - jlbh.start(); - - processor.interrupt(); - last.interrupt(); - } - printProperties(); - Jvm.pause(1000); - IOTools.deleteDirWithFiles(tmpDir); - // for a clean shutdown - BackgroundResourceReleaser.stop(); - DiskSpaceMonitor.INSTANCE.close(); - } - - static ChronicleQueue single(String tmpDir, String x) { - return ChronicleQueue.singleBuilder(tmpDir + x) - .blockSize(OS.isSparseFileSupported() ? 64L << 30 : 64L << 20) - .build(); - } - - private static void printProperties() { - long estimatedMemory = Math.round(Runtime.getRuntime().totalMemory() / 1e6); - System.out.println("-Xmx" + estimatedMemory + "m " + - "-DbyteInBinary=" + AbstractEvent.BYTES_IN_BINARY + " " + - "-DpregeneratedMarshallable=" + AbstractEvent.PREGENERATED_MARSHALLABLE + " " + - "-Dthroughput=" + THROUGHPUT + " " + - "-DrunTime=" + RUN_TIME + " " + - "-Dpath=" + PATH + " " + - "-DaccountForCoordinatedOmission=" + ACCOUNT_FOR_COORDINATED_OMISSION); - } - - private static class MyJLBHTask implements JLBHTask { - private JLBH jlbh; - private NewOrderSingle nos; - private OMSIn in; - - public MyJLBHTask(ChronicleQueue input) { - nos = new NewOrderSingle() - .sender(ShortText.parse("client")) - .target(ShortText.parse("OMS")) - .clOrdID("clOrdId") - .orderQty(1e6) - .price(1.6) - .symbol(ShortText.parse("AUDUSD")) - .ordType(OrderType.LIMIT) - .side(Side.SELL); - in = input.createAppender().methodWriter(OMSIn.class); - } - - @Override - public void init(JLBH jlbh) { - this.jlbh = jlbh; - } - - @Override - public void run(long startTimeNS) { - nos.sendingTime(startTimeNS).transactTime(now()); - in.newOrderSingle(nos); - } - } -} -.... - -Lines 164, Blanks 22, Tokens 2236 - -== File: src\main\java\town\lost\oms\OMSImpl.java -.... -/* - */ - -package town.lost.oms; - -import net.openhft.chronicle.core.time.SystemTimeProvider; -import town.lost.oms.api.OMSIn; -import town.lost.oms.api.OMSOut; -import town.lost.oms.dto.*; - -/** - * The {@code OMSImpl} class is a concrete implementation of the {@link OMSIn} interface, - * acting as an intermediary between client requests and the {@link OMSOut} interface. - * - *

This class processes requests for order creation, order cancellation, and cancelling all orders. - * It generates appropriate responses using {@link ExecutionReport} and {@link OrderCancelReject} - * and sends them through the {@link OMSOut} interface. - * - *

Note: This class is not thread-safe. If multiple threads are expected to use - * the same instance of {@code OMSImpl}, synchronization or separate instances per thread should be used. - * - *

Usage Example:

- * - *
{@code
- * OMSOut omsOut = new OMSOutImplementation();
- * OMSIn oms = new OMSImpl(omsOut);
- *
- * NewOrderSingle newOrder = new NewOrderSingle()
- *     .sender("ClientA")
- *     .target("OMS")
- *     .clOrdID("Order123")
- *     .symbol("AAPL")
- *     .orderQty(100)
- *     .price(150.00)
- *     .side(Side.BUY)
- *     .ordType(OrderType.MARKET)
- *     .transactTime(System.currentTimeMillis());
- *
- * oms.newOrderSingle(newOrder);
- * }
- */ -public class OMSImpl implements OMSIn { - // The outbound interface for sending execution reports and order cancel rejections - private final OMSOut out; - - // Reusable instance of ExecutionReport for creating new orders - private final ExecutionReport er = new ExecutionReport(); - - // Reusable instance of OrderCancelReject for cancelling orders - private final OrderCancelReject ocr = new OrderCancelReject(); - - /** - * Constructs a new {@code OMSImpl} with a given outbound interface. - * - * @param out the outbound interface to be used for sending responses - */ - public OMSImpl(OMSOut out) { - this.out = out; - } - - /** - * Processes a new single order request. The request's details are populated into an execution report and sent out. - * - * @param nos the {@link NewOrderSingle} request to process - */ - @Override - public void newOrderSingle(NewOrderSingle nos) { - // Reset the execution report DTO - er.reset(); - final long orderID = SystemTimeProvider.CLOCK.currentTimeNanos(); // Generate unique order ID - - // Populate the ExecutionReport with request details - er.sender(nos.target()) - .target(nos.sender()) - .symbol(nos.symbol()) - .clOrdID(nos.clOrdID()) - .ordType(nos.ordType()) - .orderQty(nos.orderQty()) - .price(nos.price()) - .side(nos.side()) - .sendingTime(nos.sendingTime()) - .transactTime(nos.transactTime()) - .leavesQty(0) - .cumQty(0) - .avgPx(0) - .orderID(orderID) - .text("Not ready"); - - // Send execution report - out.executionReport(er); - } - - /** - * Processes a cancel order request. The request's details are populated into an order cancel rejection and sent out. - * - * @param cor the {@link CancelOrderRequest} request to process - */ - @Override - public void cancelOrderRequest(CancelOrderRequest cor) { - // Reset the reusable OrderCancelReject instance - ocr.reset(); - // Populate OrderCancelReject with request details - ocr.sender(cor.target()) - .target(cor.sender()) - .symbol(cor.symbol()) - .clOrdID(cor.clOrdID()) - .sendingTime(cor.sendingTime()) - .reason("No such order"); - - // Send order cancellation rejection - out.orderCancelReject(ocr); - } - - /** - * Processes a cancel all orders request. The request's details are populated into an order cancel rejection and sent out. - * - * @param cancelAll the {@link CancelAll} request to process - */ - @Override - public void cancelAll(CancelAll cancelAll) { - // Reset the reusable OrderCancelReject instance - ocr.reset(); - // Populate OrderCancelReject with request details - ocr.sender(cancelAll.target()) - .target(cancelAll.sender()) - .symbol(cancelAll.symbol()) - .clOrdID(cancelAll.clOrdID()) - .sendingTime(cancelAll.sendingTime()) - .reason("No orders to cancel"); - - // Send order cancellation rejection - out.orderCancelReject(ocr); - } -} -.... - -Lines 121, Blanks 13, Tokens 1041 - -== File: src\main\java\town\lost\oms\OrderAdderMain.java -.... -/* - */ - -package town.lost.oms; - -import net.openhft.chronicle.core.pool.ClassAliasPool; -import net.openhft.chronicle.core.time.SystemTimeProvider; -import net.openhft.chronicle.core.util.Mocker; -import net.openhft.chronicle.queue.ChronicleQueue; -import net.openhft.chronicle.queue.ExcerptAppender; -import net.openhft.chronicle.queue.impl.single.SingleChronicleQueueBuilder; -import net.openhft.chronicle.queue.rollcycles.TestRollCycles; -import net.openhft.chronicle.wire.converter.ShortText; -import town.lost.oms.api.OMSIn; -import town.lost.oms.dto.*; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; - -/** - * The {@code OrderAdderMain} class is a utility application that allows users to add new orders to the Order Management System (OMS). - * - *

This class connects to a Chronicle Queue and writes {@link NewOrderSingle} messages to it. - * Users can interactively add orders by hitting the Enter key. The application will prompt the user - * and continue to accept orders until any non-empty input is entered. - */ -public class OrderAdderMain { - - /** - * The entry point of the application. - * - * @param args the input arguments (none expected) - */ - public static void main(String[] args) throws IOException { - // Add NewOrderSingle class to the alias pool - ClassAliasPool.CLASS_ALIASES.addAlias(NewOrderSingle.class); - - // Establish connection with the queue - try (ChronicleQueue q = SingleChronicleQueueBuilder.binary("in") - .rollCycle(TestRollCycles.TEST8_DAILY) - .build(); - ExcerptAppender appender = q.createAppender()) { - - // Acquire the appender and write methods for OMSIn - OMSIn in = appender.methodWriter(OMSIn.class); - - // Create a logging mock for OMSIn - OMSIn in2 = Mocker.logging(OMSIn.class, "in - ", System.out); - - // Create a new order single - NewOrderSingle nos = new NewOrderSingle() - .sender(fromShortText("sender")) - .target(fromShortText("target")) - .transactTime(now()) - .sendingTime(now()) - .account(1) - .timeInForce(TimeInForce.GTC) - .currency(Ccy.USD) - .orderQty(1) - .ordType(OrderType.MARKET) - .side(Side.BUY) - .symbol(fromShortText("EURUSD")); - - // Inform the user to add an order - System.out.println("\nHit blank line to add an order, anything else to exit"); - - // Initialize a BufferedReader to read user input - BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); - int orderCounter = 0; - while ("".equals(br.readLine())) { - // For each blank line read, add a new order - nos.clOrdID(Long.toString(orderCounter++)); - - // Send the new order - in.newOrderSingle(nos); - in2.newOrderSingle(nos); - } - } - // Inform the user that the program has finished running - System.out.println("Finished"); - - // Exit the program - System.exit(0); - } - - /** - * Returns the current system time in nanoseconds. - * - * @return the current time in nanoseconds - */ - static long now() { - return SystemTimeProvider.INSTANCE.currentTimeNanos(); - } - - /** - * Converts a string to a long using base85 encoding. - * - * @param s the string to convert - * @return the long representation of the string - */ - static long fromShortText(String s) { - return ShortText.INSTANCE.parse(s); - } -} -.... - -Lines 90, Blanks 15, Tokens 822 - -== File: src\main\java\town\lost\oms\OrderViewerMain.java -.... -/* - */ - -package town.lost.oms; - -import net.openhft.chronicle.bytes.MethodReader; -import net.openhft.chronicle.core.Jvm; -import net.openhft.chronicle.core.util.Mocker; -import net.openhft.chronicle.queue.ChronicleQueue; -import net.openhft.chronicle.queue.impl.single.SingleChronicleQueueBuilder; -import net.openhft.chronicle.queue.rollcycles.TestRollCycles; -import town.lost.oms.api.OMSIn; - -/** - * The {@code OrderViewerMain} class is a utility application that continuously reads and displays - * orders from a Chronicle Queue. It acts as a simple viewer for incoming orders, logging them to - * the console. - */ -public class OrderViewerMain { - - /** - * The entry point of the application. - * - * @param args the input arguments (none expected) - */ - public static void main(String[] args) { - // Inform the user that the program is waiting for messages - System.out.println("\nWaiting for messages..."); - - // Establish connection with the queue - try (ChronicleQueue q = SingleChronicleQueueBuilder.binary("in") - .rollCycle(TestRollCycles.TEST8_DAILY) - .build()) { - - // Create a logging mock for OMSIn - OMSIn logging = Mocker.logging(OMSIn.class, "read - ", System.out); - - // Create a MethodReader from the tail of the queue - MethodReader reader = q.createTailer().methodReader(logging); - - // Continuously read messages from the queue - while (true) { - // Read one message from the queue; pause if no message was read - if (!reader.readOne()) { - Jvm.pause(50); - } - } - } - } -} -.... - -Lines 42, Blanks 8, Tokens 378 - -== File: src\test\java\town\lost\oms\OMSImplTest.java -.... -/* - */ - -package town.lost.oms; - -import net.openhft.chronicle.core.time.SetTimeProvider; -import net.openhft.chronicle.core.time.SystemTimeProvider; -import org.junit.After; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; -import town.lost.oms.api.OMSOut; - -import java.util.Arrays; -import java.util.List; -import java.util.concurrent.TimeUnit; - -import static org.junit.Assert.assertEquals; - -/** - * Test class for OMSImpl. - * The OMSImplTest runs tests for each method in OMSImpl class. - * The test data is read from specified files and the actual output is compared against expected output. - */ -@SuppressWarnings("deprecation") -@RunWith(Parameterized.class) -public class OMSImplTest { - // Defines the paths to the tests to run. - static final List paths = Arrays.asList(new String[]{ - "newOrderSingle", - "newOrderSingleEquity", - "cancelOrderRequest", - "cancelAll" - }); - - // The name of the test, and the tester that will run the test. - final String name; - final net.openhft.chronicle.wire.utils.YamlTester tester; - - // Constructor that sets the name and tester. - public OMSImplTest(String name, net.openhft.chronicle.wire.utils.YamlTester tester) { - this.name = name; - this.tester = tester; - } - - // Defines the parameters for the parameterized test runner. - @Parameterized.Parameters(name = "{0}") - public static List parameters() { - // Returns a list of test parameters to run the tests with. - // Each test will be run with an instance of AccountManagerImpl, - // and will be subjected to various agitations to ensure robustness. - return new net.openhft.chronicle.wire.utils.YamlTesterParametersBuilder<>(out -> new OMSImpl(out), OMSOut.class, paths) - .agitators( - net.openhft.chronicle.wire.utils.YamlAgitator.messageMissing(), - net.openhft.chronicle.wire.utils.YamlAgitator.duplicateMessage(), - net.openhft.chronicle.wire.utils.YamlAgitator.overrideFields("sendingTime: '', symbol: '', side: '', orderQty: NaN, orderQty: -1, price: NaN, price: -1, clOrdId: '', ordType: ''".split(", *")), - net.openhft.chronicle.wire.utils.YamlAgitator.missingFields("sender, target, sendingTime, symbol, transactTime, account, orderQty, price, side, clOrdID, ordType, timeInForce, currency".split(", *"))) - .exceptionHandlerFunction(out -> (log, msg, thrown) -> out.jvmError(thrown == null ? msg : (msg + " " + thrown))) - .exceptionHandlerFunctionAndLog(true) - .get(); - } - - // After each test, this method resets the system time provider. - @After - public void tearDown() { - SystemTimeProvider.CLOCK = SystemTimeProvider.INSTANCE; - } - - // This is the actual test method, which uses the provided tester - // to run the test and then compares the expected output to the actual output. - @Test - public void runTester() { - // Sets the system clock to a specific time for the purpose of testing. - SystemTimeProvider.CLOCK = new SetTimeProvider("2019-12-03T09:54:37.345679") - .autoIncrement(1, TimeUnit.SECONDS); - // Asserts that the expected output matches the actual output. - assertEquals(tester.expected(), tester.actual()); - } -} -.... - -Lines 69, Blanks 10, Tokens 791 - -== File: src\test\resources\cancelAll\in.yaml -.... ---- -# This is a test for the CancelOrderRequest operation. -# The CLIENT1 (sender) is sending a request to the Order Management System (OMS1, the target) -# to cancel an order that was previously placed. The order is identified by its unique ID (clOrdID). - -cancelOrderRequest: { - sender: CLIENT1, - target: OMS1, - sendingTime: 2019-12-03T09:54:37.134475, - symbol: EURUSD, - account: ACC-12345, - clOrdID: dkj4378fwh, - origClOrdID: ABC1234567, - side: BUY -} -... ---- -# This is a test for the CancelAll operation. -# The CLIENT1 (sender) sends a request to the OMS1 (target) to cancel all orders. -cancelAll: { - sender: CLIENT1, - target: OMS1, - sendingTime: 2019-12-03T09:54:37.134475, - symbol: EURUSD, - clOrdID: CANCEL_ALL, -} -... -.... - -Lines 26, Blanks 1, Tokens 229 - -== File: src\test\resources\cancelAll\out.yaml -.... -# This is a test for the CancelOrderRequest operation. -# The CLIENT1 (sender) is sending a request to the Order Management System (OMS1, the target) -# to cancel an order that was previously placed. The order is identified by its unique ID (clOrdID). ---- -orderCancelReject: { - sender: OMS1, - target: CLIENT1, - sendingTime: 2019-12-03T09:54:37.134475, - symbol: EURUSD, - clOrdID: dkj4378fwh, - reason: No such order -} -... -# This is a test for the CancelAll operation. -# The CLIENT1 (sender) sends a request to the OMS1 (target) to cancel all orders. ---- -orderCancelReject: { - sender: OMS1, - target: CLIENT1, - sendingTime: 2019-12-03T09:54:37.134475, - symbol: EURUSD, - clOrdID: CANCEL_ALL, - reason: No orders to cancel -} -... -.... - -Lines 25, Blanks 0, Tokens 220 - -== File: src\test\resources\cancelOrderRequest\in.yaml -.... ---- -# This is a test for the CancelOrderRequest operation. -# The CLIENT1 (sender) is sending a request to the Order Management System (OMS1, the target) -# to cancel an order that was previously placed. The order is identified by its unique ID (clOrdID). - -cancelOrderRequest: { - sender: CLIENT1, - target: OMS1, - sendingTime: 2019-12-03T09:54:37.134475, - symbol: EURUSD, - account: ACC-12345, - clOrdID: dkj4378fwh, - origClOrdID: ABC1234567, - side: BUY -} -... -.... - -Lines 15, Blanks 1, Tokens 142 - -== File: src\test\resources\cancelOrderRequest\out.yaml -.... -# This is a test for the CancelOrderRequest operation. -# The CLIENT1 (sender) is sending a request to the Order Management System (OMS1, the target) -# to cancel an order that was previously placed. The order is identified by its unique ID (clOrdID). ---- -orderCancelReject: { - sender: OMS1, - target: CLIENT1, - sendingTime: 2019-12-03T09:54:37.134475, - symbol: EURUSD, - clOrdID: dkj4378fwh, - reason: No such order -} -... -.... - -Lines 13, Blanks 0, Tokens 124 - -== File: src\test\resources\newOrderSingle\in.yaml -.... ---- -# This is a test for the NewOrderSingle operation. -# The client (sender) is sending a new order to the Order Management System (OMS, the target). -# The order details such as the trading symbol, order quantity, price, and the order type are specified. - -newOrderSingle: { - sender: CLIENT, - target: OMS, - sendingTime: 2019-12-03T09:54:37.134475, - symbol: EURUSD, - transactTime: 2019-12-03T09:54:37.344751, - account: ACC-12345, - orderQty: 10E6, - price: 1.2123, - side: BUY, - clOrdID: dkj4378fwh, - ordType: LIMIT, - timeInForce: GTC, - currency: USD -} -... - -.... - -Lines 20, Blanks 2, Tokens 186 - -== File: src\test\resources\newOrderSingle\out.yaml -.... -# This is a test for the NewOrderSingle operation. -# The client (sender) is sending a new order to the Order Management System (OMS, the target). -# The order details such as the trading symbol, order quantity, price, and the order type are specified. ---- -executionReport: { - sender: OMS, - target: CLIENT, - sendingTime: 2019-12-03T09:54:37.134475, - symbol: EURUSD, - transactTime: 2019-12-03T09:54:37.344751, - orderQty: 10E6, - price: 1.2123, - orderID: 2019-12-03T09:54:37.345679, - lastPx: 0.0, - leavesQty: 0.0, - cumQty: 0.0, - avgPx: 0.0, - side: BUY, - ordType: LIMIT, - clOrdID: dkj4378fwh, - text: Not ready -} -... -.... - -Lines 23, Blanks 0, Tokens 225 - -== File: src\test\resources\newOrderSingleEquity\in.yaml -.... ---- -# Sample `NewOrderSingle` message representing a client's new order sent to the Order Management System (OMS). -newOrderSingle: { - sender: CLIENT1, - target: OMS1, - sendingTime: 2023-10-20T14:25:37.134475Z, - symbol: AAPL, - transactTime: 2023-10-20T14:25:37.134475Z, - account: ACC-12345, - orderQty: 1000, - price: 150.25, - side: buy, - clOrdID: ABC1234567, - ordType: LIMIT, - timeInForce: DAY, - currency: USD -} -... ---- -# Sample `NewOrderSingle` message representing another client's new order sent to the Order Management System (OMS). -newOrderSingle: { - sender: CLIENT2, - target: OMS1, - sendingTime: 2023-10-20T14:27:15.987654Z, - symbol: GOOGL, - transactTime: 2023-10-20T14:27:15.987654Z, - account: ACC-67890, - orderQty: 500, - price: 2750.50, - side: sell, - clOrdID: XYZ7654321, - ordType: MARKET, - timeInForce: DAY, - currency: USD -} -... -.... - -Lines 36, Blanks 0, Tokens 299 - -== File: src\test\resources\newOrderSingleEquity\out.yaml -.... -# Sample `NewOrderSingle` message representing a client's new order sent to the Order Management System (OMS). ---- -executionReport: { - sender: OMS1, - target: CLIENT1, - sendingTime: 2023-10-20T14:25:37.134475, - symbol: AAPL, - transactTime: 2023-10-20T14:25:37.134475, - orderQty: 1E3, - price: 150.25, - orderID: 2019-12-03T09:54:37.345679, - lastPx: 0.0, - leavesQty: 0.0, - cumQty: 0.0, - avgPx: 0.0, - side: BUY, - ordType: LIMIT, - clOrdID: ABC1234567, - text: Not ready -} -... -# Sample `NewOrderSingle` message representing another client's new order sent to the Order Management System (OMS). ---- -executionReport: { - sender: OMS1, - target: CLIENT2, - sendingTime: 2023-10-20T14:27:15.987654, - symbol: GOOGL, - transactTime: 2023-10-20T14:27:15.987654, - orderQty: 500.0, - price: 2750.5, - orderID: 2019-12-03T09:54:38.345679, - lastPx: 0.0, - leavesQty: 0.0, - cumQty: 0.0, - avgPx: 0.0, - side: SELL, - ordType: MARKET, - clOrdID: XYZ7654321, - text: Not ready -} -... -.... - -Lines 42, Blanks 0, Tokens 382 - -== Summary == -Total Lines: 2833 -Total Blanks: 298 -Total Tokens: 25169 -Tokens/Line: 8.9 From dca9402953c7900b7a569c7f9e987999053f7def Mon Sep 17 00:00:00 2001 From: Peter Lawrey Date: Thu, 2 Jan 2025 14:04:53 +0000 Subject: [PATCH 30/31] Update project en-GB --- account/prompts/improve-test-data.adoc | 4 ++-- .../account/AccountManagerBenchmarkMain.java | 2 +- .../chronicle/account/dto/CreateAccountTest.java | 16 ++++++++-------- .../run/chronicle/account/dto/TransferTest.java | 4 ++-- .../run/chronicle/routing/inout/api/Value.java | 2 +- .../run/chronicle/routing/out/SifterImpl.java | 2 +- .../inout/api/ViaOutTextMethodWriter.java | 2 +- .../main/java/org/trading/AggregatorImpl.java | 2 +- .../src/main/java/org/trading/OMSImpl.java | 4 ++-- .../src/main/java/org/trading/StrategyImpl.java | 2 +- .../main/java/town/lost/oms/OrderAdderMain.java | 2 +- .../chronicle/queue/simple/avro/InputMain.java | 2 +- .../queue/simple/translator/package-info.java | 2 +- 13 files changed, 23 insertions(+), 23 deletions(-) diff --git a/account/prompts/improve-test-data.adoc b/account/prompts/improve-test-data.adoc index b79e29e..26ac9ba 100644 --- a/account/prompts/improve-test-data.adoc +++ b/account/prompts/improve-test-data.adoc @@ -7,7 +7,7 @@ The goal is to produce maintainable, clear, and requirements-aligned test cases. The testing approach involves the following files: -* `_setup.yaml` - Initializes the system state before the test scenario begins (e.g., creating initial accounts). +* `_setup.yaml` - Initialises the system state before the test scenario begins (e.g., creating initial accounts). * `in.yaml` - Defines the input commands (events) that the system under test will process. * `out.yaml` - Specifies the expected events produced by the system in response to the inputs, along with helpful comments that link the outputs back to the corresponding input events. @@ -70,7 +70,7 @@ sequenceDiagram SetupFile->>TestRunner: Load initial state instructions Note over TestRunner: The Test Runner reads _setup.yaml
and applies initial configurations
(e.g., create accounts) - TestRunner->>System: Initialize system state
from _setup.yaml + TestRunner->>System: Initialise system state
from _setup.yaml Note over System: System now has initial state
(e.g., Alice’s account with 15 EUR) InputFile->>TestRunner: Provide input
commands/events diff --git a/account/src/main/java/run/chronicle/account/AccountManagerBenchmarkMain.java b/account/src/main/java/run/chronicle/account/AccountManagerBenchmarkMain.java index c355f24..c856fc0 100644 --- a/account/src/main/java/run/chronicle/account/AccountManagerBenchmarkMain.java +++ b/account/src/main/java/run/chronicle/account/AccountManagerBenchmarkMain.java @@ -53,7 +53,7 @@ public static void main(String[] args) throws InterruptedException, MalformedURL // This ExecutorService is used for running the client/gateway/service in the current process ExecutorService es = Executors.newCachedThreadPool(new AffinityThreadFactory("test")); - // Initialize a new instance of AccountManagerServiceMain. This is the main service for managing accounts. + // Initialise a new instance of AccountManagerServiceMain. This is the main service for managing accounts. AccountManagerServiceMain service = null; // Check if the host part of the URL is empty. If it is, that means we are running the service locally. diff --git a/account/src/test/java/run/chronicle/account/dto/CreateAccountTest.java b/account/src/test/java/run/chronicle/account/dto/CreateAccountTest.java index 680dace..b597628 100755 --- a/account/src/test/java/run/chronicle/account/dto/CreateAccountTest.java +++ b/account/src/test/java/run/chronicle/account/dto/CreateAccountTest.java @@ -17,7 +17,7 @@ public class CreateAccountTest { /** - * Helper method to create a fully-initialized {@link CreateAccount} object + * Helper method to create a fully-initialised {@link CreateAccount} object * for testing. Uses consistent, known values for all fields. * * @return a {@code CreateAccount} instance populated with test data @@ -63,21 +63,21 @@ public void testToString() { expectedTime, createAccount.sendingTime()); // Verify the name field is correctly set. - assertEquals("The name field should match the initialized value.", + assertEquals("The name field should match the initialised value.", "name", createAccount.name()); // Additional checks to ensure all fields are as expected. - assertEquals("The sender field should match the initialized value.", + assertEquals("The sender field should match the initialised value.", "sender", ShortText.INSTANCE.asString(createAccount.sender())); - assertEquals("The target field should match the initialized value.", + assertEquals("The target field should match the initialised value.", "target", ShortText.INSTANCE.asString(createAccount.target())); - assertEquals("The account number should match the initialized value.", + assertEquals("The account number should match the initialised value.", 2L, createAccount.account()); - assertEquals("The currency field should match the initialized value.", + assertEquals("The currency field should match the initialised value.", "CURR", ShortText.INSTANCE.asString(createAccount.currency())); - assertEquals("The balance should match the initialized value.", + assertEquals("The balance should match the initialised value.", 1.0, createAccount.balance(), 0.0); - assertEquals("The overdraft should match the initialized value.", + assertEquals("The overdraft should match the initialised value.", 3.0, createAccount.overdraft(), 0.0); } } diff --git a/account/src/test/java/run/chronicle/account/dto/TransferTest.java b/account/src/test/java/run/chronicle/account/dto/TransferTest.java index e31d8a0..bdb72a2 100755 --- a/account/src/test/java/run/chronicle/account/dto/TransferTest.java +++ b/account/src/test/java/run/chronicle/account/dto/TransferTest.java @@ -73,8 +73,8 @@ public void testToString() { assertEquals("sender", ShortText.INSTANCE.asString(transfer.sender())); assertEquals("target", ShortText.INSTANCE.asString(transfer.target())); assertEquals(NanoTime.INSTANCE.parse("2001/02/03T04:05:06.007008009"), transfer.sendingTime()); - assertEquals("Expected account 'from' to match initialized value.", 12345, transfer.from()); - assertEquals("Expected account 'to' to match initialized value.", 67890, transfer.to()); + assertEquals("Expected account 'from' to match initialised value.", 12345, transfer.from()); + assertEquals("Expected account 'to' to match initialised value.", 67890, transfer.to()); assertEquals("Expected currency to be 'CURR' after parsing.", "CURR", ShortText.INSTANCE.asString(transfer.currency())); assertEquals("Expected amount to be 1.0.", 1.0, transfer.amount(), 0.0); assertEquals("Expected reference field to match 'reference' bytes.", diff --git a/event-routing/src/main/java/run/chronicle/routing/inout/api/Value.java b/event-routing/src/main/java/run/chronicle/routing/inout/api/Value.java index b2979c3..07b24ee 100755 --- a/event-routing/src/main/java/run/chronicle/routing/inout/api/Value.java +++ b/event-routing/src/main/java/run/chronicle/routing/inout/api/Value.java @@ -14,7 +14,7 @@ public class Value extends SelfDescribingMarshallable { /** * This is a constructor for the Value class. - * It initializes the val instance variable with the provided value. + * It initialises the val instance variable with the provided value. * * @param val A long value to set as the initial value for this instance */ diff --git a/event-routing/src/main/java/run/chronicle/routing/out/SifterImpl.java b/event-routing/src/main/java/run/chronicle/routing/out/SifterImpl.java index cee4133..c5c6b85 100755 --- a/event-routing/src/main/java/run/chronicle/routing/out/SifterImpl.java +++ b/event-routing/src/main/java/run/chronicle/routing/out/SifterImpl.java @@ -29,7 +29,7 @@ public class SifterImpl implements SifterIn { /** * Constructor for the SifterImpl class. - * This initializes the SifterOut object used for outputting the processed values. + * This initialises the SifterOut object used for outputting the processed values. * * @param so A SifterOut object to be used for outputting the processed values */ diff --git a/event-routing/src/test/java/run/chronicle/routing/inout/api/ViaOutTextMethodWriter.java b/event-routing/src/test/java/run/chronicle/routing/inout/api/ViaOutTextMethodWriter.java index e4c8cab..c06b97d 100755 --- a/event-routing/src/test/java/run/chronicle/routing/inout/api/ViaOutTextMethodWriter.java +++ b/event-routing/src/test/java/run/chronicle/routing/inout/api/ViaOutTextMethodWriter.java @@ -18,7 +18,7 @@ public final class ViaOutTextMethodWriter implements ViaOut out; // Constructor for the ViaOutTextMethodWriter class. - // It initializes the Supplier for output handling and a Closeable resource to be managed. + // It initialises the Supplier for output handling and a Closeable resource to be managed. // An UpdateInterceptor is also accepted as an argument, but currently not used in the class. public ViaOutTextMethodWriter(Supplier out, Closeable closeable, UpdateInterceptor updateInterceptor) { this.out = out; diff --git a/md-pipeline/src/main/java/org/trading/AggregatorImpl.java b/md-pipeline/src/main/java/org/trading/AggregatorImpl.java index ef77b59..5bd4875 100644 --- a/md-pipeline/src/main/java/org/trading/AggregatorImpl.java +++ b/md-pipeline/src/main/java/org/trading/AggregatorImpl.java @@ -28,7 +28,7 @@ public class AggregatorImpl implements AggregatorIn { /** * Constructor for the AggregatorImpl class. - * Initializes the AggregatorOut instance for output handling. + * Initialises the AggregatorOut instance for output handling. * * @param out An implementation of the AggregatorOut interface for handling aggregated data */ diff --git a/md-pipeline/src/main/java/org/trading/OMSImpl.java b/md-pipeline/src/main/java/org/trading/OMSImpl.java index ed1e008..797a21b 100644 --- a/md-pipeline/src/main/java/org/trading/OMSImpl.java +++ b/md-pipeline/src/main/java/org/trading/OMSImpl.java @@ -19,7 +19,7 @@ public class OMSImpl implements OMSIn { /** * This is the constructor for the OMSImpl class. - * It initializes the out instance variable with the provided OMSOut object. + * It initialises the out instance variable with the provided OMSOut object. * * @param out An instance of the OMSOut interface that will handle the output of the OMS. */ @@ -29,7 +29,7 @@ public OMSImpl(OMSOut out) { /** * This is the main method for the OMSImpl class. - * It sets up the system and initializes the necessary components for running the OMS. + * It sets up the system and initialises the necessary components for running the OMS. * * @param args Command-line arguments (not used in this implementation) */ diff --git a/md-pipeline/src/main/java/org/trading/StrategyImpl.java b/md-pipeline/src/main/java/org/trading/StrategyImpl.java index c3ceb3e..8cbf696 100644 --- a/md-pipeline/src/main/java/org/trading/StrategyImpl.java +++ b/md-pipeline/src/main/java/org/trading/StrategyImpl.java @@ -21,7 +21,7 @@ public class StrategyImpl implements AggregatorOut { /** * Constructor for the StrategyImpl class. - * It initializes the output interface for OMS and a new order object. + * It initialises the output interface for OMS and a new order object. * * @param out An instance of {@link OMSIn} to be used for communication with the OMS */ diff --git a/order-processor/src/main/java/town/lost/oms/OrderAdderMain.java b/order-processor/src/main/java/town/lost/oms/OrderAdderMain.java index 0e583a3..99bc82d 100644 --- a/order-processor/src/main/java/town/lost/oms/OrderAdderMain.java +++ b/order-processor/src/main/java/town/lost/oms/OrderAdderMain.java @@ -66,7 +66,7 @@ public static void main(String[] args) throws IOException { // Inform the user to add an order System.out.println("\nHit blank line to add an order, anything else to exit"); - // Initialize a BufferedReader to read user input + // Initialise a BufferedReader to read user input BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); int orderCounter = 0; while ("".equals(br.readLine())) { diff --git a/simple-avro-example/src/main/java/net/openhft/chronicle/queue/simple/avro/InputMain.java b/simple-avro-example/src/main/java/net/openhft/chronicle/queue/simple/avro/InputMain.java index be23cb9..f344914 100644 --- a/simple-avro-example/src/main/java/net/openhft/chronicle/queue/simple/avro/InputMain.java +++ b/simple-avro-example/src/main/java/net/openhft/chronicle/queue/simple/avro/InputMain.java @@ -27,7 +27,7 @@ public static void main(String[] args) throws IOException { GenericRecord user = avro.getGenericRecord(); user.put("name", "Ben"); user.put("favorite_number", 7); - user.put("favorite_color", "red"); + user.put("favorite_colour", "red"); avro.writeToOS(user, dc.wire().bytes().outputStream()); } diff --git a/simple-translator/src/main/java/net/openhft/chronicle/queue/simple/translator/package-info.java b/simple-translator/src/main/java/net/openhft/chronicle/queue/simple/translator/package-info.java index 5a6dd0a..03c2cba 100755 --- a/simple-translator/src/main/java/net/openhft/chronicle/queue/simple/translator/package-info.java +++ b/simple-translator/src/main/java/net/openhft/chronicle/queue/simple/translator/package-info.java @@ -20,7 +20,7 @@ * *

  • * {@link net.openhft.chronicle.queue.simple.translator.TranslatorMain}: - * The entry point that initializes and configures a {@link SimpleTranslator} to bridge messages + * The entry point that initialises and configures a {@link SimpleTranslator} to bridge messages * between the English and French queues. *
  • *
  • From da336e182ce200758276c86eb26cd012fbcf4c85 Mon Sep 17 00:00:00 2001 From: Peter Lawrey Date: Thu, 2 Jan 2025 14:19:15 +0000 Subject: [PATCH 31/31] Update the account example --- .../test/resources/account/bard-jinja/in.yaml | 22 +++++++++---------- .../test/resources/account/gpt-jinja/in.yaml | 20 ++++++++--------- 2 files changed, 20 insertions(+), 22 deletions(-) diff --git a/account/src/test/resources/account/bard-jinja/in.yaml b/account/src/test/resources/account/bard-jinja/in.yaml index 035b33c..957115f 100755 --- a/account/src/test/resources/account/bard-jinja/in.yaml +++ b/account/src/test/resources/account/bard-jinja/in.yaml @@ -1,30 +1,28 @@ -{ # Generated by Bard #} +{# Generated by Bard #} --- # This operation creates 10 accounts. - { % for i in range(10, 20) % } +{% for i in range(10, 20) %} createAccount: { sender: gw1, target: vault, - sendingTime: - 2023-01-20T10:00: { { i } }, + sendingTime: 2023-01-20T10:00:{{ i }}, name: "account_{{ i }}", - account: { { i } }, + account: {{ i }}, currency: EUR, balance: 100 } - { % endfor % } +{% endfor %} ... --- # This operation creates 10 transfers between the accounts. - { % for i in range(10, 20) % } +{% for i in range(10, 20) %} transfer: { - sender: { { i } }, - target: { { i + 1 } }, - sendingTime: - 2023-01-20T10:00: + { { i } }, + sender: {{ i }}, + target: {{ i + 1 }}, + sendingTime: 2023-01-20T10:00: + {{ i }}, currency: EUR, amount: 10, reference: "transfer_{{ i }}" } - { % endfor % } +{% endfor %} ... diff --git a/account/src/test/resources/account/gpt-jinja/in.yaml b/account/src/test/resources/account/gpt-jinja/in.yaml index 805f48c..5467bde 100644 --- a/account/src/test/resources/account/gpt-jinja/in.yaml +++ b/account/src/test/resources/account/gpt-jinja/in.yaml @@ -1,30 +1,30 @@ -{ # Generated by GPT-4 #} +{# Generated by GPT-4 #} --- # Starting from account number 10, we create 10 new accounts - { % for i in range(10, 20) % } +{% for i in range(10, 20) %} createAccount: { sender: gw1, target: vault, sendingTime: "2023-01-21T10:{{ i }}:00", name: "user{{ i }}", - account: { { i } }, + account: {{ i }}, currency: EUR, balance: 1000 } - { % endfor % } +{% endfor %} ... --- # Starting from account number 10, we initiate 10 transfers to the next account - { % for i in range(10, 20-1) % } { # -1 because the last account won't have a next account to transfer to #} +{% for i in range(10, 20-1) %} {# -1 because the last account won't have a next account to transfer to #} transfer: { sender: gw2, target: vault, sendingTime: "2023-01-21T11:{{ i }}:00", - from: { { i } }, - to: { { i|add(1) } }, + from: {{ i }}, + to: {{ i|add(1) }}, currency: EUR, amount: 100, - reference: Transfer { { i } } to { { i|add(1) } } + reference: Transfer {{ i }} to {{ i|add(1) }} } - { % endfor % } - ... +{% endfor %} +... \ No newline at end of file