Skip to content

Commit 782de63

Browse files
SL-Marclaude
andcommitted
Fix linter gaps: chained Schedule/DateRules/TimeRules API, RollingWindow .Add(), and false-positive .Bars[]
QC001 now catches self.Schedule.On(), self.DateRules.EveryDay(), self.TimeRules.AfterMarketOpen() and 8 more chained scheduling patterns. Also fixes .Add() → .add() on RollingWindow variables. QC005 no longer flags data.Bars[] which is valid Slice access in on_data. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 3ea7643 commit 782de63

2 files changed

Lines changed: 122 additions & 8 deletions

File tree

quantcoder/core/qc_linter.py

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,22 @@ def unfixable_hints(self) -> List[str]:
117117
".Volume": ".volume",
118118
}
119119

120+
# self.Attr.Method( → self.attr.method( (chained scheduling API)
121+
# Ordered longest-first to avoid partial matches (EveryDay before Every)
122+
_CHAINED_MAP = {
123+
"self.Schedule.On": "self.schedule.on",
124+
"self.DateRules.EveryDay": "self.date_rules.every_day",
125+
"self.DateRules.MonthStart": "self.date_rules.month_start",
126+
"self.DateRules.MonthEnd": "self.date_rules.month_end",
127+
"self.DateRules.WeekStart": "self.date_rules.week_start",
128+
"self.DateRules.WeekEnd": "self.date_rules.week_end",
129+
"self.DateRules.Every": "self.date_rules.every",
130+
"self.TimeRules.AfterMarketOpen": "self.time_rules.after_market_open",
131+
"self.TimeRules.BeforeMarketClose": "self.time_rules.before_market_close",
132+
"self.TimeRules.At": "self.time_rules.at",
133+
"self.TimeRules.Every": "self.time_rules.every",
134+
}
135+
120136
# def MethodName(self → def method_name(self
121137
_DEF_MAP = {
122138
"Initialize": "initialize",
@@ -157,6 +173,33 @@ def _rule_qc001(code: str, issues: List[LintIssue]) -> str:
157173
))
158174
code = pattern.sub(snake, code)
159175

176+
# Chained scheduling API: self.Schedule.On( → self.schedule.on(
177+
for pascal, snake in _CHAINED_MAP.items():
178+
pattern = re.compile(re.escape(pascal) + r'(?=\s*\()')
179+
for m in pattern.finditer(code):
180+
lineno = code[:m.start()].count('\n') + 1
181+
issues.append(LintIssue(
182+
rule_id="QC001", line=lineno,
183+
message=f"PascalCase chained API {pascal}() → {snake}()",
184+
severity="error", fixed=True,
185+
original=m.group(), replacement=snake,
186+
))
187+
code = pattern.sub(snake, code)
188+
189+
# RollingWindow .Add( → .add(
190+
rw_names = set(_RW_DECL.findall(code))
191+
for name in rw_names:
192+
pattern = re.compile(r'(self\.' + re.escape(name) + r')\.Add(?=\s*\()')
193+
for m in pattern.finditer(code):
194+
lineno = code[:m.start()].count('\n') + 1
195+
issues.append(LintIssue(
196+
rule_id="QC001", line=lineno,
197+
message=f"self.{name}.Add() → self.{name}.add() (RollingWindow)",
198+
severity="error", fixed=True,
199+
original=f"self.{name}.Add", replacement=f"self.{name}.add",
200+
))
201+
code = pattern.sub(rf'\1.add', code)
202+
160203
# Method definitions: def PascalCase(self
161204
for pascal, snake in _DEF_MAP.items():
162205
pattern = re.compile(r'(def\s+)' + pascal + r'(\s*\(\s*self)')
@@ -280,8 +323,9 @@ def _rule_qc003(code: str, issues: List[LintIssue]) -> str:
280323
# ---------------------------------------------------------------------------
281324

282325
_HISTORY_SLICE_PATTERNS = [
283-
re.compile(r'\.Bars\s*\['),
326+
# .Bars[ is valid Slice access in on_data — only flag History-specific C# patterns
284327
re.compile(r'\.ForEach\s*\('),
328+
re.compile(r'\.GetValue\s*\('),
285329
]
286330

287331

tests/test_qc_linter.py

Lines changed: 77 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,58 @@ def test_portfolio_invested(self):
8484
assert ".invested" in result.code
8585
assert ".Invested" not in result.code
8686

