""" Tests for the strategy generation pipeline. Tests the flow: AI description → Blockly XML → JSON → Python """ import json import pytest import subprocess import xml.etree.ElementTree as ET import sys import os # Add src to path for imports sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src')) from PythonGenerator import PythonGenerator class TestStrategyBuilder: """Tests for the CmdForge strategy-builder tool.""" @staticmethod def _get_blocks(root): """Get all block elements, handling XML namespaces.""" # Blockly uses namespace https://developers.google.com/blockly/xml # Elements may be prefixed with {namespace}block or just block blocks = [] for elem in root.iter(): # Get local name without namespace tag = elem.tag.split('}')[-1] if '}' in elem.tag else elem.tag if tag == 'block': blocks.append(elem) return blocks @pytest.mark.integration def test_simple_rsi_strategy(self): """Test generating a simple RSI-based strategy.""" input_data = { "description": "Buy when RSI is below 30 and sell when RSI is above 70", "indicators": [{"name": "RSI", "outputs": ["RSI"]}], "signals": [], "default_source": {"exchange": "binance", "market": "BTC/USDT", "timeframe": "5m"} } result = subprocess.run( ['strategy-builder'], input=json.dumps(input_data), capture_output=True, text=True, timeout=120 ) assert result.returncode == 0, f"Strategy builder failed: {result.stderr}" # Validate XML structure xml_output = result.stdout.strip() root = ET.fromstring(xml_output) # Check it has the required structure root_tag = root.tag.split('}')[-1] if '}' in root.tag else root.tag assert root_tag == 'xml', f"Root should be 'xml', got {root_tag}" blocks = self._get_blocks(root) assert len(blocks) >= 2, f"Should have at least 2 blocks (buy and sell), got {len(blocks)}" # Check for execute_if blocks execute_if_blocks = [b for b in blocks if b.get('type') == 'execute_if'] assert len(execute_if_blocks) >= 2, "Should have at least 2 execute_if blocks" # Check for trade_action blocks trade_action_blocks = [b for b in blocks if b.get('type') == 'trade_action'] assert len(trade_action_blocks) >= 2, "Should have buy and sell trade actions" # Check for indicator blocks indicator_blocks = [b for b in blocks if 'indicator_RSI' in (b.get('type') or '')] assert len(indicator_blocks) >= 2, "Should use RSI indicator" @pytest.mark.integration def test_ema_crossover_strategy(self): """Test generating an EMA crossover strategy.""" input_data = { "description": "Buy when EMA 20 crosses above EMA 50, sell when EMA 20 crosses below EMA 50", "indicators": [ {"name": "EMA_20", "outputs": ["ema"]}, {"name": "EMA_50", "outputs": ["ema"]} ], "signals": [], "default_source": {"exchange": "binance", "market": "ETH/USDT", "timeframe": "1h"} } result = subprocess.run( ['strategy-builder'], input=json.dumps(input_data), capture_output=True, text=True, timeout=120 ) assert result.returncode == 0, f"Strategy builder failed: {result.stderr}" xml_output = result.stdout.strip() root = ET.fromstring(xml_output) # Check for EMA indicator blocks blocks = self._get_blocks(root) ema_20_blocks = [b for b in blocks if 'indicator_EMA_20' in (b.get('type') or '')] ema_50_blocks = [b for b in blocks if 'indicator_EMA_50' in (b.get('type') or '')] assert len(ema_20_blocks) >= 1, "Should use EMA_20 indicator" assert len(ema_50_blocks) >= 1, "Should use EMA_50 indicator" @pytest.mark.integration def test_no_indicators_price_only(self): """Test generating a price-based strategy without indicators.""" input_data = { "description": "Buy when price drops 5% from previous candle, sell when price rises 3%", "indicators": [], "signals": [], "default_source": {"exchange": "binance", "market": "BTC/USDT", "timeframe": "5m"} } result = subprocess.run( ['strategy-builder'], input=json.dumps(input_data), capture_output=True, text=True, timeout=120 ) assert result.returncode == 0, f"Strategy builder failed: {result.stderr}" xml_output = result.stdout.strip() root = ET.fromstring(xml_output) # Should be valid XML assert root is not None @pytest.mark.integration def test_missing_indicator_error(self): """Test that strategy mentioning indicators without config fails.""" input_data = { "description": "Buy when RSI is below 30", "indicators": [], # No indicators configured "signals": [], "default_source": {"exchange": "binance", "market": "BTC/USDT", "timeframe": "5m"} } result = subprocess.run( ['strategy-builder'], input=json.dumps(input_data), capture_output=True, text=True, timeout=120 ) assert result.returncode != 0, "Should fail when indicators mentioned but not configured" assert "indicator" in result.stderr.lower(), "Error should mention indicators" class TestPythonGenerator: """Tests for the PythonGenerator class.""" def test_simple_execute_if(self): """Test generating Python from a simple execute_if block.""" strategy_json = { "type": "strategy", "statements": [ { "type": "execute_if", "inputs": { "CONDITION": { "type": "comparison", "operator": ">", "inputs": { "LEFT": {"type": "current_price"}, "RIGHT": {"type": "dynamic_value", "values": [50000]} } } }, "statements": { "DO": [ { "type": "trade_action", "trade_type": "buy", "inputs": {"size": 0.01} } ] } } ] } generator = PythonGenerator( default_source={"exchange": "binance", "market": "BTC/USDT", "timeframe": "5m"}, strategy_id="test_strategy" ) result = generator.generate(strategy_json) assert "generated_code" in result code = result["generated_code"] # Check for expected code elements assert "def next():" in code assert "if " in code assert "get_current_price" in code assert "trade_order" in code def test_indicator_condition(self): """Test generating Python with indicator conditions.""" strategy_json = { "type": "strategy", "statements": [ { "type": "execute_if", "inputs": { "CONDITION": { "type": "comparison", "operator": "<", "inputs": { "LEFT": { "type": "indicator_RSI", "fields": {"OUTPUT": "RSI"} }, "RIGHT": {"type": "dynamic_value", "values": [30]} } } }, "statements": { "DO": [ { "type": "trade_action", "trade_type": "buy", "inputs": {"size": 0.1} } ] } } ] } generator = PythonGenerator( default_source={"exchange": "binance", "market": "BTC/USDT", "timeframe": "5m"}, strategy_id="test_indicator" ) result = generator.generate(strategy_json) code = result["generated_code"] # Check for indicator processing assert "process_indicator" in code assert "RSI" in code # Check indicators are tracked assert len(result["indicators"]) > 0 def test_logical_and_condition(self): """Test generating Python with logical AND conditions.""" strategy_json = { "type": "strategy", "statements": [ { "type": "execute_if", "inputs": { "CONDITION": { "type": "logical_and", "inputs": { "left": { "type": "comparison", "operator": "<", "inputs": { "LEFT": {"type": "indicator_RSI", "fields": {"OUTPUT": "RSI"}}, "RIGHT": {"type": "dynamic_value", "values": [30]} } }, "right": { "type": "flag_is_set", "flag_name": "bought", "flag_value": False } } } }, "statements": { "DO": [ {"type": "trade_action", "trade_type": "buy", "inputs": {"size": 0.01}} ] } } ] } generator = PythonGenerator( default_source={"exchange": "binance", "market": "BTC/USDT", "timeframe": "5m"}, strategy_id="test_and" ) result = generator.generate(strategy_json) code = result["generated_code"] # Check for logical AND assert " and " in code assert "flags.get" in code def test_set_flag(self): """Test generating Python for flag setting.""" strategy_json = { "type": "strategy", "statements": [ { "type": "set_flag", "flag_name": "bought", "flag_value": "True" } ] } generator = PythonGenerator( default_source={"exchange": "binance", "market": "BTC/USDT", "timeframe": "5m"}, strategy_id="test_flag" ) result = generator.generate(strategy_json) code = result["generated_code"] # Check for flag setting assert "flags['bought']" in code assert "True" in code # Check flag is tracked assert "bought" in result["flags_used"] def test_trade_action_with_options(self): """Test generating Python for trade with stop loss and take profit.""" strategy_json = { "type": "strategy", "statements": [ { "type": "trade_action", "trade_type": "buy", "inputs": {"size": 0.1}, "trade_options": [ {"type": "stop_loss", "inputs": {"stop_loss": 45000}}, {"type": "take_profit", "inputs": {"take_profit": 55000}} ] } ] } generator = PythonGenerator( default_source={"exchange": "binance", "market": "BTC/USDT", "timeframe": "5m"}, strategy_id="test_trade_options" ) result = generator.generate(strategy_json) code = result["generated_code"] # Check for trade order call assert "trade_order" in code assert "buy" in code def test_math_operation(self): """Test generating Python for math operations.""" strategy_json = { "type": "strategy", "statements": [ { "type": "execute_if", "inputs": { "CONDITION": { "type": "comparison", "operator": ">", "inputs": { "LEFT": {"type": "current_price"}, "RIGHT": { "type": "math_operation", "inputs": { "operator": "MULTIPLY", "left_operand": {"type": "indicator_SMA", "fields": {"OUTPUT": "sma"}}, "right_operand": {"type": "dynamic_value", "values": [1.02]} } } } } }, "statements": { "DO": [ {"type": "trade_action", "trade_type": "sell", "inputs": {"size": 0.01}} ] } } ] } generator = PythonGenerator( default_source={"exchange": "binance", "market": "BTC/USDT", "timeframe": "5m"}, strategy_id="test_math" ) result = generator.generate(strategy_json) code = result["generated_code"] # Check for math operation assert "*" in code # Multiply operator