Skip to content

feat: add pre-trade market state verification to risk management#523

Open
LembaGang wants to merge 1 commit intoTauricResearch:mainfrom
LembaGang:feature/market-state-verification-gate
Open

feat: add pre-trade market state verification to risk management#523
LembaGang wants to merge 1 commit intoTauricResearch:mainfrom
LembaGang:feature/market-state-verification-gate

Conversation

@LembaGang
Copy link
Copy Markdown

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.

  • If OPEN → advisory confirms safe to proceed
  • If CLOSED/HALTED/UNKNOWN → advisory blocks execution with reason
  • If oracle unreachable → defaults to UNKNOWN (fail-closed, blocks trade)

What changed

File Change
tradingagents/agents/risk_mgmt/market_gate.py New module — check_market_state(), create_market_gate(), ticker→MIC mapping for 22 exchanges
tradingagents/agents/__init__.py Export create_market_gate
tradingagents/default_config.py New use_market_gate: True config flag
tradingagents/graph/setup.py Wire Market Gate node between risk debate and Portfolio Manager (conditional on config)
tradingagents/graph/trading_graph.py Pass config to GraphSetup
tests/test_market_gate.py 12 tests — MIC resolution, HTTP mock, node behavior

Design decisions

  • No new dependencies — uses stdlib urllib only. No requests, no SDK.
  • Uses the free /v5/demo endpoint — no API key or account needed. Returns a cryptographically signed receipt (Ed25519) with OPEN/CLOSED/HALTED/UNKNOWN status for 28 global exchanges.
  • Optional — controlled by use_market_gate config flag (default True). Set to False to disable without touching any other code.
  • Fail-closed — network errors, timeouts, and malformed responses all resolve to UNKNOWN, which blocks the trade. The framework never silently approves during uncertainty.
  • Ticker suffix mapping — handles .L (London), .T (Tokyo), .HK (Hong Kong), .SA (Brazil), etc. Plain tickers like AAPL default to XNYS.

Example output in risk debate history

[MARKET GATE] BLOCK TRADE — market XNYS is CLOSED. Exchange XNYS status is CLOSED. Do NOT approve execution — the market is not open for trading.
[MARKET GATE] Exchange XNYS is OPEN. Market state verified — safe to proceed with execution.

Usage

# Enabled by default — no config change needed
ta = TradingAgentsGraph()

# To disable:
ta = TradingAgentsGraph(config={**DEFAULT_CONFIG, "use_market_gate": False})

Resolves #514

Test plan

  • 12 new tests pass (pytest tests/test_market_gate.py)
  • All 5 existing tests still pass
  • MIC resolution: US, London, Tokyo, Hong Kong, case-insensitive, unknown suffix fallback
  • HTTP mock: OPEN (not blocked), CLOSED (blocked), HALTED (blocked), network failure (UNKNOWN, blocked)
  • Node integration: open market adds safe advisory, closed market adds block advisory
  • Manual: run a full propagation with use_market_gate: True during market hours and verify the [MARKET GATE] advisory appears in the risk debate history

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
".TO": "XNYS", # TMX — route through NYSE hours as proxy
".TO": "XTSE", # TMX (Toronto Stock Exchange)

Comment on lines +47 to +53
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"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
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())
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
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>
@LembaGang LembaGang force-pushed the feature/market-state-verification-gate branch from 65419f4 to 176b24b Compare April 7, 2026 13:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add market-state check to Risk Management step before trade execution

1 participant