""" Pine Script v5 Syntax Validator. Validates generated Pine Script code for: - Version declaration - Required structure (strategy or indicator) - Bracket/parenthesis matching - Known function signatures - Common mistakes and deprecated syntax """ from __future__ import annotations import logging import re from dataclasses import dataclass from typing import Any, Dict, List logger = logging.getLogger(__name__) @dataclass class ValidationResult: valid: bool errors: List[Dict[str, Any]] warnings: List[Dict[str, Any]] stats: Dict[str, int] # Pine Script v5 built-in functions / namespaces VALID_NAMESPACES = { "ta", "math", "strategy", "input", "color", "plot", "hline", "plotshape", "plotchar", "fill", "bgcolor", "barcolor", "request", "str", "array", "matrix", "table", "line", "box", "label", "syminfo", "time", "timeframe", "chart", } VALID_TA_FUNCTIONS = { "sma", "ema", "wma", "rma", "vwma", "swma", "alma", "rsi", "macd", "stoch", "cci", "atr", "tr", "highest", "lowest", "highestbars", "lowestbars", "crossover", "crossunder", "cross", "change", "mom", "roc", "percentrank", "supertrend", "pivot_point_levels", "bb", "kc", "vwap", "stdev", "variance", "correlation", "barssince", "valuewhen", "cum", "rising", "falling", "dmi", "mfi", "obv", } DEPRECATED_V4 = { "study": "indicator", "security": "request.security", "input": "input.int/input.float/input.bool/input.string", "iff": "condition ? value1 : value2", } def validate_pine_script(code: str) -> Dict[str, Any]: """ Validate Pine Script v5 code and return errors/warnings. """ errors: List[Dict[str, Any]] = [] warnings: List[Dict[str, Any]] = [] lines = code.strip().split("\n") stats = { "total_lines": len(lines), "code_lines": 0, "comment_lines": 0, } if not code.strip(): errors.append({"line": 0, "message": "Empty code"}) return ValidationResult( valid=False, errors=errors, warnings=warnings, stats=stats ).__dict__ # 1. Version declaration check has_version = False for i, line in enumerate(lines): stripped = line.strip() if stripped.startswith("//"): stats["comment_lines"] += 1 if stripped.startswith("//@version=5"): has_version = True elif stripped: stats["code_lines"] += 1 if not has_version: errors.append({ "line": 1, "message": "Missing Pine Script v5 version declaration: //@version=5", "severity": "error", }) # 2. Strategy or indicator declaration has_strategy = bool(re.search(r'\bstrategy\s*\(', code)) has_indicator = bool(re.search(r'\bindicator\s*\(', code)) if not has_strategy and not has_indicator: errors.append({ "line": 0, "message": "Missing strategy() or indicator() declaration", "severity": "error", }) # 3. Bracket matching open_parens = 0 open_brackets = 0 for i, line in enumerate(lines, 1): stripped = line.strip() if stripped.startswith("//"): continue open_parens += stripped.count("(") - stripped.count(")") open_brackets += stripped.count("[") - stripped.count("]") if open_parens != 0: errors.append({ "line": 0, "message": f"Unmatched parentheses: {'extra (' if open_parens > 0 else 'extra )'}", "severity": "error", }) if open_brackets != 0: errors.append({ "line": 0, "message": f"Unmatched brackets: {'extra [' if open_brackets > 0 else 'extra ]'}", "severity": "error", }) # 4. Deprecated v4 syntax for deprecated, replacement in DEPRECATED_V4.items(): pattern = rf'\b{deprecated}\s*\(' match = re.search(pattern, code) if match: # Find line number pos = match.start() line_num = code[:pos].count("\n") + 1 # study() is a special case — it was renamed to indicator() if deprecated == "study": warnings.append({ "line": line_num, "message": f"Deprecated: '{deprecated}()' was renamed to '{replacement}()' in v5", "severity": "warning", }) elif deprecated == "security": warnings.append({ "line": line_num, "message": f"Deprecated: '{deprecated}()' should be '{replacement}()' in v5", "severity": "warning", }) # 5. Strategy without entry if has_strategy: if "strategy.entry" not in code: warnings.append({ "line": 0, "message": "Strategy has no strategy.entry() calls — no trades will be executed", "severity": "warning", }) # 6. Check for common mistakes if "var " in code: # var is valid but sometimes misused pass # Missing quotes around strings name_match = re.search(r'strategy\(\s*([^"\'])', code) if name_match: pos = name_match.start() line_num = code[:pos].count("\n") + 1 warnings.append({ "line": line_num, "message": "Strategy name should be a quoted string", "severity": "warning", }) is_valid = len(errors) == 0 return { "valid": is_valid, "errors": [{"line": e["line"], "message": e["message"], "severity": e.get("severity", "error")} for e in errors], "warnings": [{"line": w["line"], "message": w["message"], "severity": w.get("severity", "warning")} for w in warnings], "stats": stats, }