riskEngine.test.ts 1.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172
  1. import { describe, expect, it } from 'vitest';
  2. import { RiskEngine } from '../packages/risk/src/riskEngine';
  3. import type { Order, PositionSnapshot } from '../packages/domain/src/types';
  4. const baseOrder: Order = {
  5. clientId: 'risk-1',
  6. symbol: 'BTC',
  7. side: 'buy',
  8. px: 100,
  9. sz: 0.5,
  10. tif: 'GTC'
  11. };
  12. const position: PositionSnapshot = {
  13. symbol: 'BTC',
  14. base: 0,
  15. quote: 1000,
  16. ts: Date.now()
  17. };
  18. describe('RiskEngine', () => {
  19. it('blocks orders that exceed size or limits', () => {
  20. const engine = new RiskEngine({
  21. maxBaseAbs: 1,
  22. maxNotionalAbs: 150,
  23. maxOrderSz: 0.6
  24. });
  25. expect(() => engine.preCheck(baseOrder, position, 100)).not.toThrow();
  26. expect(() =>
  27. engine.preCheck({ ...baseOrder, sz: 0.7 }, position, 100)
  28. ).toThrow(/order size/);
  29. expect(() =>
  30. engine.preCheck({ ...baseOrder, sz: 0.5 }, { ...position, base: 0.7 }, 100)
  31. ).toThrow(/inventory/);
  32. });
  33. it('triggers kill switch on drawdown and resets when requested', () => {
  34. const engine = new RiskEngine(
  35. {
  36. maxBaseAbs: 2,
  37. maxNotionalAbs: 1_000,
  38. maxOrderSz: 1
  39. },
  40. {
  41. drawdownPct: 0.5,
  42. triggers: [
  43. { type: 'delta_abs', threshold: 1.6 },
  44. { type: 'hedge_failure_count', threshold: 3 }
  45. ]
  46. }
  47. );
  48. engine.updateEquity(100);
  49. engine.updateEquity(45); // 55% drawdown
  50. expect(engine.shouldHalt()).toBe(true);
  51. engine.resetKillSwitch();
  52. expect(engine.shouldHalt()).toBe(false);
  53. engine.updateDeltaAbs(2);
  54. expect(engine.shouldHalt()).toBe(true);
  55. engine.resetKillSwitch();
  56. engine.recordHedgeFailure();
  57. engine.recordHedgeFailure();
  58. engine.recordHedgeFailure();
  59. expect(engine.shouldHalt()).toBe(true);
  60. });
  61. });