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.

November 11, 2025
•
NordVarg Team
•

Building a Trading DSL: From Grammar to Execution

Programming LanguagesDSLtradingpythonlarkcompilerstype-systems
10 min read
Share:

Hardcoding trading strategies in C++ or Python is fine for developers, but what if you want to let quantitative researchers or even non-technical traders define logic? You need a Domain-Specific Language (DSL).

A DSL allows you to express trading logic concisely and safely. In this guide, we'll build a "Mini-Quant" language that can parse and execute rules like:

1IF RSI(14) > 70 AND PRICE > SMA(50) THEN SELL 100
2

1. Choosing the Tool: Python & Lark#

For this example, we'll use Lark, a modern parsing library for Python. It's fast, easy to use, and supports EBNF grammars.

The Complete Grammar#

First, we define the grammar. This tells the parser how to understand our language.

python
1from lark import Lark, Transformer, v_args
2
3grammar = """
4    ?start: rule
5
6    rule: "IF" condition "THEN" action
7
8    ?condition: expression comparison expression
9              | condition "AND" condition -> and_cond
10              | condition "OR" condition  -> or_cond
11              | "(" condition ")"
12
13    ?expression: term
14               | expression "+" term      -> add
15               | expression "-" term      -> sub
16
17    ?term: NUMBER                         -> number
18         | "PRICE"                        -> price
19         | function_call
20
21    function_call: NAME "(" NUMBER ")"
22
23    ?comparison: ">" -> gt
24               | "<" -> lt
25               | "==" -> eq
26               | ">=" -> gte
27               | "<=" -> lte
28
29    action: "BUY" NUMBER                  -> buy
30          | "SELL" NUMBER                 -> sell
31          | "HOLD"                        -> hold
32
33    NAME: /[A-Z]+/
34    %import common.NUMBER
35    %import common.WS
36    %ignore WS
37"""
38
39parser = Lark(grammar, start='start', parser='lalr')
40

2. The Abstract Syntax Tree (AST)#

When we parse a string, Lark creates a Tree. Let's see what our strategy looks like as an AST.

python
1strategy = "IF RSI(14) > 70 THEN SELL 100"
2tree = parser.parse(strategy)
3print(tree.pretty())
4

Output:

1rule
2  gt
3    function_call
4      RSI
5      14
6    number  70
7  sell  100
8

This tree structure is easy for a computer to traverse and execute.

3. Error Handling: Graceful Failures#

Production DSLs need robust error handling. Let's add syntax and semantic error detection.

Syntax Errors#

python
1from lark.exceptions import LarkError, UnexpectedInput, UnexpectedToken
2
3def parse_with_errors(strategy_text: str):
4    try:
5        tree = parser.parse(strategy_text)
6        return tree, None
7    except UnexpectedToken as e:
8        error_msg = f"Syntax error at line {e.line}, column {e.column}:\n"
9        error_msg += f"  Unexpected token: '{e.token}'\n"
10        error_msg += f"  Expected one of: {', '.join(e.expected)}"
11        return None, error_msg
12    except UnexpectedInput as e:
13        error_msg = f"Syntax error: {str(e)}"
14        return None, error_msg
15    except LarkError as e:
16        error_msg = f"Parse error: {str(e)}"
17        return None, error_msg
18
19# Test with invalid syntax
20invalid_strategy = "IF RSI(14) > 70 THEN"  # Missing action
21tree, error = parse_with_errors(invalid_strategy)
22if error:
23    print(f"Error: {error}")
24

Output:

Error: Syntax error at line 1, column 20: Unexpected token: Token('$END', '') Expected one of: BUY, SELL, HOLD

Semantic Errors#

python
1class SemanticError(Exception):
2    pass
3
4class SemanticValidator(Transformer):
5    """Validate semantic correctness of the strategy."""
6    
7    VALID_INDICATORS = {'RSI', 'SMA', 'EMA', 'MACD', 'BBANDS'}
8    
9    def function_call(self, items):
10        func_name = items[0].value
11        period = int(items[1])
12        
13        # Validate function name
14        if func_name not in self.VALID_INDICATORS:
15            raise SemanticError(
16                f"Unknown indicator: {func_name}. "
17                f"Valid indicators: {', '.join(self.VALID_INDICATORS)}"
18            )
19        
20        # Validate period range
21        if period < 1 or period > 200:
22            raise SemanticError(
23                f"Invalid period {period} for {func_name}. "
24                f"Period must be between 1 and 200."
25            )
26        
27        return ('function_call', func_name, period)
28    
29    def buy(self, items):
30        quantity = int(items[0])
31        if quantity <= 0:
32            raise SemanticError(f"Buy quantity must be positive, got {quantity}")
33        return ('buy', quantity)
34    
35    def sell(self, items):
36        quantity = int(items[0])
37        if quantity <= 0:
38            raise SemanticError(f"Sell quantity must be positive, got {quantity}")
39        return ('sell', quantity)
40
41# Test semantic validation
42try:
43    tree = parser.parse("IF INVALID(14) > 70 THEN BUY 100")
44    validator = SemanticValidator()
45    validator.transform(tree)
46except SemanticError as e:
47    print(f"Semantic Error: {e}")
48

