Skip to content

Commit 089857d

Browse files
committed
Add 4 new trading logic rules, fix 3 existing rule bugs
New rules: - Extended Hours Without Limit Order (HIGH) - Leverage Without Cap (HIGH) - Hardcoded Notional Amount (MEDIUM) - Hardcoded Crypto Pair (LOW) Bug fixes: - Infinite Loop Risk now catches while 1: in addition to while True: - Sleep Without Kill Switch now matches single decimal (time.sleep(0.5)) - load_custom_rules() reads category from YAML for mode filtering 20 tests passing.
1 parent a2e945d commit 089857d

2 files changed

Lines changed: 89 additions & 2 deletions

File tree

src/quanttape/rules.py

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -252,14 +252,14 @@ def compile(self) -> re.Pattern:
252252
),
253253
Rule(
254254
name="Infinite Loop Risk",
255-
pattern=r"^\s*while\s+True\s*:\s*(?:#.*)?$",
255+
pattern=r"^\s*while\s+(?:True|1)\s*:\s*(?:#.*)?$",
256256
severity="LOW",
257257
description="Infinite loop detected. Ensure there is a break condition or kill switch to prevent runaway execution.",
258258
category="trading_logic",
259259
),
260260
Rule(
261261
name="Sleep Without Kill Switch",
262-
pattern=r"(?ix)^\s*time\.sleep\(\s*(?:[1-9]\d*(?:\.\d+)?|0?\.\d{2,})\s*\)\s*(?:\#.*)?$",
262+
pattern=r"(?ix)^\s*time\.sleep\(\s*(?:[1-9]\d*(?:\.\d+)?|0?\.\d+)\s*\)\s*(?:\#.*)?$",
263263
severity="LOW",
264264
description="Hardcoded numeric sleep detected. Prefer configurable polling intervals with explicit shutdown/kill-switch checks around the loop.",
265265
category="trading_logic",
@@ -271,6 +271,36 @@ def compile(self) -> re.Pattern:
271271
description="Hardcoded ticker symbol. Consider making this configurable for reusability and testing.",
272272
category="trading_logic",
273273
),
274+
Rule(
275+
name="Extended Hours Without Limit Order",
276+
pattern=r"(?i)extended_hours\s*[=:]\s*True",
277+
severity="HIGH",
278+
description="Extended hours trading requires limit orders with time_in_force=day. Market orders and non-day TIF are rejected in extended sessions.",
279+
category="trading_logic",
280+
),
281+
Rule(
282+
name="Leverage Without Cap",
283+
pattern=r"(?ix)^\s*(?:leverage|margin_multiplier|margin_ratio)\s*=\s*"
284+
r"(?!.*\b(?:min|max|cap|limit|clamp|config|env|setting)\b)"
285+
r"\d+",
286+
severity="HIGH",
287+
description="Leverage or margin multiplier set without an explicit cap or config reference. Over-leverage amplifies losses and can trigger margin calls.",
288+
category="trading_logic",
289+
),
290+
Rule(
291+
name="Hardcoded Notional Amount",
292+
pattern=r"(?ix)\b(?:notional|order_value|trade_value)\s*=\s*['\"]?\d{5,}['\"]?",
293+
severity="MEDIUM",
294+
description="Large hardcoded notional/dollar amount in order. Use calculated position sizing with risk budgets instead of fixed dollar values.",
295+
category="trading_logic",
296+
),
297+
Rule(
298+
name="Hardcoded Crypto Pair",
299+
pattern=r"(?i)\b(?:symbol|ticker|pair)\s*=\s*['\"](?:[A-Z]{2,5}(?:USDT|BUSD|USD|USDC|BTC|ETH)|[A-Z]{2,5}/[A-Z]{2,5})['\"]",
300+
severity="LOW",
301+
description="Hardcoded crypto trading pair. Consider making this configurable for reusability across markets and testing.",
302+
category="trading_logic",
303+
),
274304
]
275305

276306
# --- AI agent egress rules (PII, env files, SSH keys in payloads) ---
@@ -372,6 +402,7 @@ def load_custom_rules(config_path: str) -> List[Rule]:
372402
pattern=entry["pattern"],
373403
severity=entry.get("severity", "MEDIUM"),
374404
description=entry.get("description", ""),
405+
category=entry.get("category", "general"),
375406
)
376407
)
377408
return rules

tests/test_rules.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,15 @@ def test_infinite_loop_risk_matches_plain_while_true(self):
4545
pattern = _compiled_rule("Infinite Loop Risk")
4646
self.assertIsNotNone(pattern.search("while True:"))
4747
self.assertIsNotNone(pattern.search(" while True: # daemon"))
48+
self.assertIsNotNone(pattern.search("while 1:"))
49+
self.assertIsNotNone(pattern.search(" while 1: # spin"))
4850

