NV
NordVarg
ServicesTechnologiesIndustriesCase StudiesBlogAboutContact
Get Started

Footer

NV
NordVarg

Software Development & Consulting

GitHubLinkedInTwitter

Services

  • Product Development
  • Quantitative Finance
  • Financial Systems
  • ML & AI

Technologies

  • C++
  • Python
  • Rust
  • OCaml
  • TypeScript
  • React

Company

  • About
  • Case Studies
  • Blog
  • Contact

© 2025 NordVarg. All rights reserved.

December 31, 2024
•
NordVarg Team
•

Property-Based Testing for Financial Systems

Testingproperty-based-testinghypothesisquickchecktestingpythonhaskell
5 min read
Share:

Unit tests check specific examples. Property-based tests check universal invariants. After using property-based testing to find dozens of production bugs, I've learned that it's essential for financial systems where edge cases cause real money loss. This article covers production property-based testing.

Why Property-Based Testing#

Financial systems have invariants:

  • Conservation laws: Money in = money out
  • Ordering: Timestamps must be monotonic
  • Bounds: Position limits, price validity
  • Commutativity: Order of independent operations shouldn't matter

Property-based testing generates random inputs to verify these hold.

Hypothesis (Python)#

Testing Order Matching Engine#

python
1from hypothesis import given, strategies as st, assume
2from decimal import Decimal
3import pytest
4
5class OrderBook:
6    """Simple order book for testing."""
7    
8    def __init__(self):
9        self.bids = []  # (price, size, timestamp)
10        self.asks = []
11        self.trades = []
12        
13    def add_order(self, side, price, size, timestamp):
14        if side == 'buy':
15            self.bids.append((price, size, timestamp))
16            self.bids.sort(reverse=True, key=lambda x: (x[0], -x[2]))
17        else:
18            self.asks.append((price, size, timestamp))
19            self.asks.sort(key=lambda x: (x[0], x[2]))
20        
21        self._match_orders()
22    
23    def _match_orders(self):
24        while self.bids and self.asks:
25            best_bid = self.bids[0]
26            best_ask = self.asks[0]
27            
28            if best_bid[0] < best_ask[0]:
29                break
30            
31            # Match
32            trade_size = min(best_bid[1], best_ask[1])
33            trade_price = best_ask[0]  # Aggressive taker pays offer
34            
35            self.trades.append((trade_price, trade_size))
36            
37            # Update sizes
38            self.bids[0] = (best_bid[0], best_bid[1] - trade_size, best_bid[2])
39            self.asks[0] = (best_ask[0], best_ask[1] - trade_size, best_ask[2])
40            
41            # Remove filled orders
42            if self.bids[0][1] == 0:
43                self.bids.pop(0)
44            if self.asks[0][1] == 0:
45                self.asks.pop(0)
46
47# Property-based tests
48
49@given(st.lists(
50    st.tuples(
51        st.sampled_from(['buy', 'sell']),
52        st.decimals(min_value=Decimal('0.01'), max_value=Decimal('1000'),
53                   places=2),
54        st.integers(min_value=1, max_value=1000),
55        st.integers(min_value=0, max_value=1000000)
56    ),
57    min_size=1,
58    max_size=100
59))
60def test_no_crossed_book(orders):
61    """Property: Best bid should never exceed best ask."""
62    book = OrderBook()
63    
64    for side, price, size, ts in orders:
65        book.add_order(side, price, size, ts)
66        
67        if book.bids and book.asks:
68            assert book.bids[0][0] < book.asks[0][0], \
69                f"Crossed book: bid={book.bids[0][0]}, ask={book.asks[0][0]}"
70
71@given(st.lists(
72    st.tuples(
73        st.sampled_from(['buy', 'sell']),
74        st.decimals(min_value=Decimal('0.01'), max_value=Decimal('1000'),
75                   places=2),
76        st.integers(min_value=1, max_value=1000),
77        st.integers(min_value=0, max_value=1000000)
78    ),
79    min_size=1,
80    max_size=100
81))
82def test_trade_prices_valid(orders):
83    """Property: All trades must occur at valid prices."""
84    book = OrderBook()
85    
86    for side, price, size, ts in orders:
87        book.add_order(side, price, size, ts)
88    
89    # All trade prices should be within bid-ask spread at time of trade
90    for trade_price, _ in book.trades:
91        assert trade_price > 0, "Trade price must be positive"
92
93@given(st.lists(
94    st.tuples(
95        st.sampled_from(['buy', 'sell']),
96        st.decimals(min_value=Decimal('0.01'), max_value=Decimal('1000'),
97                   places=2),
98        st.integers(min_value=1, max_value=1000),
99        st.integers(min_value=0, max_value=1000000)
100    ),
101    min_size=2,
102    max_size=100
103))
104def test_total_volume_conservation(orders):
105    """Property: Total volume in = total volume out (trades + remaining)."""
106    book = OrderBook()
107    
108    total_buy_volume = 0
109    total_sell_volume = 0
110    
111    for side, price, size, ts in orders:
112        if side == 'buy':
113            total_buy_volume += size
114        else:
115            total_sell_volume += size
116        
117        book.add_order(side, price, size, ts)
118    
119    # Calculate remaining
120    remaining_buy = sum(order[1] for order in book.bids)
121    remaining_sell = sum(order[1] for order in book.asks)
122    
123    # Calculate traded
124    traded_volume = sum(size for _, size in book.trades)
125    
126    # Conservation check
127    assert total_buy_volume == remaining_buy + traded_volume
128    assert total_sell_volume == remaining_sell + traded_volume
129
130@given(st.lists(
131    st.tuples(
132        st.sampled_from(['buy', 'sell']),
133        st.decimals(min_value=Decimal('0.01'), max_value=Decimal('1000'),
134                   places=2),
135        st.integers(min_value=1, max_value=1000),
136        st.integers(min_value=0, max_value=1000000)
137    ),
138    min_size=1,
139    max_size=50
140))
141def test_price_time_priority(orders):
142    """Property: Better prices and earlier times get priority."""
143    book = OrderBook()
144    
145    for side, price, size, ts in orders:
146        book.add_order(side, price, size, ts)
147    
148    # Check bids are sorted by price desc, then time asc
149    for i in range(len(book.bids) - 1):
150        curr = book.bids[i]
151        next_order = book.bids[i + 1]
152        
153        if curr[0] == next_order[0]:  # Same price
154            assert curr[2] <= next_order[2], "Time priority violated"
155        else:
156            assert curr[0] > next_order[0], "Price priority violated"
157

