mirror of
https://github.com/TauricResearch/TradingAgents.git
synced 2026-09-19 19:25:24 +03:00
- a non-blank message is text, whatever it would mean as a Python literal - the live layout renders on the alternate screen, so a tall layout does not scroll - a run with no readable rating says so instead of looking like a normal result - the state log keeps non-ASCII readable (#1081)
This commit is contained in:
35
cli/main.py
35
cli/main.py
@@ -42,6 +42,7 @@ from cli.utils import (
|
|||||||
select_research_depth,
|
select_research_depth,
|
||||||
select_shallow_thinking_agent,
|
select_shallow_thinking_agent,
|
||||||
)
|
)
|
||||||
|
from tradingagents.agents.utils.rating import is_review
|
||||||
from tradingagents.backtest import iter_grid, run_backtest, summarize
|
from tradingagents.backtest import iter_grid, run_backtest, summarize
|
||||||
from tradingagents.default_config import DEFAULT_CONFIG
|
from tradingagents.default_config import DEFAULT_CONFIG
|
||||||
from tradingagents.graph.analyst_execution import (
|
from tradingagents.graph.analyst_execution import (
|
||||||
@@ -916,21 +917,16 @@ def extract_content_string(content):
|
|||||||
"""Extract string content from various message formats.
|
"""Extract string content from various message formats.
|
||||||
Returns None if no meaningful text content is found.
|
Returns None if no meaningful text content is found.
|
||||||
"""
|
"""
|
||||||
import ast
|
|
||||||
|
|
||||||
def is_empty(val):
|
def is_empty(val):
|
||||||
"""Check if value is empty using Python's truthiness."""
|
"""Whether a value carries nothing to show.
|
||||||
if val is None or val == '':
|
|
||||||
return True
|
Text is judged by whether anything was written, not by what it would
|
||||||
|
mean as Python: a report saying "0" or "None" is a message the run
|
||||||
|
produced, and reading it as a falsy literal dropped it from the display.
|
||||||
|
"""
|
||||||
if isinstance(val, str):
|
if isinstance(val, str):
|
||||||
s = val.strip()
|
return not val.strip()
|
||||||
if not s:
|
return val is None or not bool(val)
|
||||||
return True
|
|
||||||
try:
|
|
||||||
return not bool(ast.literal_eval(s))
|
|
||||||
except (ValueError, SyntaxError):
|
|
||||||
return False # Can't parse = real text
|
|
||||||
return not bool(val)
|
|
||||||
|
|
||||||
if is_empty(content):
|
if is_empty(content):
|
||||||
return None
|
return None
|
||||||
@@ -1097,7 +1093,9 @@ def run_analysis(checkpoint: bool | None = None, portfolio=None):
|
|||||||
# Now start the display layout
|
# Now start the display layout
|
||||||
layout = create_layout()
|
layout = create_layout()
|
||||||
|
|
||||||
with Live(layout, refresh_per_second=4):
|
# The alternate screen keeps a layout taller than the window from redrawing
|
||||||
|
# by scrolling; the final report prints after this block, on the normal screen.
|
||||||
|
with Live(layout, refresh_per_second=4, screen=True):
|
||||||
# Initial display
|
# Initial display
|
||||||
update_display(layout, stats_handler=stats_handler, start_time=start_time)
|
update_display(layout, stats_handler=stats_handler, start_time=start_time)
|
||||||
|
|
||||||
@@ -1286,6 +1284,15 @@ def run_analysis(checkpoint: bool | None = None, portfolio=None):
|
|||||||
|
|
||||||
# Post-analysis prompts (outside Live context for clean interaction)
|
# Post-analysis prompts (outside Live context for clean interaction)
|
||||||
console.print("\n[bold cyan]Analysis Complete![/bold cyan]\n")
|
console.print("\n[bold cyan]Analysis Complete![/bold cyan]\n")
|
||||||
|
|
||||||
|
# A decision nobody can read is not a position. Say so here rather than
|
||||||
|
# leaving the run to look like a normal result.
|
||||||
|
if is_review(graph.process_signal(final_state.get("final_trade_decision", ""))):
|
||||||
|
console.print(
|
||||||
|
"[yellow]No rating could be read from the final decision, so this run "
|
||||||
|
"is recorded for review rather than as a position. Re-run, or read the "
|
||||||
|
"decision text below and judge it yourself.[/yellow]\n"
|
||||||
|
)
|
||||||
console.print(f"[dim]{analyst_wall_time_tracker.format_summary()}[/dim]")
|
console.print(f"[dim]{analyst_wall_time_tracker.format_summary()}[/dim]")
|
||||||
|
|
||||||
# Prompt to save report
|
# Prompt to save report
|
||||||
|
|||||||
77
tests/test_cli_display.py
Normal file
77
tests/test_cli_display.py
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
"""What the live display shows, and what the run log keeps.
|
||||||
|
|
||||||
|
The display drops a message it judges empty, and the state log is written for a
|
||||||
|
person to read afterwards. Both got that wrong in ways that hide real content.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from cli.main import extract_content_string
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
@pytest.mark.parametrize("text", ["0", "False", "None", "[]", "{}", "0.0"])
|
||||||
|
def test_a_message_that_reads_like_a_python_value_is_still_text(text):
|
||||||
|
"""These were parsed as Python and judged empty, so the message vanished."""
|
||||||
|
assert extract_content_string(text) == text
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
@pytest.mark.parametrize("value, expected", [
|
||||||
|
(" Hold ", "Hold"),
|
||||||
|
("", None),
|
||||||
|
(" ", None),
|
||||||
|
(None, None),
|
||||||
|
([], None),
|
||||||
|
({}, None),
|
||||||
|
({"text": "from a dict"}, "from a dict"),
|
||||||
|
([{"type": "text", "text": "part one"}, {"type": "text", "text": "part two"}], "part one part two"),
|
||||||
|
])
|
||||||
|
def test_the_other_shapes_are_unchanged(value, expected):
|
||||||
|
assert extract_content_string(value) == expected
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
def test_the_state_log_keeps_non_ascii_readable(tmp_path):
|
||||||
|
"""Reports can be in any language; the log is read by a person."""
|
||||||
|
from tradingagents.graph.trading_graph import TradingAgentsGraph
|
||||||
|
|
||||||
|
graph = object.__new__(TradingAgentsGraph)
|
||||||
|
graph.config = {"results_dir": str(tmp_path)}
|
||||||
|
graph.ticker = "600519.SS"
|
||||||
|
graph.log_states_dict = {}
|
||||||
|
|
||||||
|
graph._log_state("2026-09-01", {
|
||||||
|
"company_of_interest": "600519.SS", "trade_date": "2026-09-01",
|
||||||
|
"market_report": "市场", "sentiment_report": "情绪", "news_report": "新闻",
|
||||||
|
"fundamentals_report": "基本面", "investment_plan": "计划",
|
||||||
|
"trader_investment_plan": "交易计划", "final_trade_decision": "评级: 买入",
|
||||||
|
"investment_debate_state": {"bull_history": "", "bear_history": "", "history": "",
|
||||||
|
"current_response": "", "judge_decision": "", "count": 0},
|
||||||
|
"risk_debate_state": {"aggressive_history": "", "conservative_history": "",
|
||||||
|
"neutral_history": "", "history": "", "judge_decision": "",
|
||||||
|
"latest_speaker": "", "current_aggressive_response": "",
|
||||||
|
"current_conservative_response": "", "current_neutral_response": "",
|
||||||
|
"count": 0},
|
||||||
|
})
|
||||||
|
|
||||||
|
written = next(tmp_path.rglob("full_states_log*.json")).read_text(encoding="utf-8")
|
||||||
|
assert "买入" in written
|
||||||
|
assert "\\u" not in written
|
||||||
|
assert json.loads(written) # still valid JSON
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
def test_the_live_display_does_not_scroll_the_terminal():
|
||||||
|
"""A layout taller than the window makes rich redraw by scrolling, which
|
||||||
|
reads as flicker; the alternate screen holds it in place (#784). The final
|
||||||
|
report prints after the live view ends, so nothing is lost when it closes."""
|
||||||
|
import inspect
|
||||||
|
|
||||||
|
import cli.main as m
|
||||||
|
|
||||||
|
assert "screen=True" in inspect.getsource(m.run_analysis)
|
||||||
@@ -659,7 +659,8 @@ class TradingAgentsGraph:
|
|||||||
|
|
||||||
log_path = directory / f"full_states_log_{trade_date}.json"
|
log_path = directory / f"full_states_log_{trade_date}.json"
|
||||||
with open(log_path, "w", encoding="utf-8") as f:
|
with open(log_path, "w", encoding="utf-8") as f:
|
||||||
json.dump(self.log_states_dict[str(trade_date)], f, indent=4)
|
# Reports can be in any language and this file is read by a person.
|
||||||
|
json.dump(self.log_states_dict[str(trade_date)], f, indent=4, ensure_ascii=False)
|
||||||
|
|
||||||
def process_signal(self, full_signal):
|
def process_signal(self, full_signal):
|
||||||
"""Process a signal to extract the core decision."""
|
"""Process a signal to extract the core decision."""
|
||||||
|
|||||||
Reference in New Issue
Block a user