4951
def test_infinite_loop_risk_ignores_noncanonical_loops(self):
5052
pattern = _compiled_rule("Infinite Loop Risk")
5153
self.assertIsNone(pattern.search("while not stopped:"))
5254
self.assertIsNone(pattern.search("if cond: while True:"))
5355
self.assertIsNone(pattern.search("while True: do_work()"))
56+
self.assertIsNone(pattern.search("while 1: do_work()"))
5457

5558
def test_hardcoded_ticker_symbol_matches_real_single_symbol_assignments(self):
5659
pattern = _compiled_rule("Hardcoded Ticker Symbol")
@@ -67,6 +70,8 @@ def test_sleep_without_kill_switch_matches_hardcoded_sleep_calls(self):
6770
pattern = _compiled_rule("Sleep Without Kill Switch")
6871
self.assertIsNotNone(pattern.search("time.sleep(5)"))
6972
self.assertIsNotNone(pattern.search("time.sleep(0.25) # poll"))
73+
self.assertIsNotNone(pattern.search("time.sleep(0.5)"))
74+
self.assertIsNotNone(pattern.search("time.sleep(1.0)"))
7075

7176
def test_sleep_without_kill_switch_ignores_variable_or_inline_sleep(self):
7277
pattern = _compiled_rule("Sleep Without Kill Switch")
@@ -89,5 +94,56 @@ def test_no_position_size_limit_ignores_capped_or_risk_budget_sizing(self):
8994
self.assertIsNone(pattern.search("qty = clip(balance / price, 0, max_qty)"))
9095

9196

97+
# --- Extended Hours Without Limit Order ---
98+
def test_extended_hours_matches_true_assignments(self):
99+
pattern = _compiled_rule("Extended Hours Without Limit Order")
100+
self.assertIsNotNone(pattern.search("extended_hours=True"))
101+
self.assertIsNotNone(pattern.search("extended_hours = True"))
102+
self.assertIsNotNone(pattern.search('extended_hours: True'))
103+
104+
def test_extended_hours_ignores_false_or_variable(self):
105+
pattern = _compiled_rule("Extended Hours Without Limit Order")
106+
self.assertIsNone(pattern.search("extended_hours=False"))
107+
self.assertIsNone(pattern.search("extended_hours = use_ext"))
108+
109+
# --- Leverage Without Cap ---
110+
def test_leverage_without_cap_matches_bare_numeric(self):
111+
pattern = _compiled_rule("Leverage Without Cap")
112+
self.assertIsNotNone(pattern.search("leverage = 4"))
113+
self.assertIsNotNone(pattern.search("margin_multiplier = 10"))
114+
self.assertIsNotNone(pattern.search("margin_ratio = 2"))
115+
116+
def test_leverage_without_cap_ignores_capped_or_config(self):
117+
pattern = _compiled_rule("Leverage Without Cap")
118+
self.assertIsNone(pattern.search("leverage = min(4, max_leverage)"))
119+
self.assertIsNone(pattern.search("leverage = config.get('leverage')"))
120+
self.assertIsNone(pattern.search("margin_multiplier = env.MAX_LEVERAGE"))
121+
122+
# --- Hardcoded Notional Amount ---
123+
def test_hardcoded_notional_matches_large_values(self):
124+
pattern = _compiled_rule("Hardcoded Notional Amount")
125+
self.assertIsNotNone(pattern.search("notional = 100000"))
126+
self.assertIsNotNone(pattern.search("order_value = 50000"))
127+
self.assertIsNotNone(pattern.search("trade_value = 25000"))
128+
129+
def test_hardcoded_notional_ignores_small_or_variable(self):
130+
pattern = _compiled_rule("Hardcoded Notional Amount")
131+
self.assertIsNone(pattern.search("notional = 999"))
132+
self.assertIsNone(pattern.search("notional = computed_value"))
133+
self.assertIsNone(pattern.search("order_value = get_notional()"))
134+
135+
# --- Hardcoded Crypto Pair ---
136+
def test_hardcoded_crypto_pair_matches_common_pairs(self):
137+
pattern = _compiled_rule("Hardcoded Crypto Pair")
138+
self.assertIsNotNone(pattern.search('symbol="BTCUSDT"'))
139+
self.assertIsNotNone(pattern.search("pair='ETH/USD'"))
140+
self.assertIsNotNone(pattern.search('ticker="SOLUSDC"'))
141+
142+
def test_hardcoded_crypto_pair_ignores_variables_and_equity_tickers(self):
143+
pattern = _compiled_rule("Hardcoded Crypto Pair")
144+
self.assertIsNone(pattern.search("symbol = get_pair()"))
145+
self.assertIsNone(pattern.search('symbol="AAPL"'))
146+
147+
92148
if __name__ == "__main__":
93149
unittest.main()

0 commit comments

Comments
 (0)