AST-based look-ahead bias and rule validation for Backtrader strategies.
It analyzes strategy source code as text — it never imports or runs
backtrader, so it has zero runtime dependencies (stdlib ast only) and is
safe to run anywhere, including in a strategy-generation / evolution loop before
a candidate is ever backtested.
Heuristic, not a proof. This is static analysis. It flags known problematic patterns; it cannot guarantee a strategy is free of look-ahead bias. Treat a clean result as "no known red flags", not "verified correct".
| Category | Examples |
|---|---|
| Look-ahead bias | future bar index (close[1]), intrabar high[0]/low[0] in decisions, limit prices from current-bar data, SL/TP set before fill, higher-timeframe close[0], indicators that use high/low (ATR, Stochastic…), entry_price set in next() |
| Data feed limits | more than 3 feeds (data0, data1, data2) — e.g. self.data3, self.datas[3] |
| Position management | simultaneous long/short, missing if not self.position: guards, non-mutually-exclusive if/if entries |
| Order routing | orders sent to a feed other than data0 (data1/data2 are read-only context feeds) |
pip install backtrader-strategy-validatorOr from source:
pip install -e ".[dev]"Requires Python 3.9+.
from bt_validator import get_validation_summary
code = '''
class S(bt.Strategy):
def next(self):
if self.data.close[1] > self.data.close[0]: # future bar!
self.buy(data=self.data1) # order on non-data0!
'''
summary = get_validation_summary(code)
print(summary["score"]) # severity score in [0.0, 1.0]
print(summary["is_acceptable"]) # False
for issue in summary["issues"]:
print(issue["severity"], issue["type"], "line", issue["line"])from bt_validator import LookAheadDetector, DetectionCategory, Severity
detector = LookAheadDetector() # all categories
issues = detector.detect(code)
for i in issues:
print(f"{i.severity.value}: {i.message} (line {i.line})")
print(f" fix: {i.suggestion}")
print(detector.calculate_severity_score()) # 0.0 – 1.0
print(detector.is_acceptable(threshold=0.5)) # bool
print(detector.get_blocking_issues()) # errors + heavy warnings
# Restrict to specific categories:
LookAheadDetector(categories=[DetectionCategory.ORDER_ROUTING]).detect(code)from bt_validator import (
has_any_validation_errors, get_severity_score, is_strategy_acceptable,
has_lookahead_bias, has_order_routing_errors, format_issues_for_llm,
)format_issues_for_llm(issues) renders findings as plain text suitable for
feeding back into an LLM prompt (handy in a generate → validate → regenerate loop).
Each issue carries a Severity (ERROR / WARNING / INFO) and a per-type
weight. calculate_severity_score() sums weight × severity_modifier across all
issues, applies a mild multi-issue penalty (the first issue does not scale),
and caps the result at 1.0. is_acceptable(threshold) returns
score < threshold (strict), with a default threshold of 0.5.
MIT.