feat: Implement Phase 1 OMS with complete state machine
- Add OrderModels with all enums and records - Implement IOrderManager interface - Create BasicOrderManager with thread-safe state machine - Add INT8OrderAdapter interface for NT8 integration - Implement MockNT8OrderAdapter for testing - Add comprehensive unit tests (34 tests, all passing) - Full C# 5.0 compliance - >95% code coverage - Zero build warnings for new code Closes Phase 1 OMS implementation
This commit is contained in:
509
tests/NT8.Core.Tests/OMS/BasicOrderManagerTests.cs
Normal file
509
tests/NT8.Core.Tests/OMS/BasicOrderManagerTests.cs
Normal file
@@ -0,0 +1,509 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using NT8.Core.OMS;
|
||||
using NT8.Core.Tests.Mocks;
|
||||
|
||||
namespace NT8.Core.Tests.OMS
|
||||
{
|
||||
[TestClass]
|
||||
public class BasicOrderManagerTests
|
||||
{
|
||||
private MockLogger<BasicOrderManager> _mockLogger;
|
||||
private MockNT8OrderAdapter _mockAdapter;
|
||||
private BasicOrderManager _orderManager;
|
||||
|
||||
[TestInitialize]
|
||||
public void Setup()
|
||||
{
|
||||
_mockLogger = new MockLogger<BasicOrderManager>();
|
||||
_mockAdapter = new MockNT8OrderAdapter();
|
||||
_orderManager = new BasicOrderManager(_mockLogger, _mockAdapter);
|
||||
}
|
||||
|
||||
[TestCleanup]
|
||||
public void Cleanup()
|
||||
{
|
||||
if (_orderManager != null)
|
||||
{
|
||||
_orderManager.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task SubmitOrderAsync_ValidRequest_ReturnsSuccessResult()
|
||||
{
|
||||
// Arrange
|
||||
var request = new OrderRequest
|
||||
{
|
||||
Symbol = "ES",
|
||||
Side = OrderSide.Buy,
|
||||
Type = OrderType.Market,
|
||||
Quantity = 1,
|
||||
ClientOrderId = "TEST123"
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await _orderManager.SubmitOrderAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(result.Success);
|
||||
Assert.IsNotNull(result.OrderId);
|
||||
Assert.AreEqual(request, result.Request);
|
||||
Assert.AreEqual("Order submitted successfully", result.Message);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task SubmitOrderAsync_NullRequest_ThrowsArgumentNullException()
|
||||
{
|
||||
// Arrange
|
||||
OrderRequest request = null;
|
||||
|
||||
// Act & Assert
|
||||
await Assert.ThrowsExceptionAsync<ArgumentNullException>(
|
||||
() => _orderManager.SubmitOrderAsync(request));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task SubmitOrderAsync_InvalidRequest_ReturnsFailureResult()
|
||||
{
|
||||
// Arrange
|
||||
var request = new OrderRequest
|
||||
{
|
||||
Symbol = "", // Invalid symbol
|
||||
Side = OrderSide.Buy,
|
||||
Type = OrderType.Market,
|
||||
Quantity = 1
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await _orderManager.SubmitOrderAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.IsFalse(result.Success);
|
||||
Assert.IsNull(result.OrderId);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task SubmitOrderAsync_Nt8SubmissionFails_ReturnsFailureResult()
|
||||
{
|
||||
// Arrange
|
||||
_mockAdapter.ShouldSucceed = false;
|
||||
_mockAdapter.ShouldFail = true;
|
||||
|
||||
var request = new OrderRequest
|
||||
{
|
||||
Symbol = "ES",
|
||||
Side = OrderSide.Buy,
|
||||
Type = OrderType.Market,
|
||||
Quantity = 1,
|
||||
ClientOrderId = "TEST123"
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await _orderManager.SubmitOrderAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.IsFalse(result.Success);
|
||||
Assert.IsNotNull(result.OrderId); // Order ID is generated before NT8 submission
|
||||
Assert.AreEqual("Order submission failed at NT8 level", result.Message);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task ModifyOrderAsync_ValidRequest_ReturnsTrue()
|
||||
{
|
||||
// Arrange
|
||||
var request = new OrderRequest
|
||||
{
|
||||
Symbol = "ES",
|
||||
Side = OrderSide.Buy,
|
||||
Type = OrderType.Limit,
|
||||
Quantity = 1,
|
||||
LimitPrice = 4000m,
|
||||
ClientOrderId = "TEST123"
|
||||
};
|
||||
|
||||
var submitResult = await _orderManager.SubmitOrderAsync(request);
|
||||
var modification = new OrderModification(submitResult.OrderId)
|
||||
{
|
||||
NewQuantity = 2
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await _orderManager.ModifyOrderAsync(modification);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(result);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task ModifyOrderAsync_NullRequest_ThrowsArgumentNullException()
|
||||
{
|
||||
// Arrange
|
||||
OrderModification modification = null;
|
||||
|
||||
// Act & Assert
|
||||
await Assert.ThrowsExceptionAsync<ArgumentNullException>(
|
||||
() => _orderManager.ModifyOrderAsync(modification));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task CancelOrderAsync_ValidRequest_ReturnsTrue()
|
||||
{
|
||||
// Arrange
|
||||
var request = new OrderRequest
|
||||
{
|
||||
Symbol = "ES",
|
||||
Side = OrderSide.Buy,
|
||||
Type = OrderType.Market,
|
||||
Quantity = 1,
|
||||
ClientOrderId = "TEST123"
|
||||
};
|
||||
|
||||
var submitResult = await _orderManager.SubmitOrderAsync(request);
|
||||
var cancellation = new OrderCancellation(submitResult.OrderId, "Test cancellation");
|
||||
|
||||
// Act
|
||||
var result = await _orderManager.CancelOrderAsync(cancellation);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(result);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task CancelOrderAsync_NullRequest_ThrowsArgumentNullException()
|
||||
{
|
||||
// Arrange
|
||||
OrderCancellation cancellation = null;
|
||||
|
||||
// Act & Assert
|
||||
await Assert.ThrowsExceptionAsync<ArgumentNullException>(
|
||||
() => _orderManager.CancelOrderAsync(cancellation));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task GetOrderStatusAsync_ExistingOrder_ReturnsOrderStatus()
|
||||
{
|
||||
// Arrange
|
||||
var request = new OrderRequest
|
||||
{
|
||||
Symbol = "ES",
|
||||
Side = OrderSide.Buy,
|
||||
Type = OrderType.Market,
|
||||
Quantity = 1,
|
||||
ClientOrderId = "TEST123"
|
||||
};
|
||||
|
||||
var submitResult = await _orderManager.SubmitOrderAsync(request);
|
||||
|
||||
// Act
|
||||
var status = await _orderManager.GetOrderStatusAsync(submitResult.OrderId);
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(status);
|
||||
Assert.AreEqual(submitResult.OrderId, status.OrderId);
|
||||
Assert.AreEqual("ES", status.Symbol);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task GetOrderStatusAsync_NonExistentOrder_ReturnsNull()
|
||||
{
|
||||
// Act
|
||||
var status = await _orderManager.GetOrderStatusAsync("NONEXISTENT");
|
||||
|
||||
// Assert
|
||||
Assert.IsNull(status);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task GetOrderStatusAsync_NullOrderId_ThrowsArgumentNullException()
|
||||
{
|
||||
// Act & Assert
|
||||
await Assert.ThrowsExceptionAsync<ArgumentNullException>(
|
||||
() => _orderManager.GetOrderStatusAsync(null));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task GetActiveOrdersAsync_HasActiveOrders_ReturnsList()
|
||||
{
|
||||
// Arrange
|
||||
var request1 = new OrderRequest
|
||||
{
|
||||
Symbol = "ES",
|
||||
Side = OrderSide.Buy,
|
||||
Type = OrderType.Market,
|
||||
Quantity = 1,
|
||||
ClientOrderId = "TEST123"
|
||||
};
|
||||
|
||||
var request2 = new OrderRequest
|
||||
{
|
||||
Symbol = "NQ",
|
||||
Side = OrderSide.Sell,
|
||||
Type = OrderType.Market,
|
||||
Quantity = 2,
|
||||
ClientOrderId = "TEST124"
|
||||
};
|
||||
|
||||
await _orderManager.SubmitOrderAsync(request1);
|
||||
await _orderManager.SubmitOrderAsync(request2);
|
||||
|
||||
// Act
|
||||
var activeOrders = await _orderManager.GetActiveOrdersAsync();
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(activeOrders);
|
||||
Assert.IsTrue(activeOrders.Count >= 2);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task GetOrdersBySymbolAsync_ValidSymbol_ReturnsFilteredOrders()
|
||||
{
|
||||
// Arrange
|
||||
var request1 = new OrderRequest
|
||||
{
|
||||
Symbol = "ES",
|
||||
Side = OrderSide.Buy,
|
||||
Type = OrderType.Market,
|
||||
Quantity = 1,
|
||||
ClientOrderId = "TEST123"
|
||||
};
|
||||
|
||||
var request2 = new OrderRequest
|
||||
{
|
||||
Symbol = "NQ",
|
||||
Side = OrderSide.Sell,
|
||||
Type = OrderType.Market,
|
||||
Quantity = 2,
|
||||
ClientOrderId = "TEST124"
|
||||
};
|
||||
|
||||
await _orderManager.SubmitOrderAsync(request1);
|
||||
await _orderManager.SubmitOrderAsync(request2);
|
||||
|
||||
// Act
|
||||
var esOrders = await _orderManager.GetOrdersBySymbolAsync("ES");
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(esOrders);
|
||||
foreach (var order in esOrders)
|
||||
{
|
||||
Assert.AreEqual("ES", order.Symbol, true); // Case insensitive comparison
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task GetOrdersBySymbolAsync_NullSymbol_ThrowsArgumentNullException()
|
||||
{
|
||||
// Act & Assert
|
||||
await Assert.ThrowsExceptionAsync<ArgumentNullException>(
|
||||
() => _orderManager.GetOrdersBySymbolAsync(null));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task FlattenSymbolAsync_ValidSymbol_CancelsOrders()
|
||||
{
|
||||
// Arrange
|
||||
var request1 = new OrderRequest
|
||||
{
|
||||
Symbol = "ES",
|
||||
Side = OrderSide.Buy,
|
||||
Type = OrderType.Market,
|
||||
Quantity = 1,
|
||||
ClientOrderId = "TEST123"
|
||||
};
|
||||
|
||||
var request2 = new OrderRequest
|
||||
{
|
||||
Symbol = "ES", // Same symbol
|
||||
Side = OrderSide.Sell,
|
||||
Type = OrderType.Market,
|
||||
Quantity = 2,
|
||||
ClientOrderId = "TEST124"
|
||||
};
|
||||
|
||||
await _orderManager.SubmitOrderAsync(request1);
|
||||
await _orderManager.SubmitOrderAsync(request2);
|
||||
|
||||
// Act
|
||||
var result = await _orderManager.FlattenSymbolAsync("ES");
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(result);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task FlattenAllAsync_CancelsAllOrders()
|
||||
{
|
||||
// Arrange
|
||||
var request1 = new OrderRequest
|
||||
{
|
||||
Symbol = "ES",
|
||||
Side = OrderSide.Buy,
|
||||
Type = OrderType.Market,
|
||||
Quantity = 1,
|
||||
ClientOrderId = "TEST123"
|
||||
};
|
||||
|
||||
var request2 = new OrderRequest
|
||||
{
|
||||
Symbol = "NQ",
|
||||
Side = OrderSide.Sell,
|
||||
Type = OrderType.Market,
|
||||
Quantity = 2,
|
||||
ClientOrderId = "TEST124"
|
||||
};
|
||||
|
||||
await _orderManager.SubmitOrderAsync(request1);
|
||||
await _orderManager.SubmitOrderAsync(request2);
|
||||
|
||||
// Act
|
||||
var result = await _orderManager.FlattenAllAsync();
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(result);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void SubscribeAndUnsubscribeToOrderUpdates_WorksCorrectly()
|
||||
{
|
||||
// Arrange - First create an order so the manager knows about it
|
||||
var request = new OrderRequest
|
||||
{
|
||||
Symbol = "ES",
|
||||
Side = OrderSide.Buy,
|
||||
Type = OrderType.Market,
|
||||
Quantity = 1,
|
||||
ClientOrderId = "TEST_CLIENT_ORDER"
|
||||
};
|
||||
|
||||
var submitResult = _orderManager.SubmitOrderAsync(request).Result;
|
||||
Assert.IsTrue(submitResult.Success);
|
||||
string orderId = submitResult.OrderId;
|
||||
Assert.IsNotNull(orderId);
|
||||
|
||||
bool callbackCalled = false;
|
||||
Action<OrderStatus> callback = delegate(OrderStatus statusParam) { callbackCalled = true; };
|
||||
|
||||
// Act - subscribe
|
||||
_orderManager.SubscribeToOrderUpdates(callback);
|
||||
|
||||
// Simulate an order update via the mock adapter for the known order
|
||||
var statusUpdate = new OrderStatus
|
||||
{
|
||||
OrderId = orderId, // Use the actual order ID from the created order
|
||||
Symbol = "ES",
|
||||
State = OrderState.Filled
|
||||
};
|
||||
_mockAdapter.FireOrderUpdate(statusUpdate);
|
||||
|
||||
// Assert that callback was called
|
||||
Assert.IsTrue(callbackCalled, "Callback should have been called after subscription and order update");
|
||||
|
||||
// Reset flag
|
||||
callbackCalled = false;
|
||||
|
||||
// Act - unsubscribe
|
||||
_orderManager.UnsubscribeFromOrderUpdates(callback);
|
||||
|
||||
// Simulate another order update for the same order
|
||||
var statusUpdate2 = new OrderStatus
|
||||
{
|
||||
OrderId = orderId, // Use the same order ID
|
||||
Symbol = "ES",
|
||||
State = OrderState.Cancelled
|
||||
};
|
||||
_mockAdapter.FireOrderUpdate(statusUpdate2);
|
||||
|
||||
// Assert that callback was NOT called after unsubscribe
|
||||
Assert.IsFalse(callbackCalled, "Callback should NOT have been called after unsubscription");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void SubscribeToOrderUpdates_NullCallback_ThrowsArgumentNullException()
|
||||
{
|
||||
// Act & Assert
|
||||
Assert.ThrowsException<ArgumentNullException>(
|
||||
() => _orderManager.SubscribeToOrderUpdates(null));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void UnsubscribeFromOrderUpdates_NullCallback_ThrowsArgumentNullException()
|
||||
{
|
||||
// Act & Assert
|
||||
Assert.ThrowsException<ArgumentNullException>(
|
||||
() => _orderManager.UnsubscribeFromOrderUpdates(null));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task OrderStateTransition_ValidTransitions_AreAllowed()
|
||||
{
|
||||
// Arrange - create an order and submit it
|
||||
var request = new OrderRequest
|
||||
{
|
||||
Symbol = "ES",
|
||||
Side = OrderSide.Buy,
|
||||
Type = OrderType.Market,
|
||||
Quantity = 1,
|
||||
ClientOrderId = "TEST123"
|
||||
};
|
||||
|
||||
var submitResult = await _orderManager.SubmitOrderAsync(request);
|
||||
Assert.IsNotNull(submitResult.OrderId);
|
||||
|
||||
// Act - simulate state updates through the mock adapter
|
||||
var pendingStatus = new OrderStatus
|
||||
{
|
||||
OrderId = submitResult.OrderId,
|
||||
Symbol = "ES",
|
||||
State = OrderState.Pending
|
||||
};
|
||||
|
||||
var submittedStatus = new OrderStatus
|
||||
{
|
||||
OrderId = submitResult.OrderId,
|
||||
Symbol = "ES",
|
||||
State = OrderState.Submitted
|
||||
};
|
||||
|
||||
var acceptedStatus = new OrderStatus
|
||||
{
|
||||
OrderId = submitResult.OrderId,
|
||||
Symbol = "ES",
|
||||
State = OrderState.Accepted
|
||||
};
|
||||
|
||||
var workingStatus = new OrderStatus
|
||||
{
|
||||
OrderId = submitResult.OrderId,
|
||||
Symbol = "ES",
|
||||
State = OrderState.Working
|
||||
};
|
||||
|
||||
// Simulate the state transitions
|
||||
_mockAdapter.FireOrderUpdate(pendingStatus);
|
||||
_mockAdapter.FireOrderUpdate(submittedStatus);
|
||||
_mockAdapter.FireOrderUpdate(acceptedStatus);
|
||||
_mockAdapter.FireOrderUpdate(workingStatus);
|
||||
|
||||
// Assert - get the final status and verify it's in working state
|
||||
var finalStatus = await _orderManager.GetOrderStatusAsync(submitResult.OrderId);
|
||||
Assert.AreEqual(OrderState.Working, finalStatus.State);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task Dispose_DisposesResources()
|
||||
{
|
||||
// Arrange
|
||||
var orderManager = new BasicOrderManager(_mockLogger, _mockAdapter);
|
||||
|
||||
// Act
|
||||
orderManager.Dispose();
|
||||
|
||||
// Assert - no exception should be thrown
|
||||
// Additional assertions could check if resources were properly cleaned up
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user