4. Type System: Static Type Checking#

Let's add a type system to catch errors at parse time.

python
1from enum import Enum
2from typing import Union
3
4class Type(Enum):
5    NUMBER = "number"
6    BOOLEAN = "boolean"
7    ACTION = "action"
8
9class TypeChecker(Transformer):
10    """Type checker for the DSL."""
11    
12    def number(self, items):
13        return (Type.NUMBER, float(items[0]))
14    
15    def price(self, _):
16        return (Type.NUMBER, 'PRICE')
17    
18    def function_call(self, items):
19        # All indicators return numbers
20        return (Type.NUMBER, ('function_call', items[0].value, int(items[1])))
21    
22    def gt(self, items):
23        left_type, left_val = items[0]
24        right_type, right_val = items[1]
25        
26        if left_type != Type.NUMBER or right_type != Type.NUMBER:
27            raise TypeError(
28                f"Comparison requires numbers, got {left_type} and {right_type}"
29            )
30        
31        return (Type.BOOLEAN, ('gt', left_val, right_val))
32    
33    def lt(self, items):
34        left_type, left_val = items[0]
35        right_type, right_val = items[1]
36        
37        if left_type != Type.NUMBER or right_type != Type.NUMBER:
38            raise TypeError(
39                f"Comparison requires numbers, got {left_type} and {right_type}"
40            )
41        
42        return (Type.BOOLEAN, ('lt', left_val, right_val))
43    
44    def and_cond(self, items):
45        left_type, left_val = items[0]
46        right_type, right_val = items[1]
47        
48        if left_type != Type.BOOLEAN or right_type != Type.BOOLEAN:
49            raise TypeError(
50                f"AND requires booleans, got {left_type} and {right_type}"
51            )
52        
53        return (Type.BOOLEAN, ('and', left_val, right_val))
54    
55    def rule(self, items):
56        cond_type, cond_val = items[0]
57        action_type, action_val = items[1]
58        
59        if cond_type != Type.BOOLEAN:
60            raise TypeError(f"Condition must be boolean, got {cond_type}")
61        
62        if action_type != Type.ACTION:
63            raise TypeError(f"Action required, got {action_type}")
64        
65        return ('rule', cond_val, action_val)
66    
67    def buy(self, items):
68        return (Type.ACTION, ('buy', int(items[0])))
69    
70    def sell(self, items):
71        return (Type.ACTION, ('sell', int(items[0])))
72
73# Test type checking
74try:
75    tree = parser.parse("IF RSI(14) THEN BUY 100")  # Missing comparison
76    type_checker = TypeChecker()
77    type_checker.transform(tree)
78except TypeError as e:
79    print(f"Type Error: {e}")
80

5. Execution: The Interpreter Pattern#

Now we need to run this logic. We can use a Transformer to walk the tree and evaluate it against market data.

