diff --git a/cli/main.py b/cli/main.py index d6f2b72ce..3074271d6 100644 --- a/cli/main.py +++ b/cli/main.py @@ -505,15 +505,18 @@ def get_user_selections(): console.print( create_question_box( "Step 1: Ticker Symbol", - "Enter the exact ticker symbol to analyze, including exchange suffix when needed (examples: SPY, CNC.TO, 7203.T, 0700.HK)", + "Enter the ticker, with exchange suffix when needed (e.g. SPY, 0700.HK, BTC-USD)", "SPY", ) ) selected_ticker = get_ticker() asset_type = detect_asset_type(selected_ticker) - console.print( - f"[green]Detected asset type:[/green] {asset_type.value}" - ) + # Only announce when it's not the default stock path, to avoid printing + # "stock" on every run. + if asset_type.value != "stock": + console.print( + f"[green]Detected asset type:[/green] {asset_type.value}" + ) # Step 2: Analysis date default_date = datetime.datetime.now().strftime("%Y-%m-%d") @@ -639,29 +642,6 @@ def get_user_selections(): } -def get_ticker(): - """Get ticker symbol from user input, preserving exchange suffixes.""" - # typer.prompt strips trailing dot-suffixes on some shells (e.g. 000404.SH - # collapses to 000404). questionary.text reads the raw line. - ticker = questionary.text( - "", - validate=lambda value: ( - not value.strip() - or ( - all(ch.isalnum() or ch in "._-^" for ch in value.strip()) - and len(value.strip()) <= 32 - ) - ) - or "Please enter a valid ticker symbol, e.g. AAPL, 000404.SZ, 0700.HK.", - ).ask() - - if ticker is None: - console.print("\n[red]No ticker symbol provided. Exiting...[/red]") - raise typer.Exit(1) - - return (ticker.strip() or "SPY").upper() - - def get_analysis_date(): """Get the analysis date from user input.""" while True: @@ -1076,7 +1056,8 @@ def run_analysis(checkpoint: bool = False): # Add initial messages message_buffer.add_message("System", f"Selected ticker: {selections['ticker']}") - message_buffer.add_message("System", f"Detected asset type: {selections['asset_type']}") + if selections["asset_type"] != "stock": + message_buffer.add_message("System", f"Detected asset type: {selections['asset_type']}") message_buffer.add_message( "System", f"Analysis date: {selections['analysis_date']}" ) diff --git a/cli/utils.py b/cli/utils.py index 900c3d95f..d6adb6fad 100644 --- a/cli/utils.py +++ b/cli/utils.py @@ -12,7 +12,7 @@ from tradingagents.llm_clients.model_catalog import get_model_options console = Console() -TICKER_INPUT_EXAMPLES = "Examples: SPY, CNC.TO, 7203.T, 0700.HK" +TICKER_INPUT_EXAMPLES = "SPY, 0700.HK, BTC-USD" ANALYST_ORDER = [ ("Market Analyst", AnalystType.MARKET), @@ -25,10 +25,19 @@ CRYPTO_SUFFIXES = ("-USD", "-USDT", "-USDC", "-BTC", "-ETH") def get_ticker() -> str: - """Prompt the user to enter a ticker symbol.""" + """Prompt the user to enter a ticker symbol, preserving exchange suffixes. + + Uses questionary.text (not typer.prompt, which strips trailing dot-suffixes + like ``000404.SH`` on some shells) and validates the symbol charset so an + obvious typo is caught before the run starts. + """ ticker = questionary.text( - f"Enter the exact ticker symbol to analyze ({TICKER_INPUT_EXAMPLES}):", - validate=lambda x: len(x.strip()) > 0 or "Please enter a valid ticker symbol.", + f"Enter ticker symbol (e.g. {TICKER_INPUT_EXAMPLES}):", + validate=lambda x: ( + not x.strip() + or (all(ch.isalnum() or ch in "._-^" for ch in x.strip()) and len(x.strip()) <= 32) + or "Please enter a valid ticker symbol, e.g. AAPL, 000404.SZ, 0700.HK." + ), style=questionary.Style( [ ("text", "fg:green"), @@ -37,11 +46,11 @@ def get_ticker() -> str: ), ).ask() - if not ticker: + if ticker is None: console.print("\n[red]No ticker symbol provided. Exiting...[/red]") exit(1) - return normalize_ticker_symbol(ticker) + return normalize_ticker_symbol(ticker) if ticker.strip() else "SPY" def normalize_ticker_symbol(ticker: str) -> str: diff --git a/tests/test_ticker_symbol_handling.py b/tests/test_ticker_symbol_handling.py index 7fbe5315d..99db0b1eb 100644 --- a/tests/test_ticker_symbol_handling.py +++ b/tests/test_ticker_symbol_handling.py @@ -16,6 +16,14 @@ class TickerSymbolHandlingTests(unittest.TestCase): self.assertIn("7203.T", context) self.assertIn("exchange suffix", context) + def test_single_get_ticker_no_shadow(self): + # Regression: cli/main.py had a duplicate get_ticker with an empty + # questionary prompt (rendered as a bare "?") that shadowed the + # descriptive one in cli/utils. Keep a single canonical definition. + import cli.main + import cli.utils + self.assertIs(cli.main.get_ticker, cli.utils.get_ticker) + if __name__ == "__main__": unittest.main()