- 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
510 lines
16 KiB
C#
510 lines
16 KiB
C#
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
|
|
}
|
|
}
|
|
}
|