feat: add pre-trade market state verification to risk management#523
feat: add pre-trade market state verification to risk management#523LembaGang wants to merge 1 commit intoTauricResearch:mainfrom
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces a pre-trade market state verification gate to ensure trades are only approved when the target exchange is open. It adds a new market_gate module that maps ticker suffixes to Market Identifier Codes (MICs) and queries a headless oracle for the current market status. The gate is integrated into the LangGraph workflow as an optional node before the Portfolio Manager. Feedback focuses on improving the accuracy of the Toronto Stock Exchange mapping, optimizing string normalization in the ticker-to-MIC resolution, and enhancing the efficiency of JSON response parsing.
| # Tickers without a suffix are assumed to be US equities (XNYS). | ||
| SUFFIX_TO_MIC = { | ||
| "": "XNYS", # US equities (default) | ||
| ".TO": "XNYS", # TMX — route through NYSE hours as proxy |
There was a problem hiding this comment.
The MIC for the Toronto Stock Exchange (TMX) is XTSE. Using XNYS (NYSE) as a proxy for Canadian equities may lead to incorrect results on days when the US market is open but the Canadian market is closed (e.g., Victoria Day, Canada Day) or vice versa. If the oracle supports XTSE, it should be used here.
| ".TO": "XNYS", # TMX — route through NYSE hours as proxy | |
| ".TO": "XTSE", # TMX (Toronto Stock Exchange) |
| def _ticker_to_mic(ticker: str) -> str: | ||
| """Derive the exchange MIC from a ticker's suffix.""" | ||
| for suffix, mic in SUFFIX_TO_MIC.items(): | ||
| if suffix and ticker.upper().endswith(suffix.upper()): | ||
| return mic | ||
| # No suffix -> US equity | ||
| return "XNYS" |
There was a problem hiding this comment.
The current implementation calls .upper() on the ticker for every iteration of the loop. It is more efficient to normalize the ticker once before starting the loop. Additionally, the "" key in SUFFIX_TO_MIC is explicitly skipped in the loop but then effectively handled by the fallback at the end; removing it from the dictionary would clarify the logic.
| def _ticker_to_mic(ticker: str) -> str: | |
| """Derive the exchange MIC from a ticker's suffix.""" | |
| for suffix, mic in SUFFIX_TO_MIC.items(): | |
| if suffix and ticker.upper().endswith(suffix.upper()): | |
| return mic | |
| # No suffix -> US equity | |
| return "XNYS" | |
| def _ticker_to_mic(ticker: str) -> str: | |
| """Derive the exchange MIC from a ticker's suffix.""" | |
| ticker_upper = ticker.upper() | |
| for suffix, mic in SUFFIX_TO_MIC.items(): | |
| if suffix and ticker_upper.endswith(suffix.upper()): | |
| return mic | |
| # No suffix -> US equity | |
| return "XNYS" |
| try: | ||
| req = Request(url, headers={"User-Agent": "TradingAgents/1.0"}) | ||
| with urlopen(req, timeout=timeout) as resp: | ||
| data = json.loads(resp.read().decode()) |
There was a problem hiding this comment.
When parsing JSON from a file-like object returned by urlopen, it is more efficient to use json.load(resp) directly rather than reading the entire response into memory with read() and then decoding it. json.load handles the stream and encoding detection automatically.
| data = json.loads(resp.read().decode()) | |
| data = json.load(resp) |
Add a Market Gate node between the risk debate and Portfolio Manager that verifies the target exchange is open before trade execution. - New `market_gate.py` with `check_market_state()` and `create_market_gate()` - Calls Headless Oracle /v5/demo (free, no API key required) - Fail-closed: network errors default to UNKNOWN (blocked) - Ticker suffix → MIC mapping for 22 global exchanges - Injects [MARKET GATE] advisory into risk debate history - Optional via `use_market_gate` config flag (default: True) - 12 new tests covering MIC resolution, HTTP mocking, and node behavior - Zero new dependencies (stdlib urllib only) Resolves TauricResearch#514 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
65419f4 to
176b24b
Compare
Summary
The risk management agents evaluate volatility, position sizing, and market sentiment — but don't verify whether the target exchange is actually open before the Portfolio Manager approves execution. This means the framework can approve trades during market closures, DST transition windows, or exchange circuit breaker halts.
This PR adds a Market Gate node that runs between the risk debate and Portfolio Manager. It checks real-time exchange status via a single HTTP call and injects the result into the debate history so the Portfolio Manager sees it before making the final decision.
What changed
tradingagents/agents/risk_mgmt/market_gate.pycheck_market_state(),create_market_gate(), ticker→MIC mapping for 22 exchangestradingagents/agents/__init__.pycreate_market_gatetradingagents/default_config.pyuse_market_gate: Trueconfig flagtradingagents/graph/setup.pytradingagents/graph/trading_graph.pytests/test_market_gate.pyDesign decisions
urllibonly. Norequests, no SDK./v5/demoendpoint — no API key or account needed. Returns a cryptographically signed receipt (Ed25519) with OPEN/CLOSED/HALTED/UNKNOWN status for 28 global exchanges.use_market_gateconfig flag (defaultTrue). Set toFalseto disable without touching any other code..L(London),.T(Tokyo),.HK(Hong Kong),.SA(Brazil), etc. Plain tickers likeAAPLdefault to XNYS.Example output in risk debate history
Usage
Resolves #514
Test plan
pytest tests/test_market_gate.py)use_market_gate: Trueduring market hours and verify the[MARKET GATE]advisory appears in the risk debate history