Skip to content

Commit 55d72de

Browse files
SL-Marclaude
andcommitted
Add QC011 (IndicatorBase[float]) + fix QC009 self.var loop pattern
QC011: IndicatorBase[float] crashes C#-Python generic interop. Auto-fix to IndicatorBase[IndicatorDataPoint]. Common LLM mistake when creating custom indicators. QC009: Now handles self.var = ["EURUSD", ...] + for x in self.var pattern (previously only caught bare variable lists). 58 tests passing (4 new). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 4ca2988 commit 55d72de

2 files changed

Lines changed: 67 additions & 4 deletions

File tree

quantcoder/core/qc_linter.py

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919

2020
@dataclass
2121
class LintIssue:
22-
rule_id: str # "QC001"–"QC010"
22+
rule_id: str # "QC001"–"QC011"
2323
line: int
2424
message: str
2525
severity: str # "error" | "warning"
@@ -447,14 +447,15 @@ def _classify_tickers(tickers: list) -> str:
447447
return "equity"
448448

449449

450-
# Pattern: var = ["EURUSD", "GBPJPY", ...] (list of string literals)
450+
# Pattern: var = ["EURUSD", "GBPJPY", ...] or self.var = [...] (list of string literals)
451451
_TICKER_LIST = re.compile(
452-
r'(\w+)\s*=\s*\[((?:\s*["\'][A-Z]{3,10}(?:/[A-Z]{2,5})?["\']\s*,?\s*)+)\]'
452+
r'(?:self\.)?(\w+)\s*=\s*\[((?:\s*["\'][A-Z]{3,10}(?:/[A-Z]{2,5})?["\']\s*,?\s*)+)\]'
453453
)
454454

455455
# Pattern: for var in list_var: ... self.add_equity(var
456+
# Also match: for var in self.list_var: ... self.add_equity(var
456457
_LOOP_ADD_EQUITY = re.compile(
457-
r'for\s+(\w+)\s+in\s+(\w+)\s*:.*?self\.add_equity\s*\(\s*\1\b',
458+
r'for\s+(\w+)\s+in\s+(?:self\.)?(\w+)\s*:.*?self\.add_equity\s*\(\s*\1\b',
458459
re.DOTALL,
459460
)
460461

@@ -550,6 +551,26 @@ def _rule_qc008(code: str, issues: List[LintIssue]) -> str:
550551
return code
551552

552553

554+
# ---------------------------------------------------------------------------
555+
# QC011 — IndicatorBase[float] → IndicatorBase[IndicatorDataPoint]
556+
# ---------------------------------------------------------------------------
557+
558+
def _rule_qc011(code: str, issues: List[LintIssue]) -> str:
559+
"""Fix IndicatorBase[float] which crashes C#-Python generic interop."""
560+
pattern = re.compile(r'IndicatorBase\s*\[\s*float\s*\]')
561+
for m in pattern.finditer(code):
562+
lineno = code[:m.start()].count('\n') + 1
563+
issues.append(LintIssue(
564+
rule_id="QC011", line=lineno,
565+
message="IndicatorBase[float] → IndicatorBase[IndicatorDataPoint] "
566+
"(C# generic type constraint)",
567+
severity="error", fixed=True,
568+
original=m.group(), replacement="IndicatorBase[IndicatorDataPoint]",
569+
))
570+
code = pattern.sub("IndicatorBase[IndicatorDataPoint]", code)
571+
return code
572+
573+
553574
# ---------------------------------------------------------------------------
554575
# QC010 — Reserved QCAlgorithm attribute names (auto-fix)
555576
# ---------------------------------------------------------------------------
@@ -601,6 +622,7 @@ def _rule_qc010(code: str, issues: List[LintIssue]) -> str:
601622
_rule_qc007, # Resolution casing
602623
_rule_qc009, # Wrong asset class API (runs after QC001 normalizes add_equity)
603624
_rule_qc010, # Reserved QCAlgorithm attribute names
625+
_rule_qc011, # IndicatorBase[float] → IndicatorBase[IndicatorDataPoint]
604626
_rule_qc004, # Action() wrapper
605627
_rule_qc002, # len() on RollingWindow
606628
_rule_qc003, # .Values on RollingWindow

tests/test_qc_linter.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -472,6 +472,21 @@ def test_loop_pattern_equity_unchanged(self):
472472
assert len(qc009_issues) == 0
473473
assert "self.add_equity(t" in result.code
474474

475+
def test_loop_pattern_self_var(self):
476+
"""self.currency_pairs = [...] + for symbol in self.currency_pairs."""
477+
code = (
478+
"self.currency_pairs = [\n"
479+
" \"EURGBP\", \"EURUSD\", \"EURJPY\", \"CHFJPY\",\n"
480+
" \"EURCHF\", \"USDCHF\", \"USDJPY\", \"USDCAD\"\n"
481+
"]\n"
482+
"for symbol in self.currency_pairs:\n"
483+
" self.add_equity(symbol, Resolution.TICK)\n"
484+
)
485+
result = lint_qc_code(code)
486+
assert result.had_fixes
487+
assert "self.add_forex(symbol" in result.code
488+
assert "self.add_equity" not in result.code
489+
475490

476491
# ---------------------------------------------------------------------------
477492
# QC010 — Reserved QCAlgorithm attribute names
@@ -534,6 +549,32 @@ def test_alpha_value_not_matched(self):
534549
assert "self.alpha_value" in result.code
535550

536551

552+
# ---------------------------------------------------------------------------
553+
# QC011 — IndicatorBase[float]
554+
# ---------------------------------------------------------------------------
555+
556+
class TestQC011IndicatorBase:
557+
"""IndicatorBase[float] → IndicatorBase[IndicatorDataPoint]."""
558+
559+
def test_indicator_base_float_fixed(self):
560+
code = "class MyInd(IndicatorBase[float]):\n pass\n"
561+
result = lint_qc_code(code)
562+
assert result.had_fixes
563+
assert "IndicatorBase[IndicatorDataPoint]" in result.code
564+
assert "IndicatorBase[float]" not in result.code
565+
566+
def test_indicator_base_correct_unchanged(self):
567+
code = "class MyInd(IndicatorBase[IndicatorDataPoint]):\n pass\n"
568+
result = lint_qc_code(code)
569+
qc011_issues = _issues_by_rule(result, "QC011")
570+
assert len(qc011_issues) == 0
571+
572+
def test_indicator_base_with_spaces(self):
573+
code = "class MyInd(IndicatorBase[ float ]):\n pass\n"
574+
result = lint_qc_code(code)
575+
assert "IndicatorBase[IndicatorDataPoint]" in result.code
576+
577+
537578
class TestComposition:
538579
"""Multiple rules apply to the same code."""
539580

0 commit comments

Comments
 (0)