python
1class StrategyExecutor(Transformer):
2    def __init__(self, market_data):
3        self.data = market_data
4        self.indicators = {}  # Cache computed indicators
5
6    def number(self, items):
7        return float(items[0])
8
9    def price(self, _):
10        return self.data['close']
11
12    def function_call(self, items):
13        func_name = items[0].value
14        period = int(items[1])
15        
16        # Cache key
17        cache_key = f"{func_name}_{period}"
18        if cache_key in self.indicators:
19            return self.indicators[cache_key]
20        
21        # Calculate indicator
22        if func_name == 'RSI':
23            value = self.calculate_rsi(period)
24        elif func_name == 'SMA':
25            value = self.calculate_sma(period)
26        elif func_name == 'EMA':
27            value = self.calculate_ema(period)
28        else:
29            raise ValueError(f"Unknown function: {func_name}")
30        
31        self.indicators[cache_key] = value
32        return value
33
34    def gt(self, items):
35        return items[0] > items[1]
36    
37    def lt(self, items):
38        return items[0] < items[1]
39    
40    def and_cond(self, items):
41        return items[0] and items[1]
42    
43    def or_cond(self, items):
44        return items[0] or items[1]
45
46    def rule(self, items):
47        condition, action = items
48        if condition:
49            return action
50        return "HOLD"
51
52    def buy(self, items):
53        return f"ORDER: BUY {int(items[0])}"
54    
55    def sell(self, items):
56        return f"ORDER: SELL {int(items[0])}"
57
58    def calculate_rsi(self, period):
59        """Calculate RSI indicator."""
60        prices = self.data['history'][-period-1:]
61        gains = [max(prices[i] - prices[i-1], 0) for i in range(1, len(prices))]
62        losses = [max(prices[i-1] - prices[i], 0) for i in range(1, len(prices))]
63        
64        avg_gain = sum(gains) / period
65        avg_loss = sum(losses) / period
66        
67        if avg_loss == 0:
68            return 100
69        
70        rs = avg_gain / avg_loss
71        rsi = 100 - (100 / (1 + rs))
72        return rsi
73    
74    def calculate_sma(self, period):
75        """Calculate Simple Moving Average."""
76        prices = self.data['history'][-period:]
77        return sum(prices) / period
78    
79    def calculate_ema(self, period):
80        """Calculate Exponential Moving Average."""
81        prices = self.data['history'][-period:]
82        multiplier = 2 / (period + 1)
83        ema = prices[0]
84        for price in prices[1:]:
85            ema = (price - ema) * multiplier + ema
86        return ema
87
88### Running It
89
90```python
91import numpy as np
92
93# Simulate market data
94market_state = {
95    'close': 150.0,
96    'history': list(100 + np.cumsum(np.random.randn(100) * 0.5))
97}
98
99strategy_text = "IF RSI(14) > 70 AND PRICE > SMA(50) THEN SELL 100"
100tree = parser.parse(strategy_text)
101
102executor = StrategyExecutor(market_state)
103result = executor.transform(tree)
104print(result)  # Output: ORDER: SELL 100 (if conditions met)
105

6. Transpiler: Generating C++ Code#

The Python interpreter approach above is great for backtesting and research. It's flexible and easy to debug.

However, for High-Frequency Trading (HFT), walking a tree in Python is too slow.

The Transpiler Approach#

Instead of executing the AST in Python, we can transpile (translate) it into C++ code.

python
1class CppCodeGenerator(Transformer):
2    """Generate C++ code from the AST."""
3    
4    def number(self, items):
5        return str(items[0])
6    
7    def price(self, _):
8        return "ctx.price"
9    
10    def function_call(self, items):
11        func_name = items[0].value.lower()
12        period = items[1]
13        return f"ctx.indicators.{func_name}({period})"
14    
15    def gt(self, items):
16        return f"({items[0]} > {items[1]})"
17    
18    def lt(self, items):
19        return f"({items[0]} < {items[1]})"
20    
21    def and_cond(self, items):
22        return f"({items[0]} && {items[1]})"
23    
24    def or_cond(self, items):
25        return f"({items[0]} || {items[1]})"
26    
27    def buy(self, items):
28        return f"ctx.order_manager.send(Side::BUY, {items[0]})"
29    
30    def sell(self, items):
31        return f"ctx.order_manager.send(Side::SELL, {items[0]})"
32    
33    def rule(self, items):
34        condition = items[0]
35        action = items[1]
36        
37        cpp_code = f"""
38// Generated strategy code
39void execute_strategy(TradingContext& ctx) {{
40    if {condition} {{
41        {action};
42    }}
43}}
44"""
45        return cpp_code
46
47# Generate C++ code
48strategy_text = "IF PRICE > SMA(50) THEN BUY 100"
49tree = parser.parse(strategy_text)
50generator = CppCodeGenerator()
51cpp_code = generator.transform(tree)
52print(cpp_code)
53

Output:

cpp
1// Generated strategy code
2void execute_strategy(TradingContext& ctx) {
3    if (ctx.price > ctx.indicators.sma(50)) {
4        ctx.order_manager.send(Side::BUY, 100);
5    }
6}
7

This generated code is then compiled with g++ -O3 into a shared object (.so) and loaded dynamically by the trading engine. This gives you the best of both worlds:

  1. Ease of use: Traders write simple DSL rules.
  2. Performance: The machine executes optimized machine code.

7. Testing the DSL#

Testing a DSL requires testing both the parser and the execution logic.

python
1import unittest
2
3class TestTradingDSL(unittest.TestCase):
4    
5    def test_parse_simple_rule(self):
6        """Test parsing a simple rule."""
7        tree = parser.parse("IF PRICE > 100 THEN BUY 50")
8        self.assertIsNotNone(tree)
9    
10    def test_syntax_error(self):
11        """Test syntax error detection."""
12        tree, error = parse_with_errors("IF PRICE > THEN BUY 50")
13        self.assertIsNone(tree)
14        self.assertIn("Syntax error", error)
15    
16    def test_semantic_error_invalid_indicator(self):
17        """Test semantic error for invalid indicator."""
18        with self.assertRaises(SemanticError):
19            tree = parser.parse("IF INVALID(14) > 70 THEN BUY 100")
20            SemanticValidator().transform(tree)
21    
22    def test_type_error(self):
23        """Test type checking."""
24        with self.assertRaises(TypeError):
25            tree = parser.parse("IF RSI(14) THEN BUY 100")  # Missing comparison
26            TypeChecker().transform(tree)
27    
28    def test_execution(self):
29        """Test strategy execution."""
30        market_data = {
31            'close': 150.0,
32            'history': [100 + i * 0.5 for i in range(100)]
33        }
34        
35        tree = parser.parse("IF PRICE > 100 THEN BUY 50")
36        executor = StrategyExecutor(market_data)
37        result = executor.transform(tree)
38        
39        self.assertEqual(result, "ORDER: BUY 50")
40
41if __name__ == '__main__':
42    unittest.main()
43

8. Security: Sandboxing and Resource Limits#

When allowing users to write DSL code, security is paramount.

Resource Limits#

python
1import signal
2from contextlib import contextmanager
3
4class TimeoutError(Exception):
5    pass
6
7@contextmanager
8def timeout(seconds):
9    """Context manager for timeout."""
10    def timeout_handler(signum, frame):
11        raise TimeoutError(f"Execution exceeded {seconds} seconds")
12    
13    # Set the signal handler
14    signal.signal(signal.SIGALRM, timeout_handler)
15    signal.alarm(seconds)
16    
17    try:
18        yield
19    finally:
20        signal.alarm(0)
21
22# Use timeout to prevent infinite loops
23try:
24    with timeout(5):
25        # Execute user strategy
26        tree = parser.parse(user_strategy)
27        executor = StrategyExecutor(market_data)
28        result = executor.transform(tree)
29except TimeoutError as e:
30    print(f"Strategy execution timeout: {e}")
31

Preventing Malicious Code#

  1. Whitelist Functions: Only allow predefined indicators
  2. No External Calls: DSL cannot make network requests or file I/O
  3. Memory Limits: Limit indicator history size
  4. Rate Limiting: Limit strategy execution frequency
python
1class SecureStrategyExecutor(StrategyExecutor):
2    MAX_HISTORY_SIZE = 1000
3    MAX_INDICATOR_PERIOD = 200
4    
5    def __init__(self, market_data):
6        # Limit history size
7        if len(market_data['history']) > self.MAX_HISTORY_SIZE:
8            market_data['history'] = market_data['history'][-self.MAX_HISTORY_SIZE:]
9        
10        super().__init__(market_data)
11    
12    def function_call(self, items):
13        period = int(items[1])
14        
15        # Enforce period limits
16        if period > self.MAX_INDICATOR_PERIOD:
17            raise ValueError(
18                f"Indicator period {period} exceeds maximum {self.MAX_INDICATOR_PERIOD}"
19            )
20        
21        return super().function_call(items)
22

Conclusion#

Building a DSL sounds daunting, but modern tools like Lark make it accessible. By separating the definition of a strategy from its execution, you empower your trading team to iterate faster while keeping your core engine robust and performant.

Key Takeaways:

  1. Grammar Design: Keep it simple and expressive
  2. Error Handling: Provide clear, actionable error messages
  3. Type System: Catch errors at parse time, not runtime
  4. Performance: Transpile to C++ for production HFT systems
  5. Security: Sandbox execution and enforce resource limits
  6. Testing: Test both parsing and execution thoroughly

Whether you're building a backtesting DSL or a production trading language, these principles will guide you to a robust, maintainable system.

NT

NordVarg Team

Technical Writer

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

DSLtradingpythonlarkcompilers

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•10 min read
Zig for Fintech: Performance, Safety, and C Interop
Programming Languageszigsystems-programming
Nov 28, 2025•6 min read
ReasonML and Melange: Type-Safe React Development with OCaml
Programming Languagesreasonmlmelange
Nov 24, 2024•10 min read
Factor Models in Production: From Research to Live Trading
Quantitative Financefactor-investingquantitative-finance

Interested in working together?