mirror of
https://github.com/TauricResearch/TradingAgents.git
synced 2026-06-16 21:06:15 +03:00
feat(reflection): configurable alpha benchmark for non-US tickers
SPY was hardcoded as the alpha benchmark in both the return-fetch path and the reflection label, which produced meaningless alpha for .NS / .T / .HK / .L / .TO / .AX / .BO listings — FX drift between a local-currency stock and a USD index dominates the spread. DEFAULT_CONFIG now exposes benchmark_ticker (explicit override) and benchmark_map (suffix → regional index, with SPY as the empty-suffix default). TRADINGAGENTS_BENCHMARK_TICKER joins the env-overlay table. Trading graph resolves the benchmark once per ticker and threads it through to both _fetch_returns and reflect_on_final_decision, so the alpha label reads "Alpha vs ^N225" for Tokyo listings, "Alpha vs ^HSI" for Hong Kong, etc., instead of the misleading "Alpha vs SPY".
This commit is contained in:
@@ -535,6 +535,93 @@ class TestDeferredReflection:
|
||||
assert raw is not None and alpha is not None and days is not None
|
||||
assert days == 2
|
||||
|
||||
# TradingAgentsGraph._resolve_benchmark — picks index for alpha calc
|
||||
|
||||
def test_resolve_benchmark_explicit_override(self):
|
||||
"""config['benchmark_ticker'] wins for every ticker."""
|
||||
mock_graph = MagicMock(spec=TradingAgentsGraph)
|
||||
mock_graph.config = {
|
||||
"benchmark_ticker": "QQQ",
|
||||
"benchmark_map": {"": "SPY", ".T": "^N225"},
|
||||
}
|
||||
assert TradingAgentsGraph._resolve_benchmark(mock_graph, "7203.T") == "QQQ"
|
||||
assert TradingAgentsGraph._resolve_benchmark(mock_graph, "NVDA") == "QQQ"
|
||||
|
||||
def test_resolve_benchmark_suffix_map(self):
|
||||
"""Known suffixes route to their regional index."""
|
||||
mock_graph = MagicMock(spec=TradingAgentsGraph)
|
||||
mock_graph.config = {
|
||||
"benchmark_ticker": None,
|
||||
"benchmark_map": {
|
||||
".T": "^N225", ".HK": "^HSI", ".NS": "^NSEI",
|
||||
".L": "^FTSE", ".TO": "^GSPTSE", ".AX": "^AXJO",
|
||||
".BO": "^BSESN", "": "SPY",
|
||||
},
|
||||
}
|
||||
assert TradingAgentsGraph._resolve_benchmark(mock_graph, "7203.T") == "^N225"
|
||||
assert TradingAgentsGraph._resolve_benchmark(mock_graph, "0700.HK") == "^HSI"
|
||||
assert TradingAgentsGraph._resolve_benchmark(mock_graph, "RELIANCE.NS") == "^NSEI"
|
||||
assert TradingAgentsGraph._resolve_benchmark(mock_graph, "AZN.L") == "^FTSE"
|
||||
|
||||
def test_resolve_benchmark_us_ticker_defaults_to_spy(self):
|
||||
"""US tickers (no dotted suffix) take the empty-suffix entry."""
|
||||
mock_graph = MagicMock(spec=TradingAgentsGraph)
|
||||
mock_graph.config = {
|
||||
"benchmark_ticker": None,
|
||||
"benchmark_map": {"": "SPY", ".T": "^N225"},
|
||||
}
|
||||
assert TradingAgentsGraph._resolve_benchmark(mock_graph, "NVDA") == "SPY"
|
||||
assert TradingAgentsGraph._resolve_benchmark(mock_graph, "AAPL") == "SPY"
|
||||
|
||||
def test_resolve_benchmark_unknown_suffix_falls_back(self):
|
||||
"""Unrecognised suffix (BRK.B, FAKE.XX) falls back to SPY."""
|
||||
mock_graph = MagicMock(spec=TradingAgentsGraph)
|
||||
mock_graph.config = {
|
||||
"benchmark_ticker": None,
|
||||
"benchmark_map": {"": "SPY", ".T": "^N225"},
|
||||
}
|
||||
assert TradingAgentsGraph._resolve_benchmark(mock_graph, "FAKE.XX") == "SPY"
|
||||
assert TradingAgentsGraph._resolve_benchmark(mock_graph, "BRK.B") == "SPY"
|
||||
|
||||
def test_resolve_benchmark_case_insensitive(self):
|
||||
"""Suffix matching is case-insensitive so 7203.t resolves like 7203.T."""
|
||||
mock_graph = MagicMock(spec=TradingAgentsGraph)
|
||||
mock_graph.config = {
|
||||
"benchmark_ticker": None,
|
||||
"benchmark_map": {".T": "^N225", "": "SPY"},
|
||||
}
|
||||
assert TradingAgentsGraph._resolve_benchmark(mock_graph, "7203.t") == "^N225"
|
||||
|
||||
def test_reflector_includes_benchmark_in_label(self):
|
||||
"""benchmark_name appears in the prompt label, not 'SPY' hardcoded."""
|
||||
mock_llm = MagicMock()
|
||||
mock_llm.invoke.return_value.content = "Directionally correct."
|
||||
reflector = Reflector(mock_llm)
|
||||
reflector.reflect_on_final_decision(
|
||||
final_decision=DECISION_BUY,
|
||||
raw_return=0.05,
|
||||
alpha_return=0.02,
|
||||
benchmark_name="^N225",
|
||||
)
|
||||
messages = mock_llm.invoke.call_args[0][0]
|
||||
human_content = next(content for role, content in messages if role == "human")
|
||||
assert "Alpha vs ^N225:" in human_content
|
||||
assert "Alpha vs SPY:" not in human_content
|
||||
|
||||
def test_reflector_defaults_to_spy_for_unupdated_callers(self):
|
||||
"""Default benchmark_name keeps the SPY label for legacy callers."""
|
||||
mock_llm = MagicMock()
|
||||
mock_llm.invoke.return_value.content = "ok"
|
||||
reflector = Reflector(mock_llm)
|
||||
reflector.reflect_on_final_decision(
|
||||
final_decision=DECISION_BUY,
|
||||
raw_return=0.05,
|
||||
alpha_return=0.02,
|
||||
)
|
||||
messages = mock_llm.invoke.call_args[0][0]
|
||||
human_content = next(content for role, content in messages if role == "human")
|
||||
assert "Alpha vs SPY:" in human_content
|
||||
|
||||
# TradingAgentsGraph._resolve_pending_entries
|
||||
|
||||
def test_resolve_skips_other_tickers(self, tmp_path):
|
||||
|
||||
Reference in New Issue
Block a user