93 lines
3.5 KiB
Python
93 lines
3.5 KiB
Python
"""Minimal RegFlow implementation for tests.
|
|
|
|
Provides a tiny DSL compiler and a per-trade proof generator sufficient
|
|
for the unit tests in tests/test_core.py.
|
|
"""
|
|
|
|
from typing import Dict, List, Any
|
|
|
|
|
|
def _parse_line_to_rule(tokens: List[str]) -> Dict[str, Any]:
|
|
# Expected formats:
|
|
# constraint max_position <venue> <instrument> <limit>
|
|
# constraint min_cash <venue> <limit>
|
|
if len(tokens) < 2:
|
|
raise ValueError("Invalid constraint line: need at least type")
|
|
rtype = tokens[1]
|
|
if rtype == "max_position":
|
|
# tokens: ["constraint", "max_position", venue, instrument, limit]
|
|
if len(tokens) != 5:
|
|
raise ValueError("Invalid max_position constraint: need venue, instrument, limit")
|
|
venue, instrument, limit = tokens[2], tokens[3], int(tokens[4])
|
|
return {"type": "max_position", "venue": venue, "instrument": instrument, "limit": limit}
|
|
elif rtype == "min_cash":
|
|
# tokens: ["constraint", "min_cash", venue, limit]
|
|
if len(tokens) != 4:
|
|
raise ValueError("Invalid min_cash constraint: need venue, limit")
|
|
venue, limit = tokens[2], int(tokens[3])
|
|
return {"type": "min_cash", "venue": venue, "limit": limit}
|
|
else:
|
|
# Unknown constraint type; store generically
|
|
return {"type": rtype, "raw": tokens[2:]}
|
|
|
|
|
|
def compile_dsl(dsl: str) -> Dict[str, Any]:
|
|
"""Compile a tiny DSL into a canonical IR representation.
|
|
|
|
The DSL supports lines like:
|
|
constraint max_position venue AAPL 1000
|
|
constraint min_cash venue 5000
|
|
Returns a dict with a single key 'rules' containing a list of rule dicts.
|
|
"""
|
|
rules: List[Dict[str, Any]] = []
|
|
for raw_line in dsl.strip().splitlines():
|
|
line = raw_line.strip()
|
|
if not line or line.startswith("#"): # skip empty or comments
|
|
continue
|
|
tokens = line.split()
|
|
if not tokens:
|
|
continue
|
|
if tokens[0] != "constraint":
|
|
# ignore non-constraint lines for tests
|
|
continue
|
|
rule = _parse_line_to_rule(tokens)
|
|
rules.append(rule)
|
|
return {"rules": rules}
|
|
|
|
|
|
def _evaluate_rule(rule: Dict[str, Any], trade: Dict[str, Any]) -> Dict[str, Any]:
|
|
venue = trade.get("venue")
|
|
if rule.get("venue") is not None and venue != rule["venue"]:
|
|
return {"ok": False, "rule": rule, "actual": None}
|
|
|
|
if rule["type"] == "max_position":
|
|
actual = trade.get("qty")
|
|
ok = actual is not None and actual <= rule["limit"]
|
|
return {"ok": ok, "rule": rule, "actual": actual}
|
|
if rule["type"] == "min_cash":
|
|
actual = trade.get("cash")
|
|
ok = actual is not None and actual >= rule["limit"]
|
|
return {"ok": ok, "rule": rule, "actual": actual}
|
|
# Fallback: unknown rule type treated as OK (not used by tests)
|
|
return {"ok": True, "rule": rule, "actual": None}
|
|
|
|
|
|
def generate_proof(ir: Dict[str, Any], trade: Dict[str, Any]) -> Dict[str, Any]:
|
|
"""Generate a simple per-trade proof against the IR.
|
|
|
|
Returns a dict with:
|
|
- valid: bool
|
|
- summary: str
|
|
- details: list of {ok: bool, rule: dict, actual: Any}
|
|
"""
|
|
rules = ir.get("rules", [])
|
|
details = []
|
|
all_ok = True
|
|
for rule in rules:
|
|
result = _evaluate_rule(rule, trade)
|
|
details.append(result)
|
|
if not result["ok"]:
|
|
all_ok = False
|
|
summary = "all applicable constraints satisfied" if all_ok else "some applicable constraints violated"
|
|
return {"valid": all_ok, "summary": summary, "details": details}
|