87+
def test_chained_schedule_api_fixed(self):
88+
code = (
89+
"self.Schedule.On(self.DateRules.EveryDay(), "
90+
"self.TimeRules.AfterMarketOpen('SPY', 30), self.rebalance)\n"
91+
)
92+
result = lint_qc_code(code)
93+
assert result.had_fixes
94+
assert "self.schedule.on(" in result.code
95+
assert "self.date_rules.every_day()" in result.code
96+
assert "self.time_rules.after_market_open(" in result.code
97+
assert "self.Schedule.On" not in result.code
98+
assert "self.DateRules" not in result.code
99+
assert "self.TimeRules" not in result.code
100+
101+
def test_chained_time_rules_variants(self):
102+
code = (
103+
"self.Schedule.On(self.DateRules.MonthStart(), "
104+
"self.TimeRules.BeforeMarketClose('SPY', 5), self.close)\n"
105+
)
106+
result = lint_qc_code(code)
107+
assert "self.date_rules.month_start()" in result.code
108+
assert "self.time_rules.before_market_close(" in result.code
109+
110+
def test_chained_every_day_vs_every(self):
111+
"""EveryDay should not partially match Every."""
112+
code = (
113+
"a = self.DateRules.EveryDay()\n"
114+
"b = self.DateRules.Every(DayOfWeek.MONDAY)\n"
115+
)
116+
result = lint_qc_code(code)
117+
assert "self.date_rules.every_day()" in result.code
118+
assert "self.date_rules.every(DayOfWeek" in result.code
119+
120+
def test_rolling_window_add_fixed(self):
121+
code = (
122+
"self.prices = RollingWindow[float](20)\n"
123+
"self.prices.Add(data['SPY'].close)\n"
124+
)
125+
result = lint_qc_code(code)
126+
assert result.had_fixes
127+
assert "self.prices.add(" in result.code
128+
assert "self.prices.Add(" not in result.code
129+
130+
def test_clean_schedule_api_unchanged(self):
131+
code = (
132+
"self.schedule.on(self.date_rules.every_day(), "
133+
"self.time_rules.after_market_open('SPY', 30), self.rebalance)\n"
134+
)
135+
result = lint_qc_code(code)
136+
qc001_issues = _issues_by_rule(result, "QC001")
137+
assert len(qc001_issues) == 0
138+
87139

88140
# ---------------------------------------------------------------------------
89141
# QC002 — len() on RollingWindow
@@ -180,20 +232,27 @@ def test_no_action_no_change(self):
180232
class TestQC005HistorySlice:
181233
"""Warn about C# History iteration patterns."""
182234

183-
def test_bars_access_warned(self):
184-
code = "history.Bars[symbol].Close\n"
235+
def test_foreach_warned(self):
236+
code = "history.ForEach(lambda x: x)\n"
185237
result = lint_qc_code(code)
186238
qc005_issues = _issues_by_rule(result, "QC005")
187239
assert len(qc005_issues) == 1
188240
assert qc005_issues[0].severity == "warning"
189241
assert not qc005_issues[0].fixed
190242

191-
def test_foreach_warned(self):
192-
code = "history.ForEach(lambda x: x)\n"
243+
def test_getvalue_warned(self):
244+
code = "history.GetValue(symbol)\n"
193245
result = lint_qc_code(code)
194246
qc005_issues = _issues_by_rule(result, "QC005")
195247
assert len(qc005_issues) == 1
196248

249+
def test_bars_access_not_warned(self):
250+
"""data.Bars[symbol] is valid Slice access in on_data, not a History issue."""
251+
code = "if data.Bars.ContainsKey(self.symbol):\n price = data.Bars[self.symbol].close\n"
252+
result = lint_qc_code(code)
253+
qc005_issues = _issues_by_rule(result, "QC005")
254+
assert len(qc005_issues) == 0
255+
197256
def test_clean_history_no_warning(self):
198257
code = "df = self.history(self.symbol, 20, Resolution.DAILY)\n"
199258
result = lint_qc_code(code)
@@ -319,14 +378,15 @@ def test_full_pascal_algo(self):
319378
" self.spy = spy.Symbol\n"
320379
" self.rsi = self.RSI(self.spy, 14, Resolution.Daily)\n"
321380
" self.prices = RollingWindow[float](20)\n"
322-
" self.schedule.on(\n"
323-
" self.date_rules.every_day(),\n"
324-
" self.time_rules.at(10, 0),\n"
381+
" self.Schedule.On(\n"
382+
" self.DateRules.EveryDay(),\n"
383+
" self.TimeRules.AfterMarketOpen('SPY', 30),\n"
325384
" Action(self.trade)\n"
326385
" )\n"
327386
"\n"
328387
" def OnData(self, data):\n"
329388
" if self.rsi.IsReady:\n"
389+
" self.prices.Add(data['SPY'].close)\n"
330390
" if len(self.prices) >= 20:\n"
331391
" avg = sum(self.prices.Values) / 20\n"
332392
"\n"
@@ -345,6 +405,16 @@ def test_full_pascal_algo(self):
345405
assert ".invested" in result.code
346406
assert "self.liquidate" in result.code
347407

408+
# QC001 chained API fixes
409+
assert "self.schedule.on(" in result.code
410+
assert "self.date_rules.every_day()" in result.code
411+
assert "self.time_rules.after_market_open(" in result.code
412+
assert "self.Schedule.On" not in result.code
413+
414+
# QC001 .Add() fix
415+
assert "self.prices.add(" in result.code
416+
assert "self.prices.Add(" not in result.code
417+
348418
# QC007 fixes
349419
assert "Resolution.DAILY" in result.code
350420
assert "Resolution.Daily" not in result.code

0 commit comments

Comments
 (0)