Repository navigation
fix(risk): stop the position limit being bypassed by a key mismatch (BUG-20) - #24
Merged
Merged
Conversation
…BUG-20)
The limit checks read `order["ticker"]`:
ticker = order.get("ticker", "")
current_value = self.positions.get(ticker, {}).get("value", 0)
`trading/live_runner.py` passed `{"code": ...}` -- the name `Order`, the
positions table and the rest of the codebase use. That resolved to an empty
ticker, found no existing position, and re-checked the limit from zero.
Demonstrated with a 49% holding against a 5% single-name limit and a 10k order:
{"ticker": "600519", ...} -> approved=False (weight 50.0% exceeds 5.0%)
{"code": "600519", ...} -> approved=True <- limit bypassed
The tests never caught it because they pass `ticker`, which is the key the
function documents and the one key that worked.
Two callers were wrong:
trading/live_runner.py passed `code`
strategy/portfolio_orchestrator called check_pre_trade(ticker=..., side=...)
with keyword arguments against a method that
takes a single dict, and then read
`check.get('approved')` from a return value
that is an (approved, breaches) tuple. Both
wrong, so that path raised instead of
checking anything.
Fix, in three parts:
1. `check_pre_trade` accepts `ticker`, `symbol` or `code`. All three are names
this codebase actually uses for the same thing, so accepting them is the
honest contract; `risk/realtime_engine.py` already accepted ticker/symbol.
2. It **fails closed** on an order with no identifier at all. Position and
sector limits cannot be evaluated without one, and the old behaviour --
silently checking from zero -- is precisely the bypass. Risk checks must
not approve what they cannot evaluate.
3. Both callers pass the documented shape.
Tests (5 new, parametrized over the three key names):
- every alias hits the limit check (all three failed before for code/symbol)
- an order with no identifier is blocked
- an identifier with no position yet is still checked, not rejected, so the
fail-closed rule does not over-block
Verified by reverting the guard: 3 of the new tests fail, the `ticker` variant
passes both ways -- it was the one path that worked.
1339 passed, 1 skipped.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars. ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Advanced Run ID: Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
P1 — risk-control bypass.
The bypass
Limit checks read
order["ticker"]:trading/live_runner.pypassed{"code": ...}— the nameOrder, the positions table and the rest of the codebase use. It resolved to an empty ticker, found no existing position, and re-checked the limit from zero.Demonstrated with a 49% holding against a 5% single-name limit, and a 10k order:
The tests never caught it because they pass
ticker— the key the function documents, and the one key that worked.Two callers were wrong
trading/live_runner.pycodestrategy/portfolio_orchestrator.pycheck_pre_trade(ticker=..., side=...)with keyword arguments against a method taking a single dict, then readcheck.get('approved')from a return value that is an(approved, breaches)tuple. Both wrong — that path raised rather than checking anythingFix, in three parts
ticker,symbolorcode. All three are names this codebase actually uses for the same thing;risk/realtime_engine.pyalready accepted ticker/symbol.Tests — 5 new, parametrized over the three key names
Verified by reverting the guard: 3 of the new tests fail, and the
tickervariant passes both ways, because it was the one path that worked.1339 passed, 1 skipped.🤖 Generated with Claude Code