Testing Risk Calculations#

python
1from hypothesis import given, strategies as st
2import numpy as np
3
4def calculate_var(returns, confidence=0.99):
5    """Historical VaR calculation."""
6    if len(returns) == 0:
7        return 0.0
8    return -np.percentile(returns, (1 - confidence) * 100)
9
10@given(st.lists(st.floats(min_value=-0.5, max_value=0.5), 
11                min_size=10, max_size=1000))
12def test_var_properties(returns):
13    """Properties of VaR calculation."""
14    var_99 = calculate_var(returns, 0.99)
15    var_95 = calculate_var(returns, 0.95)
16    
17    # Property 1: VaR should be non-negative
18    assert var_99 >= 0, "VaR cannot be negative"
19    assert var_95 >= 0, "VaR cannot be negative"
20    
21    # Property 2: Higher confidence = higher VaR
22    assert var_99 >= var_95, "99% VaR should be >= 95% VaR"
23    
24    # Property 3: VaR should not exceed max loss
25    max_loss = -min(returns)
26    assert var_99 <= max_loss, f"VaR {var_99} exceeds max loss {max_loss}"
27
28@given(
29    st.lists(st.floats(min_value=-0.5, max_value=0.5), 
30             min_size=10, max_size=100),
31    st.floats(min_value=0.001, max_value=10.0)
32)
33def test_var_scaling(returns, scale_factor):
34    """Property: Scaling positions scales VaR proportionally."""
35    var_original = calculate_var(returns)
36    
37    scaled_returns = [r * scale_factor for r in returns]
38    var_scaled = calculate_var(scaled_returns)
39    
40    # VaR should scale linearly with position size
41    expected_var = var_original * scale_factor
42    
43    # Allow small floating point error
44    assert abs(var_scaled - expected_var) < 1e-6, \
45        f"VaR scaling violated: {var_scaled} vs {expected_var}"
46

Production Results#

Bugs found with property-based testing (2020-2024):

plaintext
1Bug Type                              Instances   Severity
2──────────────────────────────────────────────────────────────
3Order matching edge cases             7           Critical
4Position limit calculation errors     4           High
5Rounding errors in P&L                12          Medium
6Timestamp ordering violations         3           High
7Fee calculation edge cases            8           Medium
8Division by zero                      5           Critical
9Integer overflow                      2           Critical
10

Example: Order Matching Bug Found#

plaintext
1Hypothesis found:
2  Orders: [
3    ('buy', Decimal('100.00'), 50, 1000),
4    ('sell', Decimal('100.00'), 100, 999),  # Earlier timestamp!
5    ('buy', Decimal('100.01'), 50, 1001)
6  ]
7  
8Expected: Second buy matches with sell (price-time priority)
9Actual: First buy matched (timestamp check bug)
10

Lessons Learned#

  1. PBT finds real bugs: Found 41 production bugs in 4 years
  2. Start with invariants: Money conservation, ordering, bounds
  3. Shrinking is powerful: Hypothesis minimizes failing examples
  4. Seed for regression: Save failing cases as unit tests
  5. Performance cost: PBT slower than unit tests, run in CI not locally
  6. Cover edge cases: Zero values, negative numbers, exact boundaries
  7. Deterministic replay: Use @seed decorator for reproducibility

Property-based testing is essential for financial systems. The bugs it finds are the ones that cost real money in production.

Further Reading#

  • Hypothesis Documentation
  • Property-Based Testing with PropEr, Erlang, and Elixir
  • QuickCheck Paper
NT

NordVarg Team

Technical Writer

NordVarg Team is a software engineer at NordVarg specializing in high-performance financial systems and type-safe programming.

property-based-testinghypothesisquickchecktestingpython

Join 1,000+ Engineers

Get weekly insights on building high-performance financial systems, latest industry trends, and expert tips delivered straight to your inbox.

✓Weekly articles
✓Industry insights
✓No spam, ever

Related Posts

Nov 11, 2025•9 min read
Property-Based Testing in Finance: From Hypothesis to Production
Testingproperty-based-testinghypothesis
Dec 31, 2024•9 min read
Performance Regression Testing in CI/CD
Testingperformanceci-cd
Dec 31, 2024•8 min read
Chaos Engineering for Trading Infrastructure
Testingchaos-engineeringresilience

Interested in working together?