From 8536ccacdd1cb05b2e8d2d4aa195d72c5aeb2e0d Mon Sep 17 00:00:00 2001 From: Zhigong Liu Date: Fri, 17 Apr 2026 18:57:31 -0400 Subject: [PATCH 1/2] chore: ignore CLAUDE.md (local AI assistant context file) Co-Authored-By: Claude Sonnet 4.6 --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 9a2904a9..a8b1d630 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +# Claude Code local files +CLAUDE.md + # Byte-compiled / optimized / DLL files __pycache__/ *.py[codz] From 9ccb1913ddc3bafd499d7370eb3e880588c458e1 Mon Sep 17 00:00:00 2001 From: Zhigong Liu Date: Sat, 18 Apr 2026 15:43:54 -0400 Subject: [PATCH 2/2] fix(cli): add encoding='utf-8' to all file I/O on Windows (closes #543, #550) --- cli/main.py | 123 ++++++++++++++++++++++------------------------------ 1 file changed, 51 insertions(+), 72 deletions(-) diff --git a/cli/main.py b/cli/main.py index 33d110fb..3b659478 100644 --- a/cli/main.py +++ b/cli/main.py @@ -463,7 +463,7 @@ def update_display(layout, spinner_text=None, stats_handler=None, start_time=Non def get_user_selections(): """Get all user selections before starting the analysis display.""" # Display ASCII art welcome message - with open(Path(__file__).parent / "static" / "welcome.txt", "r") as f: + with open(Path(__file__).parent / "static" / "welcome.txt", "r", encoding="utf-8") as f: welcome_ascii = f.read() # Create welcome box content @@ -638,91 +638,70 @@ def get_analysis_date(): def save_report_to_disk(final_state, ticker: str, save_path: Path): """Save complete analysis report to disk with organized subfolders.""" + + def write_section(directory: Path, items: list[tuple[str, str, str]], source: dict) -> list[tuple[str, str]]: + """Write files for present keys; return (label, text) pairs.""" + parts = [] + for key, filename, label in items: + if text := source.get(key): + directory.mkdir(exist_ok=True) + (directory / filename).write_text(text, encoding="utf-8") + parts.append((label, text)) + return parts + + def join_parts(parts): + return "\n\n".join(f"### {name}\n{text}" for name, text in parts) + save_path.mkdir(parents=True, exist_ok=True) sections = [] # 1. Analysts - analysts_dir = save_path / "1_analysts" - analyst_parts = [] - if final_state.get("market_report"): - analysts_dir.mkdir(exist_ok=True) - (analysts_dir / "market.md").write_text(final_state["market_report"]) - analyst_parts.append(("Market Analyst", final_state["market_report"])) - if final_state.get("sentiment_report"): - analysts_dir.mkdir(exist_ok=True) - (analysts_dir / "sentiment.md").write_text(final_state["sentiment_report"]) - analyst_parts.append(("Social Analyst", final_state["sentiment_report"])) - if final_state.get("news_report"): - analysts_dir.mkdir(exist_ok=True) - (analysts_dir / "news.md").write_text(final_state["news_report"]) - analyst_parts.append(("News Analyst", final_state["news_report"])) - if final_state.get("fundamentals_report"): - analysts_dir.mkdir(exist_ok=True) - (analysts_dir / "fundamentals.md").write_text(final_state["fundamentals_report"]) - analyst_parts.append(("Fundamentals Analyst", final_state["fundamentals_report"])) - if analyst_parts: - content = "\n\n".join(f"### {name}\n{text}" for name, text in analyst_parts) - sections.append(f"## I. Analyst Team Reports\n\n{content}") + parts = write_section(save_path / "1_analysts", [ + ("market_report", "market.md", "Market Analyst"), + ("sentiment_report", "sentiment.md", "Social Analyst"), + ("news_report", "news.md", "News Analyst"), + ("fundamentals_report", "fundamentals.md", "Fundamentals Analyst"), + ], final_state) + if parts: + sections.append(f"## I. Analyst Team Reports\n\n{join_parts(parts)}") # 2. Research - if final_state.get("investment_debate_state"): - research_dir = save_path / "2_research" - debate = final_state["investment_debate_state"] - research_parts = [] - if debate.get("bull_history"): - research_dir.mkdir(exist_ok=True) - (research_dir / "bull.md").write_text(debate["bull_history"]) - research_parts.append(("Bull Researcher", debate["bull_history"])) - if debate.get("bear_history"): - research_dir.mkdir(exist_ok=True) - (research_dir / "bear.md").write_text(debate["bear_history"]) - research_parts.append(("Bear Researcher", debate["bear_history"])) - if debate.get("judge_decision"): - research_dir.mkdir(exist_ok=True) - (research_dir / "manager.md").write_text(debate["judge_decision"]) - research_parts.append(("Research Manager", debate["judge_decision"])) - if research_parts: - content = "\n\n".join(f"### {name}\n{text}" for name, text in research_parts) - sections.append(f"## II. Research Team Decision\n\n{content}") + if debate := final_state.get("investment_debate_state"): + parts = write_section(save_path / "2_research", [ + ("bull_history", "bull.md", "Bull Researcher"), + ("bear_history", "bear.md", "Bear Researcher"), + ("judge_decision", "manager.md", "Research Manager"), + ], debate) + if parts: + sections.append(f"## II. Research Team Decision\n\n{join_parts(parts)}") # 3. Trading - if final_state.get("trader_investment_plan"): - trading_dir = save_path / "3_trading" - trading_dir.mkdir(exist_ok=True) - (trading_dir / "trader.md").write_text(final_state["trader_investment_plan"]) - sections.append(f"## III. Trading Team Plan\n\n### Trader\n{final_state['trader_investment_plan']}") + parts = write_section(save_path / "3_trading", [ + ("trader_investment_plan", "trader.md", "Trader"), + ], final_state) + if parts: + sections.append(f"## III. Trading Team Plan\n\n{join_parts(parts)}") # 4. Risk Management - if final_state.get("risk_debate_state"): - risk_dir = save_path / "4_risk" - risk = final_state["risk_debate_state"] - risk_parts = [] - if risk.get("aggressive_history"): - risk_dir.mkdir(exist_ok=True) - (risk_dir / "aggressive.md").write_text(risk["aggressive_history"]) - risk_parts.append(("Aggressive Analyst", risk["aggressive_history"])) - if risk.get("conservative_history"): - risk_dir.mkdir(exist_ok=True) - (risk_dir / "conservative.md").write_text(risk["conservative_history"]) - risk_parts.append(("Conservative Analyst", risk["conservative_history"])) - if risk.get("neutral_history"): - risk_dir.mkdir(exist_ok=True) - (risk_dir / "neutral.md").write_text(risk["neutral_history"]) - risk_parts.append(("Neutral Analyst", risk["neutral_history"])) - if risk_parts: - content = "\n\n".join(f"### {name}\n{text}" for name, text in risk_parts) - sections.append(f"## IV. Risk Management Team Decision\n\n{content}") + if risk := final_state.get("risk_debate_state"): + parts = write_section(save_path / "4_risk", [ + ("aggressive_history", "aggressive.md", "Aggressive Analyst"), + ("conservative_history","conservative.md", "Conservative Analyst"), + ("neutral_history", "neutral.md", "Neutral Analyst"), + ], risk) + if parts: + sections.append(f"## IV. Risk Management Team Decision\n\n{join_parts(parts)}") # 5. Portfolio Manager - if risk.get("judge_decision"): + if decision := risk.get("judge_decision"): portfolio_dir = save_path / "5_portfolio" portfolio_dir.mkdir(exist_ok=True) - (portfolio_dir / "decision.md").write_text(risk["judge_decision"]) - sections.append(f"## V. Portfolio Manager Decision\n\n### Portfolio Manager\n{risk['judge_decision']}") + (portfolio_dir / "decision.md").write_text(decision, encoding="utf-8") + sections.append(f"## V. Portfolio Manager Decision\n\n### Portfolio Manager\n{decision}") # Write consolidated report header = f"# Trading Analysis Report: {ticker}\n\nGenerated: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n" - (save_path / "complete_report.md").write_text(header + "\n\n".join(sections)) + (save_path / "complete_report.md").write_text(header + "\n\n".join(sections), encoding="utf-8") return save_path / "complete_report.md" @@ -980,7 +959,7 @@ def run_analysis(): func(*args, **kwargs) timestamp, message_type, content = obj.messages[-1] content = content.replace("\n", " ") # Replace newlines with spaces - with open(log_file, "a") as f: + with open(log_file, "a", encoding="utf-8") as f: f.write(f"{timestamp} [{message_type}] {content}\n") return wrapper @@ -991,7 +970,7 @@ def run_analysis(): func(*args, **kwargs) timestamp, tool_name, args = obj.tool_calls[-1] args_str = ", ".join(f"{k}={v}" for k, v in args.items()) - with open(log_file, "a") as f: + with open(log_file, "a", encoding="utf-8") as f: f.write(f"{timestamp} [Tool Call] {tool_name}({args_str})\n") return wrapper @@ -1005,7 +984,7 @@ def run_analysis(): if content: file_name = f"{section_name}.md" text = "\n".join(str(item) for item in content) if isinstance(content, list) else content - with open(report_dir / file_name, "w") as f: + with open(report_dir / file_name, "w", encoding="utf-8") as f: f.write(text) return wrapper