← back to Zero Code Trading Bot

scripts/validate_strategy.py

83 lines

#!/usr/bin/env python3
"""
validate_strategy.py — validate a strategy.json BEFORE a bot ever runs it.

    scripts/validate_strategy.py path/to/strategy.json

Checks required keys, sane value ranges, and — critically — that every entry/exit
rule parses under the sandboxed evaluator and references only known market
variables. A strategy that fails here would silently misfire at runtime; catch it
now. Exit code 0 = valid, 1 = invalid.
"""

import ast
import json
import sys

# Kept in sync with assets/bot-template/safe_eval.py MARKET_VARS.
MARKET_VARS = ("price", "volume_usd", "liquidity_usd", "spread", "days_to_resolve")
_OK_NODES = (ast.Expression, ast.BoolOp, ast.BinOp, ast.UnaryOp, ast.Compare,
             ast.Name, ast.Constant, ast.And, ast.Or, ast.Not, ast.USub, ast.UAdd,
             ast.Add, ast.Sub, ast.Mult, ast.Div, ast.Mod, ast.Pow,
             ast.Eq, ast.NotEq, ast.Lt, ast.LtE, ast.Gt, ast.GtE, ast.Load)


def check_rule(expr):
    try:
        tree = ast.parse(expr, mode="eval")
    except SyntaxError as e:
        return f"cannot parse: {e}"
    for node in ast.walk(tree):
        if not isinstance(node, _OK_NODES):
            return f"disallowed element {type(node).__name__} in {expr!r}"
        if isinstance(node, ast.Name) and node.id not in MARKET_VARS + ("True", "False"):
            return f"unknown variable {node.id!r} in {expr!r} (allowed: {', '.join(MARKET_VARS)})"
    return None


def validate(strat):
    errs = []
    for key in ("name", "venue", "entry", "exit", "sizing", "risk"):
        if key not in strat:
            errs.append(f"missing required key: {key}")
    if strat.get("venue") not in ("polymarket", "kalshi"):
        errs.append(f"venue must be polymarket|kalshi, got {strat.get('venue')!r}")
    if strat.get("mode", "paper") not in ("paper", "live"):
        errs.append("mode must be paper|live")
    for section in ("entry", "exit"):
        for i, rule in enumerate(strat.get(section, [])):
            if "when" not in rule:
                errs.append(f"{section}[{i}] missing 'when'")
                continue
            problem = check_rule(rule["when"])
            if problem:
                errs.append(f"{section}[{i}]: {problem}")
    sizing = strat.get("sizing", {})
    if sizing.get("amount", 0) <= 0:
        errs.append("sizing.amount must be > 0")
    if sizing.get("max_position_usd", 0) < sizing.get("amount", 0):
        errs.append("sizing.max_position_usd must be >= sizing.amount")
    risk = strat.get("risk", {})
    if "max_total_exposure_usd" not in risk:
        errs.append("risk.max_total_exposure_usd is required (exposure cap)")
    return errs


def main():
    if len(sys.argv) != 2:
        sys.exit("usage: validate_strategy.py <strategy.json>")
    with open(sys.argv[1]) as f:
        strat = json.load(f)
    errs = validate(strat)
    if errs:
        print("❌ INVALID strategy:")
        for e in errs:
            print("   - " + e)
        sys.exit(1)
    print(f"✅ valid strategy: {strat['name']} ({strat['venue']}, mode={strat.get('mode','paper')})")
    print(f"   entry rules: {len(strat.get('entry', []))}  exit rules: {len(strat.get('exit', []))}")


if __name__ == "__main__":
    main()