Spaces:
Sleeping
Sleeping
| // tests/test_risk_manager.cpp | |
| // Unit tests for the risk management gateway | |
| using namespace hft; | |
| static int tests_passed = 0; | |
| static int tests_failed = 0; | |
| static Order make_order(const std::string& sym, Side side, OrderType type, | |
| double price, Quantity qty) { | |
| Order o; | |
| o.id = 1; o.symbol = sym; o.side = side; o.type = type; | |
| o.price = to_price(price); o.qty = qty; | |
| return o; | |
| } | |
| // βββ Basic Approval ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| TEST(test_valid_order_approved) { | |
| RiskLimits limits; | |
| limits.max_order_qty = 1000; | |
| limits.max_position = 10000; | |
| limits.max_notional_usd = 1000000; | |
| limits.max_orders_per_sec = 100; | |
| RiskManager rm(limits); | |
| auto o = make_order("AAPL", Side::BUY, OrderType::LIMIT, 185.0, 100); | |
| auto result = rm.check_order(o); | |
| ASSERT_TRUE(result.approved); | |
| ASSERT_EQ(rm.orders_checked(), 1); | |
| ASSERT_EQ(rm.orders_rejected(), 0); | |
| PASS(); | |
| } | |
| // βββ Rejection Tests βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| TEST(test_reject_qty_too_large) { | |
| RiskLimits limits; | |
| limits.max_order_qty = 100; | |
| RiskManager rm(limits); | |
| auto o = make_order("AAPL", Side::BUY, OrderType::LIMIT, 185.0, 500); | |
| auto result = rm.check_order(o); | |
| ASSERT_TRUE(!result.approved); | |
| ASSERT_EQ(result.reason, RiskRejectReason::MAX_ORDER_QTY); | |
| PASS(); | |
| } | |
| TEST(test_reject_zero_qty) { | |
| RiskLimits limits; | |
| RiskManager rm(limits); | |
| auto o = make_order("AAPL", Side::BUY, OrderType::LIMIT, 185.0, 0); | |
| auto result = rm.check_order(o); | |
| ASSERT_TRUE(!result.approved); | |
| ASSERT_EQ(result.reason, RiskRejectReason::INVALID_QTY); | |
| PASS(); | |
| } | |
| TEST(test_reject_negative_qty) { | |
| RiskLimits limits; | |
| RiskManager rm(limits); | |
| auto o = make_order("AAPL", Side::BUY, OrderType::LIMIT, 185.0, -10); | |
| auto result = rm.check_order(o); | |
| ASSERT_TRUE(!result.approved); | |
| ASSERT_EQ(result.reason, RiskRejectReason::INVALID_QTY); | |
| PASS(); | |
| } | |
| TEST(test_reject_negative_limit_price) { | |
| RiskLimits limits; | |
| RiskManager rm(limits); | |
| auto o = make_order("AAPL", Side::BUY, OrderType::LIMIT, -5.0, 100); | |
| auto result = rm.check_order(o); | |
| ASSERT_TRUE(!result.approved); | |
| ASSERT_EQ(result.reason, RiskRejectReason::INVALID_PRICE); | |
| PASS(); | |
| } | |
| TEST(test_reject_notional_too_large) { | |
| RiskLimits limits; | |
| limits.max_order_qty = 100000; | |
| limits.max_notional_usd = 1000.0; // very low limit | |
| RiskManager rm(limits); | |
| auto o = make_order("AAPL", Side::BUY, OrderType::LIMIT, 185.0, 100); | |
| // notional = 185 * 100 = 18500 > 1000 | |
| auto result = rm.check_order(o); | |
| ASSERT_TRUE(!result.approved); | |
| ASSERT_EQ(result.reason, RiskRejectReason::MAX_NOTIONAL); | |
| PASS(); | |
| } | |
| TEST(test_reject_position_limit) { | |
| RiskLimits limits; | |
| limits.max_order_qty = 10000; | |
| limits.max_position = 100; | |
| limits.max_notional_usd = 1e9; | |
| limits.max_orders_per_sec = 10000; | |
| RiskManager rm(limits); | |
| // Fill a position of 90 | |
| rm.on_fill("AAPL", Side::BUY, 90, 185.0); | |
| // Try to buy 20 more β projected = 110 > max 100 | |
| auto o = make_order("AAPL", Side::BUY, OrderType::LIMIT, 185.0, 20); | |
| auto result = rm.check_order(o); | |
| ASSERT_TRUE(!result.approved); | |
| ASSERT_EQ(result.reason, RiskRejectReason::MAX_POSITION); | |
| PASS(); | |
| } | |
| // βββ Rate Limiter ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| TEST(test_rejected_orders_dont_count_toward_rate_limit) { | |
| // This is the Bug #9 regression test | |
| RiskLimits limits; | |
| limits.max_order_qty = 100; | |
| limits.max_orders_per_sec = 5; | |
| limits.max_notional_usd = 1e9; | |
| limits.max_position = 1000000; | |
| RiskManager rm(limits); | |
| // Send 10 INVALID orders (qty too large) β these should NOT count | |
| for (int i = 0; i < 10; ++i) { | |
| auto bad = make_order("AAPL", Side::BUY, OrderType::LIMIT, 185.0, 500); | |
| rm.check_order(bad); | |
| } | |
| // Now send a valid order β it should still be approved | |
| auto good = make_order("AAPL", Side::BUY, OrderType::LIMIT, 185.0, 50); | |
| auto result = rm.check_order(good); | |
| ASSERT_TRUE(result.approved); // rate limit not exhausted by rejects | |
| PASS(); | |
| } | |
| // βββ Position & PnL Tracking βββββββββββββββββββββββββββββββββββββββββββββββββ | |
| TEST(test_position_after_fill) { | |
| RiskLimits limits; | |
| RiskManager rm(limits); | |
| rm.on_fill("AAPL", Side::BUY, 100, 185.0); | |
| auto pos = rm.get_position("AAPL"); | |
| ASSERT_EQ(pos.net_qty, 100); | |
| ASSERT_TRUE(std::abs(pos.avg_price - 185.0) < 0.01); | |
| PASS(); | |
| } | |
| TEST(test_realized_pnl) { | |
| RiskLimits limits; | |
| RiskManager rm(limits); | |
| // Buy 100 @ 185 | |
| rm.on_fill("AAPL", Side::BUY, 100, 185.0); | |
| // Sell 100 @ 190 β realized = (190 - 185) * 100 = 500 | |
| rm.on_fill("AAPL", Side::SELL, 100, 190.0); | |
| auto pos = rm.get_position("AAPL"); | |
| ASSERT_EQ(pos.net_qty, 0); | |
| ASSERT_TRUE(std::abs(pos.realized_pnl - 500.0) < 0.01); | |
| PASS(); | |
| } | |
| TEST(test_unrealized_pnl) { | |
| RiskLimits limits; | |
| RiskManager rm(limits); | |
| rm.on_fill("AAPL", Side::BUY, 100, 185.0); | |
| rm.update_market_price("AAPL", 190.0); | |
| auto pos = rm.get_position("AAPL"); | |
| // unrealized = (190 - 185) * 100 = 500 | |
| ASSERT_TRUE(std::abs(pos.unrealized_pnl - 500.0) < 0.01); | |
| PASS(); | |
| } | |
| TEST(test_disabled_risk_approves_all) { | |
| RiskLimits limits; | |
| limits.enabled = false; | |
| RiskManager rm(limits); | |
| auto o = make_order("AAPL", Side::BUY, OrderType::LIMIT, 185.0, 999999); | |
| auto result = rm.check_order(o); | |
| ASSERT_TRUE(result.approved); | |
| PASS(); | |
| } | |
| // βββ Main ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| int main() { | |
| std::cout << "\n=== Risk Manager β Unit Tests ===\n\n"; | |
| std::cout << "\nβββββββββββββββββββββββββββββββββββββββββββββ\n"; | |
| std::cout << " Passed: " << tests_passed << "\n"; | |
| std::cout << " Failed: " << tests_failed << "\n"; | |
| std::cout << "βββββββββββββββββββββββββββββββββββββββββββββ\n\n"; | |
| return tests_failed > 0 ? 1 : 0; | |
| } | |