Spaces:
Running
Running
| """ | |
| 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__) | |
| 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, | |
| } | |