diff --git a/macro_agents/src/macro_agents/defs/agents/asset_class_relationship_analyzer.py b/macro_agents/src/macro_agents/defs/agents/asset_class_relationship_analyzer.py index 7d7a3c2..5903380 100644 --- a/macro_agents/src/macro_agents/defs/agents/asset_class_relationship_analyzer.py +++ b/macro_agents/src/macro_agents/defs/agents/asset_class_relationship_analyzer.py @@ -127,6 +127,9 @@ def extract_relationship_summary(analysis_content: str) -> Dict[str, Any]: """Extract key insights from asset class relationship analysis for metadata.""" summary = {} + if not analysis_content: + return summary + correlation_matches = re.findall( r"(?:strongest|highest|strong).*?correlation[:\s]+([^.\n]+)", analysis_content, diff --git a/macro_agents/src/macro_agents/defs/agents/backtest_asset_class_relationship_analyzer.py b/macro_agents/src/macro_agents/defs/agents/backtest_asset_class_relationship_analyzer.py index 81a974c..73cb243 100644 --- a/macro_agents/src/macro_agents/defs/agents/backtest_asset_class_relationship_analyzer.py +++ b/macro_agents/src/macro_agents/defs/agents/backtest_asset_class_relationship_analyzer.py @@ -122,120 +122,214 @@ def backtest_analyze_asset_class_relationships( "completion_tokens": 0, "total_tokens": 0, } + failed_dates = [] + successful_dates = [] for backtest_date in backtest_dates: context.log.info( f"Processing backtest date: {backtest_date} ({backtest_dates.index(backtest_date) + 1}/{len(backtest_dates)})" ) - initial_history_length = ( - len(economic_analysis._lm.history) - if hasattr(economic_analysis, "_lm") - else 0 - ) - - context.log.info("Retrieving backtest economy state analysis...") - economy_state_analysis = get_latest_backtest_economy_state_analysis( - md, backtest_date, config.model_provider, config.model_name - ) + try: + initial_history_length = ( + len(economic_analysis._lm.history) + if hasattr(economic_analysis, "_lm") + else 0 + ) - if not economy_state_analysis: - raise ValueError( - f"No backtest economy state analysis found for {backtest_date} " - f"with provider {config.model_provider} and model {config.model_name}. Please run backtest_analyze_economy_state first." + context.log.info("Retrieving backtest economy state analysis...") + economy_state_analysis = get_latest_backtest_economy_state_analysis( + md, backtest_date, config.model_provider, config.model_name ) - context.log.info("Gathering market performance data with cutoff date...") - market_data = economic_analysis.get_market_data(md, cutoff_date=backtest_date) + if not economy_state_analysis: + raise ValueError( + f"No backtest economy state analysis found for {backtest_date} " + f"with provider {config.model_provider} and model {config.model_name}. Please run backtest_analyze_economy_state first." + ) - context.log.info("Gathering correlation data with cutoff date...") - try: - correlation_data = economic_analysis.get_correlation_data( - md, - sample_size=100, - sampling_strategy="top_correlations", - cutoff_date=backtest_date, - ) - except Exception as e: - context.log.warning( - f"Could not retrieve correlation data: {e}. Continuing without it." + context.log.info("Gathering market performance data with cutoff date...") + market_data = economic_analysis.get_market_data( + md, cutoff_date=backtest_date ) - correlation_data = "" - - context.log.info("Gathering commodity data with cutoff date...") - commodity_data = economic_analysis.get_commodity_data( - md, cutoff_date=backtest_date - ) - context.log.info( - "Running asset class relationship analysis with historical data..." - ) - analysis_result = relationship_analyzer( - economy_state_analysis=economy_state_analysis, - market_data=market_data, - correlation_data=correlation_data, - commodity_data=commodity_data, - ) + context.log.info("Gathering correlation data with cutoff date...") + try: + correlation_data = economic_analysis.get_correlation_data( + md, + sample_size=100, + sampling_strategy="top_correlations", + cutoff_date=backtest_date, + ) + except Exception as e: + context.log.warning( + f"Could not retrieve correlation data: {e}. Continuing without it." + ) + correlation_data = "" + + context.log.info("Gathering commodity data with cutoff date...") + commodity_data = economic_analysis.get_commodity_data( + md, cutoff_date=backtest_date + ) - token_usage = _get_token_usage( - economic_analysis, initial_history_length, context - ) + context.log.info( + "Running asset class relationship analysis with historical data..." + ) + try: + analysis_result = relationship_analyzer( + economy_state_analysis=economy_state_analysis, + market_data=market_data, + correlation_data=correlation_data, + commodity_data=commodity_data, + ) + + context.log.debug( + f"Analysis result type: {type(analysis_result)}, " + f"has 'relationship_analysis' attr: {hasattr(analysis_result, 'relationship_analysis')}, " + f"dir: {[attr for attr in dir(analysis_result) if not attr.startswith('_')]}" + ) + + if hasattr(analysis_result, "relationship_analysis"): + analysis_content_value = analysis_result.relationship_analysis + context.log.debug( + f"analysis_result.relationship_analysis type: {type(analysis_content_value)}, " + f"is None: {analysis_content_value is None}, " + f"is empty string: {analysis_content_value == ''}, " + f"length: {len(analysis_content_value) if analysis_content_value else 0}" + ) + if ( + not analysis_content_value + or analysis_content_value.strip() == "" + ): + context.log.warning( + f"Empty relationship analysis content detected for {backtest_date}. " + f"Full analysis_result object: {analysis_result}" + ) + if hasattr(economic_analysis, "_lm") and hasattr( + economic_analysis._lm, "history" + ): + recent_history = economic_analysis._lm.history[-3:] + context.log.warning( + f"Recent LLM history (last 3 entries): {recent_history}" + ) + if recent_history: + last_entry = recent_history[-1] + context.log.warning( + f"Last LLM history entry type: {type(last_entry)}, " + f"keys/attrs: {last_entry.keys() if isinstance(last_entry, dict) else dir(last_entry)[:10]}" + ) + if isinstance(last_entry, dict): + if "messages" in last_entry: + context.log.warning( + f"Last entry messages: {last_entry['messages']}" + ) + if "response" in last_entry: + context.log.warning( + f"Last entry response: {last_entry['response']}" + ) + if "output" in last_entry: + context.log.warning( + f"Last entry output: {last_entry['output']}" + ) + else: + context.log.error( + f"analysis_result does not have 'relationship_analysis' attribute. " + f"Available attributes: {[attr for attr in dir(analysis_result) if not attr.startswith('_')]}" + ) + + except Exception as e: + context.log.error( + f"Error during LLM analysis call for {backtest_date}: {str(e)}", + exc_info=True, + ) + if hasattr(economic_analysis, "_lm") and hasattr( + economic_analysis._lm, "history" + ): + recent_history = economic_analysis._lm.history[-3:] + context.log.debug( + f"LLM history at error (last 3 entries): {recent_history}" + ) + raise + + token_usage = _get_token_usage( + economic_analysis, initial_history_length, context + ) - if ( - "total_cost_usd" not in token_usage - or token_usage.get("total_cost_usd", 0) == 0 - ): - cost_data = _calculate_cost( - provider=provider, - model_name=model_name, - prompt_tokens=token_usage.get("prompt_tokens", 0), - completion_tokens=token_usage.get("completion_tokens", 0), + if ( + "total_cost_usd" not in token_usage + or token_usage.get("total_cost_usd", 0) == 0 + ): + cost_data = _calculate_cost( + provider=provider, + model_name=model_name, + prompt_tokens=token_usage.get("prompt_tokens", 0), + completion_tokens=token_usage.get("completion_tokens", 0), + ) + token_usage.update(cost_data) + + total_token_usage["prompt_tokens"] += token_usage.get("prompt_tokens", 0) + total_token_usage["completion_tokens"] += token_usage.get( + "completion_tokens", 0 + ) + total_token_usage["total_tokens"] += token_usage.get("total_tokens", 0) + if "total_cost_usd" in token_usage: + total_token_usage["total_cost_usd"] = total_token_usage.get( + "total_cost_usd", 0 + ) + token_usage.get("total_cost_usd", 0) + if "prompt_cost_usd" in token_usage: + total_token_usage["prompt_cost_usd"] = total_token_usage.get( + "prompt_cost_usd", 0 + ) + token_usage.get("prompt_cost_usd", 0) + if "completion_cost_usd" in token_usage: + total_token_usage["completion_cost_usd"] = total_token_usage.get( + "completion_cost_usd", 0 + ) + token_usage.get("completion_cost_usd", 0) + + analysis_timestamp = datetime.now() + json_result = { + "analysis_type": "asset_class_relationships", + "analysis_content": analysis_result.relationship_analysis, + "analysis_timestamp": analysis_timestamp.isoformat(), + "analysis_date": analysis_timestamp.strftime("%Y-%m-%d"), + "analysis_time": analysis_timestamp.strftime("%H:%M:%S"), + "backtest_date": backtest_date, + "model_provider": config.model_provider, + "model_name": config.model_name, + "data_sources": { + "economy_state_table": "backtest_economy_state_analysis", + "market_data_table": "us_sector_summary_snapshot", + "correlation_data_table": "leading_econ_return_indicator_snapshot", + "commodity_data_tables": [ + "energy_commodities_summary_snapshot", + "input_commodities_summary_snapshot", + "agriculture_commodities_summary_snapshot", + ], + }, + "dagster_run_id": context.run_id, + "dagster_asset_key": str(context.asset_key), + } + + all_results.append(json_result) + successful_dates.append(backtest_date) + + context.log.info( + f"Successfully processed {backtest_date}, writing result to database..." + ) + md.write_results_to_table( + [json_result], + output_table="backtest_asset_class_relationship_analysis", + if_exists="append", + context=context, ) - token_usage.update(cost_data) + context.log.info(f"Result for {backtest_date} written to database") - total_token_usage["prompt_tokens"] += token_usage.get("prompt_tokens", 0) - total_token_usage["completion_tokens"] += token_usage.get( - "completion_tokens", 0 - ) - total_token_usage["total_tokens"] += token_usage.get("total_tokens", 0) - if "total_cost_usd" in token_usage: - total_token_usage["total_cost_usd"] = total_token_usage.get( - "total_cost_usd", 0 - ) + token_usage.get("total_cost_usd", 0) - if "prompt_cost_usd" in token_usage: - total_token_usage["prompt_cost_usd"] = total_token_usage.get( - "prompt_cost_usd", 0 - ) + token_usage.get("prompt_cost_usd", 0) - if "completion_cost_usd" in token_usage: - total_token_usage["completion_cost_usd"] = total_token_usage.get( - "completion_cost_usd", 0 - ) + token_usage.get("completion_cost_usd", 0) - - analysis_timestamp = datetime.now() - json_result = { - "analysis_type": "asset_class_relationships", - "analysis_content": analysis_result.relationship_analysis, - "analysis_timestamp": analysis_timestamp.isoformat(), - "analysis_date": analysis_timestamp.strftime("%Y-%m-%d"), - "analysis_time": analysis_timestamp.strftime("%H:%M:%S"), - "backtest_date": backtest_date, - "model_provider": config.model_provider, - "model_name": config.model_name, - "data_sources": { - "economy_state_table": "backtest_economy_state_analysis", - "market_data_table": "us_sector_summary_snapshot", - "correlation_data_table": "leading_econ_return_indicator_snapshot", - "commodity_data_tables": [ - "energy_commodities_summary_snapshot", - "input_commodities_summary_snapshot", - "agriculture_commodities_summary_snapshot", - ], - }, - "dagster_run_id": context.run_id, - "dagster_asset_key": str(context.asset_key), - } - - all_results.append(json_result) + except Exception as e: + error_msg = f"Error processing backtest date {backtest_date}: {str(e)}" + context.log.error(error_msg) + failed_dates.append({"date": backtest_date, "error": str(e)}) + context.log.warning( + f"Continuing with remaining dates. {len(successful_dates)} successful, {len(failed_dates)} failed so far." + ) if ( "total_cost_usd" not in total_token_usage @@ -249,34 +343,38 @@ def backtest_analyze_asset_class_relationships( ) total_token_usage.update(cost_data) + if len(all_results) == 0: + error_msg = f"No backtest dates were successfully processed. All {len(backtest_dates)} date(s) failed." + context.log.error(error_msg) + raise ValueError(error_msg) + context.log.info( - f"Writing {len(all_results)} backtest asset class relationship analysis records to database..." - ) - md.write_results_to_table( - all_results, - output_table="backtest_asset_class_relationship_analysis", - if_exists="append", - context=context, + f"Processed {len(successful_dates)} successful date(s), {len(failed_dates)} failed date(s). " + f"All successful results have been written to database." ) - # Create summary metadata from first result first_result = all_results[0] - analysis_summary = extract_relationship_summary(first_result["analysis_content"]) + analysis_content = first_result.get("analysis_content") or "" + analysis_summary = ( + extract_relationship_summary(analysis_content) if analysis_content else {} + ) result_metadata = { - "analysis_completed": True, + "analysis_completed": len(failed_dates) == 0, "analysis_timestamp": first_result["analysis_timestamp"], "backtest_dates_processed": backtest_dates, + "successful_dates": successful_dates, + "failed_dates": failed_dates, "num_dates_processed": len(backtest_dates), + "num_successful": len(successful_dates), + "num_failed": len(failed_dates), "model_provider": config.model_provider, "model_name": config.model_name, "output_table": "backtest_asset_class_relationship_analysis", "records_written": len(all_results), "data_sources": first_result["data_sources"], "analysis_summary": analysis_summary, - "analysis_preview": first_result["analysis_content"][:500] - if first_result["analysis_content"] - else "", + "analysis_preview": analysis_content[:500] if analysis_content else "", "token_usage": total_token_usage, "provider": economic_analysis._get_provider(), } @@ -284,4 +382,20 @@ def backtest_analyze_asset_class_relationships( context.log.info( f"Backtest asset class relationship analysis complete: {result_metadata}" ) + + if len(failed_dates) > 0: + degraded_summary = ( + f"Backtest completed with {len(failed_dates)} failure(s) out of {len(backtest_dates)} total date(s). " + f"Successful results have been saved. Asset status: DEGRADED (partial success)." + ) + context.log.warning(degraded_summary) + result_metadata["status"] = "degraded" + result_metadata["degraded_reason"] = ( + f"{len(failed_dates)} of {len(backtest_dates)} dates failed" + ) + result_metadata["degraded_failed_dates"] = [f["date"] for f in failed_dates] + result_metadata["partial_success"] = True + return dg.MaterializeResult(metadata=result_metadata) + + result_metadata["status"] = "success" return dg.MaterializeResult(metadata=result_metadata) diff --git a/macro_agents/src/macro_agents/defs/agents/backtest_economy_state_analyzer.py b/macro_agents/src/macro_agents/defs/agents/backtest_economy_state_analyzer.py index a99e806..21c70e0 100644 --- a/macro_agents/src/macro_agents/defs/agents/backtest_economy_state_analyzer.py +++ b/macro_agents/src/macro_agents/defs/agents/backtest_economy_state_analyzer.py @@ -176,132 +176,226 @@ def backtest_analyze_economy_state( "completion_tokens": 0, "total_tokens": 0, } + failed_dates = [] + successful_dates = [] for backtest_date in backtest_dates: context.log.info( f"Processing backtest date: {backtest_date} ({backtest_dates.index(backtest_date) + 1}/{len(backtest_dates)})" ) - initial_history_length = ( - len(economic_analysis._lm.history) - if hasattr(economic_analysis, "_lm") - else 0 - ) - - context.log.info("Gathering economic data with cutoff date...") - economic_data = economic_analysis.get_economic_data( - md, cutoff_date=backtest_date - ) + try: + initial_history_length = ( + len(economic_analysis._lm.history) + if hasattr(economic_analysis, "_lm") + else 0 + ) - context.log.info("Gathering commodity data with cutoff date...") - commodity_data = economic_analysis.get_commodity_data( - md, cutoff_date=backtest_date - ) + context.log.info("Gathering economic data with cutoff date...") + economic_data = economic_analysis.get_economic_data( + md, cutoff_date=backtest_date + ) - context.log.info( - "Gathering Financial Conditions Index data with cutoff date..." - ) - fci_data = economic_analysis.get_financial_conditions_index( - md, cutoff_date=backtest_date - ) + context.log.info("Gathering commodity data with cutoff date...") + commodity_data = economic_analysis.get_commodity_data( + md, cutoff_date=backtest_date + ) - context.log.info("Gathering housing market data with cutoff date...") - housing_data = economic_analysis.get_housing_data(md, cutoff_date=backtest_date) + context.log.info( + "Gathering Financial Conditions Index data with cutoff date..." + ) + fci_data = economic_analysis.get_financial_conditions_index( + md, cutoff_date=backtest_date + ) - context.log.info("Gathering yield curve data with cutoff date...") - yield_curve_data = economic_analysis.get_yield_curve_data( - md, cutoff_date=backtest_date - ) + context.log.info("Gathering housing market data with cutoff date...") + housing_data = economic_analysis.get_housing_data( + md, cutoff_date=backtest_date + ) - context.log.info("Gathering economic trends data with cutoff date...") - economic_trends = economic_analysis.get_economic_trends( - md, cutoff_date=backtest_date - ) + context.log.info("Gathering yield curve data with cutoff date...") + yield_curve_data = economic_analysis.get_yield_curve_data( + md, cutoff_date=backtest_date + ) - context.log.info( - f"Running economy state analysis with historical data including FCI, housing, yield curve, and trends (personality: {config.personality})..." - ) - analysis_result = analyzer_to_use( - economic_data=economic_data, - commodity_data=commodity_data, - financial_conditions_index=fci_data, - housing_data=housing_data, - yield_curve_data=yield_curve_data, - economic_trends=economic_trends, - personality=config.personality, - ) + context.log.info("Gathering economic trends data with cutoff date...") + economic_trends = economic_analysis.get_economic_trends( + md, cutoff_date=backtest_date + ) - token_usage = _get_token_usage( - economic_analysis, initial_history_length, context - ) + context.log.info( + f"Running economy state analysis with historical data including FCI, housing, yield curve, and trends (personality: {config.personality})..." + ) + try: + analysis_result = analyzer_to_use( + economic_data=economic_data, + commodity_data=commodity_data, + financial_conditions_index=fci_data, + housing_data=housing_data, + yield_curve_data=yield_curve_data, + economic_trends=economic_trends, + personality=config.personality, + ) + + context.log.debug( + f"Analysis result type: {type(analysis_result)}, " + f"has 'analysis' attr: {hasattr(analysis_result, 'analysis')}, " + f"dir: {[attr for attr in dir(analysis_result) if not attr.startswith('_')]}" + ) + + if hasattr(analysis_result, "analysis"): + analysis_content_value = analysis_result.analysis + context.log.debug( + f"analysis_result.analysis type: {type(analysis_content_value)}, " + f"is None: {analysis_content_value is None}, " + f"is empty string: {analysis_content_value == ''}, " + f"length: {len(analysis_content_value) if analysis_content_value else 0}" + ) + if ( + not analysis_content_value + or analysis_content_value.strip() == "" + ): + context.log.warning( + f"Empty analysis content detected for {backtest_date}. " + f"Full analysis_result object: {analysis_result}" + ) + if hasattr(economic_analysis, "_lm") and hasattr( + economic_analysis._lm, "history" + ): + recent_history = economic_analysis._lm.history[-3:] + context.log.warning( + f"Recent LLM history (last 3 entries): {recent_history}" + ) + if recent_history: + last_entry = recent_history[-1] + context.log.warning( + f"Last LLM history entry type: {type(last_entry)}, " + f"keys/attrs: {last_entry.keys() if isinstance(last_entry, dict) else dir(last_entry)[:10]}" + ) + if isinstance(last_entry, dict): + if "messages" in last_entry: + context.log.warning( + f"Last entry messages: {last_entry['messages']}" + ) + if "response" in last_entry: + context.log.warning( + f"Last entry response: {last_entry['response']}" + ) + if "output" in last_entry: + context.log.warning( + f"Last entry output: {last_entry['output']}" + ) + else: + context.log.error( + f"analysis_result does not have 'analysis' attribute. " + f"Available attributes: {[attr for attr in dir(analysis_result) if not attr.startswith('_')]}" + ) + + except Exception as e: + context.log.error( + f"Error during LLM analysis call for {backtest_date}: {str(e)}", + exc_info=True, + ) + if hasattr(economic_analysis, "_lm") and hasattr( + economic_analysis._lm, "history" + ): + recent_history = economic_analysis._lm.history[-3:] + context.log.debug( + f"LLM history at error (last 3 entries): {recent_history}" + ) + raise + + token_usage = _get_token_usage( + economic_analysis, initial_history_length, context + ) - if ( - "total_cost_usd" not in token_usage - or token_usage.get("total_cost_usd", 0) == 0 - ): - provider = economic_analysis._get_provider() - model_name = economic_analysis._get_model_name() - cost_data = _calculate_cost( - provider=provider, - model_name=model_name, - prompt_tokens=token_usage.get("prompt_tokens", 0), - completion_tokens=token_usage.get("completion_tokens", 0), + if ( + "total_cost_usd" not in token_usage + or token_usage.get("total_cost_usd", 0) == 0 + ): + provider = economic_analysis._get_provider() + model_name = economic_analysis._get_model_name() + cost_data = _calculate_cost( + provider=provider, + model_name=model_name, + prompt_tokens=token_usage.get("prompt_tokens", 0), + completion_tokens=token_usage.get("completion_tokens", 0), + ) + token_usage.update(cost_data) + + total_token_usage["prompt_tokens"] += token_usage.get("prompt_tokens", 0) + total_token_usage["completion_tokens"] += token_usage.get( + "completion_tokens", 0 ) - token_usage.update(cost_data) + total_token_usage["total_tokens"] += token_usage.get("total_tokens", 0) + if "total_cost_usd" in token_usage: + total_token_usage["total_cost_usd"] = total_token_usage.get( + "total_cost_usd", 0 + ) + token_usage.get("total_cost_usd", 0) + if "prompt_cost_usd" in token_usage: + total_token_usage["prompt_cost_usd"] = total_token_usage.get( + "prompt_cost_usd", 0 + ) + token_usage.get("prompt_cost_usd", 0) + if "completion_cost_usd" in token_usage: + total_token_usage["completion_cost_usd"] = total_token_usage.get( + "completion_cost_usd", 0 + ) + token_usage.get("completion_cost_usd", 0) + + analysis_timestamp = datetime.now() + json_result = { + "analysis_type": "economy_state", + "analysis_content": analysis_result.analysis, + "analysis_timestamp": analysis_timestamp.isoformat(), + "analysis_date": analysis_timestamp.strftime("%Y-%m-%d"), + "analysis_time": analysis_timestamp.strftime("%H:%M:%S"), + "backtest_date": backtest_date, + "model_provider": config.model_provider, + "model_name": config.model_name, + "personality": config.personality, + "data_sources": { + "economic_data_table": "fred_series_latest_aggregates_snapshot", + "commodity_data_tables": [ + "energy_commodities_summary_snapshot", + "input_commodities_summary_snapshot", + "agriculture_commodities_summary_snapshot", + ], + "financial_conditions_index_table": "financial_conditions_index", + "housing_data_tables": [ + "housing_inventory_latest_aggregates", + "housing_mortgage_rates", + ], + "yield_curve_table": "stg_treasury_yields", + "economic_trends_table": "fred_monthly_diff", + "market_data_tables": [ + "us_sector_summary_snapshot", + "major_indicies_summary", + ], + }, + "dagster_run_id": context.run_id, + "dagster_asset_key": str(context.asset_key), + } + + all_results.append(json_result) + successful_dates.append(backtest_date) - total_token_usage["prompt_tokens"] += token_usage.get("prompt_tokens", 0) - total_token_usage["completion_tokens"] += token_usage.get( - "completion_tokens", 0 - ) - total_token_usage["total_tokens"] += token_usage.get("total_tokens", 0) - if "total_cost_usd" in token_usage: - total_token_usage["total_cost_usd"] = total_token_usage.get( - "total_cost_usd", 0 - ) + token_usage.get("total_cost_usd", 0) - if "prompt_cost_usd" in token_usage: - total_token_usage["prompt_cost_usd"] = total_token_usage.get( - "prompt_cost_usd", 0 - ) + token_usage.get("prompt_cost_usd", 0) - if "completion_cost_usd" in token_usage: - total_token_usage["completion_cost_usd"] = total_token_usage.get( - "completion_cost_usd", 0 - ) + token_usage.get("completion_cost_usd", 0) - - analysis_timestamp = datetime.now() - json_result = { - "analysis_type": "economy_state", - "analysis_content": analysis_result.analysis, - "analysis_timestamp": analysis_timestamp.isoformat(), - "analysis_date": analysis_timestamp.strftime("%Y-%m-%d"), - "analysis_time": analysis_timestamp.strftime("%H:%M:%S"), - "backtest_date": backtest_date, - "model_provider": config.model_provider, - "model_name": config.model_name, - "personality": config.personality, - "data_sources": { - "economic_data_table": "fred_series_latest_aggregates_snapshot", - "commodity_data_tables": [ - "energy_commodities_summary_snapshot", - "input_commodities_summary_snapshot", - "agriculture_commodities_summary_snapshot", - ], - "financial_conditions_index_table": "financial_conditions_index", - "housing_data_tables": [ - "housing_inventory_latest_aggregates", - "housing_mortgage_rates", - ], - "yield_curve_table": "stg_treasury_yields", - "economic_trends_table": "fred_monthly_diff", - "market_data_tables": [ - "us_sector_summary_snapshot", - "major_indicies_summary", - ], - }, - "dagster_run_id": context.run_id, - "dagster_asset_key": str(context.asset_key), - } - - all_results.append(json_result) + context.log.info( + f"Successfully processed {backtest_date}, writing result to database..." + ) + md.write_results_to_table( + [json_result], + output_table="backtest_economy_state_analysis", + if_exists="append", + context=context, + ) + context.log.info(f"Result for {backtest_date} written to database") + + except Exception as e: + error_msg = f"Error processing backtest date {backtest_date}: {str(e)}" + context.log.error(error_msg) + failed_dates.append({"date": backtest_date, "error": str(e)}) + context.log.warning( + f"Continuing with remaining dates. {len(successful_dates)} successful, {len(failed_dates)} failed so far." + ) if ( "total_cost_usd" not in total_token_usage @@ -315,25 +409,31 @@ def backtest_analyze_economy_state( ) total_token_usage.update(cost_data) + if len(all_results) == 0: + error_msg = f"No backtest dates were successfully processed. All {len(backtest_dates)} date(s) failed." + context.log.error(error_msg) + raise ValueError(error_msg) + context.log.info( - f"Writing {len(all_results)} backtest economy state analysis records to database..." - ) - md.write_results_to_table( - all_results, - output_table="backtest_economy_state_analysis", - if_exists="append", - context=context, + f"Processed {len(successful_dates)} successful date(s), {len(failed_dates)} failed date(s). " + f"All successful results have been written to database." ) - # Create summary metadata from first result first_result = all_results[0] - analysis_summary = extract_economy_state_summary(first_result["analysis_content"]) + analysis_content = first_result.get("analysis_content") or "" + analysis_summary = ( + extract_economy_state_summary(analysis_content) if analysis_content else {} + ) result_metadata = { - "analysis_completed": True, + "analysis_completed": len(failed_dates) == 0, "analysis_timestamp": first_result["analysis_timestamp"], "backtest_dates_processed": backtest_dates, + "successful_dates": successful_dates, + "failed_dates": failed_dates, "num_dates_processed": len(backtest_dates), + "num_successful": len(successful_dates), + "num_failed": len(failed_dates), "model_provider": config.model_provider, "model_name": config.model_name, "personality": config.personality, @@ -341,12 +441,26 @@ def backtest_analyze_economy_state( "records_written": len(all_results), "data_sources": first_result["data_sources"], "analysis_summary": analysis_summary, - "analysis_preview": first_result["analysis_content"][:500] - if first_result["analysis_content"] - else "", + "analysis_preview": analysis_content[:500] if analysis_content else "", "token_usage": total_token_usage, "provider": economic_analysis._get_provider(), } context.log.info(f"Backtest economy state analysis complete: {result_metadata}") + + if len(failed_dates) > 0: + degraded_summary = ( + f"Backtest completed with {len(failed_dates)} failure(s) out of {len(backtest_dates)} total date(s). " + f"Successful results have been saved. Asset status: DEGRADED (partial success)." + ) + context.log.warning(degraded_summary) + result_metadata["status"] = "degraded" + result_metadata["degraded_reason"] = ( + f"{len(failed_dates)} of {len(backtest_dates)} dates failed" + ) + result_metadata["degraded_failed_dates"] = [f["date"] for f in failed_dates] + result_metadata["partial_success"] = True + return dg.MaterializeResult(metadata=result_metadata) + + result_metadata["status"] = "success" return dg.MaterializeResult(metadata=result_metadata) diff --git a/macro_agents/src/macro_agents/defs/agents/backtest_investment_recommendations.py b/macro_agents/src/macro_agents/defs/agents/backtest_investment_recommendations.py index 82fa2cc..2600b45 100644 --- a/macro_agents/src/macro_agents/defs/agents/backtest_investment_recommendations.py +++ b/macro_agents/src/macro_agents/defs/agents/backtest_investment_recommendations.py @@ -160,103 +160,197 @@ def backtest_generate_investment_recommendations( "completion_tokens": 0, "total_tokens": 0, } + failed_dates = [] + successful_dates = [] for backtest_date in backtest_dates: context.log.info( f"Processing backtest date: {backtest_date} ({backtest_dates.index(backtest_date) + 1}/{len(backtest_dates)})" ) - initial_history_length = ( - len(economic_analysis._lm.history) - if hasattr(economic_analysis, "_lm") - else 0 - ) + try: + initial_history_length = ( + len(economic_analysis._lm.history) + if hasattr(economic_analysis, "_lm") + else 0 + ) - context.log.info("Retrieving backtest economy state analysis...") - economy_state_analysis = get_latest_backtest_economy_state_analysis( - md, backtest_date, config.model_provider, config.model_name - ) + context.log.info("Retrieving backtest economy state analysis...") + economy_state_analysis = get_latest_backtest_economy_state_analysis( + md, backtest_date, config.model_provider, config.model_name + ) - if not economy_state_analysis: - raise ValueError( - f"No backtest economy state analysis found for {backtest_date} " - f"with provider {config.model_provider} and model {config.model_name}. Please run backtest_analyze_economy_state first." + if not economy_state_analysis: + raise ValueError( + f"No backtest economy state analysis found for {backtest_date} " + f"with provider {config.model_provider} and model {config.model_name}. Please run backtest_analyze_economy_state first." + ) + + context.log.info("Retrieving backtest asset class relationship analysis...") + relationship_analysis = get_latest_backtest_relationship_analysis( + md, backtest_date, config.model_provider, config.model_name ) - context.log.info("Retrieving backtest asset class relationship analysis...") - relationship_analysis = get_latest_backtest_relationship_analysis( - md, backtest_date, config.model_provider, config.model_name - ) + if not relationship_analysis: + raise ValueError( + f"No backtest asset class relationship analysis found for {backtest_date} " + f"with provider {config.model_provider} and model {config.model_name}. Please run backtest_analyze_asset_class_relationships first." + ) - if not relationship_analysis: - raise ValueError( - f"No backtest asset class relationship analysis found for {backtest_date} " - f"with provider {config.model_provider} and model {config.model_name}. Please run backtest_analyze_asset_class_relationships first." + context.log.info( + f"Generating backtest investment recommendations (personality: {config.personality})..." ) + try: + recommendations_result = recommendations_generator( + economy_state_analysis=economy_state_analysis, + asset_class_relationship_analysis=relationship_analysis, + personality=config.personality, + ) - context.log.info( - f"Generating backtest investment recommendations (personality: {config.personality})..." - ) - recommendations_result = recommendations_generator( - economy_state_analysis=economy_state_analysis, - asset_class_relationship_analysis=relationship_analysis, - personality=config.personality, - ) + context.log.debug( + f"Recommendations result type: {type(recommendations_result)}, " + f"has 'recommendations' attr: {hasattr(recommendations_result, 'recommendations')}, " + f"dir: {[attr for attr in dir(recommendations_result) if not attr.startswith('_')]}" + ) - token_usage = _get_token_usage( - economic_analysis, initial_history_length, context - ) + if hasattr(recommendations_result, "recommendations"): + recommendations_content_value = ( + recommendations_result.recommendations + ) + context.log.debug( + f"recommendations_result.recommendations type: {type(recommendations_content_value)}, " + f"is None: {recommendations_content_value is None}, " + f"is empty string: {recommendations_content_value == ''}, " + f"length: {len(recommendations_content_value) if recommendations_content_value else 0}" + ) + if ( + not recommendations_content_value + or recommendations_content_value.strip() == "" + ): + context.log.warning( + f"Empty recommendations content detected for {backtest_date}. " + f"Full recommendations_result object: {recommendations_result}" + ) + if hasattr(economic_analysis, "_lm") and hasattr( + economic_analysis._lm, "history" + ): + recent_history = economic_analysis._lm.history[-3:] + context.log.warning( + f"Recent LLM history (last 3 entries): {recent_history}" + ) + if recent_history: + last_entry = recent_history[-1] + context.log.warning( + f"Last LLM history entry type: {type(last_entry)}, " + f"keys/attrs: {last_entry.keys() if isinstance(last_entry, dict) else dir(last_entry)[:10]}" + ) + if isinstance(last_entry, dict): + if "messages" in last_entry: + context.log.warning( + f"Last entry messages: {last_entry['messages']}" + ) + if "response" in last_entry: + context.log.warning( + f"Last entry response: {last_entry['response']}" + ) + if "output" in last_entry: + context.log.warning( + f"Last entry output: {last_entry['output']}" + ) + else: + context.log.error( + f"recommendations_result does not have 'recommendations' attribute. " + f"Available attributes: {[attr for attr in dir(recommendations_result) if not attr.startswith('_')]}" + ) + + except Exception as e: + context.log.error( + f"Error during LLM recommendations call for {backtest_date}: {str(e)}", + exc_info=True, + ) + if hasattr(economic_analysis, "_lm") and hasattr( + economic_analysis._lm, "history" + ): + recent_history = economic_analysis._lm.history[-3:] + context.log.debug( + f"LLM history at error (last 3 entries): {recent_history}" + ) + raise + + token_usage = _get_token_usage( + economic_analysis, initial_history_length, context + ) - if ( - "total_cost_usd" not in token_usage - or token_usage.get("total_cost_usd", 0) == 0 - ): - cost_data = _calculate_cost( - provider=provider, - model_name=model_name, - prompt_tokens=token_usage.get("prompt_tokens", 0), - completion_tokens=token_usage.get("completion_tokens", 0), + if ( + "total_cost_usd" not in token_usage + or token_usage.get("total_cost_usd", 0) == 0 + ): + cost_data = _calculate_cost( + provider=provider, + model_name=model_name, + prompt_tokens=token_usage.get("prompt_tokens", 0), + completion_tokens=token_usage.get("completion_tokens", 0), + ) + token_usage.update(cost_data) + + total_token_usage["prompt_tokens"] += token_usage.get("prompt_tokens", 0) + total_token_usage["completion_tokens"] += token_usage.get( + "completion_tokens", 0 ) - token_usage.update(cost_data) + total_token_usage["total_tokens"] += token_usage.get("total_tokens", 0) + if "total_cost_usd" in token_usage: + total_token_usage["total_cost_usd"] = total_token_usage.get( + "total_cost_usd", 0 + ) + token_usage.get("total_cost_usd", 0) + if "prompt_cost_usd" in token_usage: + total_token_usage["prompt_cost_usd"] = total_token_usage.get( + "prompt_cost_usd", 0 + ) + token_usage.get("prompt_cost_usd", 0) + if "completion_cost_usd" in token_usage: + total_token_usage["completion_cost_usd"] = total_token_usage.get( + "completion_cost_usd", 0 + ) + token_usage.get("completion_cost_usd", 0) + + analysis_timestamp = datetime.now() + json_result = { + "analysis_type": "investment_recommendations", + "recommendations_content": recommendations_result.recommendations, + "analysis_timestamp": analysis_timestamp.isoformat(), + "analysis_date": analysis_timestamp.strftime("%Y-%m-%d"), + "analysis_time": analysis_timestamp.strftime("%H:%M:%S"), + "backtest_date": backtest_date, + "model_provider": config.model_provider, + "model_name": config.model_name, + "personality": config.personality, + "data_sources": { + "economy_state_table": "backtest_economy_state_analysis", + "relationship_analysis_table": "backtest_asset_class_relationship_analysis", + }, + "dagster_run_id": context.run_id, + "dagster_asset_key": str(context.asset_key), + } + + all_results.append(json_result) + successful_dates.append(backtest_date) - total_token_usage["prompt_tokens"] += token_usage.get("prompt_tokens", 0) - total_token_usage["completion_tokens"] += token_usage.get( - "completion_tokens", 0 - ) - total_token_usage["total_tokens"] += token_usage.get("total_tokens", 0) - if "total_cost_usd" in token_usage: - total_token_usage["total_cost_usd"] = total_token_usage.get( - "total_cost_usd", 0 - ) + token_usage.get("total_cost_usd", 0) - if "prompt_cost_usd" in token_usage: - total_token_usage["prompt_cost_usd"] = total_token_usage.get( - "prompt_cost_usd", 0 - ) + token_usage.get("prompt_cost_usd", 0) - if "completion_cost_usd" in token_usage: - total_token_usage["completion_cost_usd"] = total_token_usage.get( - "completion_cost_usd", 0 - ) + token_usage.get("completion_cost_usd", 0) - - analysis_timestamp = datetime.now() - json_result = { - "analysis_type": "investment_recommendations", - "recommendations_content": recommendations_result.recommendations, - "analysis_timestamp": analysis_timestamp.isoformat(), - "analysis_date": analysis_timestamp.strftime("%Y-%m-%d"), - "analysis_time": analysis_timestamp.strftime("%H:%M:%S"), - "backtest_date": backtest_date, - "model_provider": config.model_provider, - "model_name": config.model_name, - "personality": config.personality, - "data_sources": { - "economy_state_table": "backtest_economy_state_analysis", - "relationship_analysis_table": "backtest_asset_class_relationship_analysis", - }, - "dagster_run_id": context.run_id, - "dagster_asset_key": str(context.asset_key), - } - - all_results.append(json_result) + context.log.info( + f"Successfully processed {backtest_date}, writing result to database..." + ) + md.write_results_to_table( + [json_result], + output_table="backtest_investment_recommendations", + if_exists="append", + context=context, + ) + context.log.info(f"Result for {backtest_date} written to database") + + except Exception as e: + error_msg = f"Error processing backtest date {backtest_date}: {str(e)}" + context.log.error(error_msg) + failed_dates.append({"date": backtest_date, "error": str(e)}) + context.log.warning( + f"Continuing with remaining dates. {len(successful_dates)} successful, {len(failed_dates)} failed so far." + ) if ( "total_cost_usd" not in total_token_usage @@ -270,27 +364,33 @@ def backtest_generate_investment_recommendations( ) total_token_usage.update(cost_data) + if len(all_results) == 0: + error_msg = f"No backtest dates were successfully processed. All {len(backtest_dates)} date(s) failed." + context.log.error(error_msg) + raise ValueError(error_msg) + context.log.info( - f"Writing {len(all_results)} backtest investment recommendations records to database..." - ) - md.write_results_to_table( - all_results, - output_table="backtest_investment_recommendations", - if_exists="append", - context=context, + f"Processed {len(successful_dates)} successful date(s), {len(failed_dates)} failed date(s). " + f"All successful results have been written to database." ) - # Create summary metadata from first result first_result = all_results[0] - recommendations_summary = extract_recommendations_summary( - first_result["recommendations_content"] + recommendations_content = first_result.get("recommendations_content") or "" + recommendations_summary = ( + extract_recommendations_summary(recommendations_content) + if recommendations_content + else {} ) result_metadata = { - "analysis_completed": True, + "analysis_completed": len(failed_dates) == 0, "analysis_timestamp": first_result["analysis_timestamp"], "backtest_dates_processed": backtest_dates, + "successful_dates": successful_dates, + "failed_dates": failed_dates, "num_dates_processed": len(backtest_dates), + "num_successful": len(successful_dates), + "num_failed": len(failed_dates), "model_provider": config.model_provider, "model_name": config.model_name, "personality": config.personality, @@ -298,9 +398,9 @@ def backtest_generate_investment_recommendations( "records_written": len(all_results), "data_sources": first_result["data_sources"], "recommendations_summary": recommendations_summary, - "recommendations_preview": first_result["recommendations_content"][:500] - if first_result["recommendations_content"] - else "", + "recommendations_preview": ( + recommendations_content[:500] if recommendations_content else "" + ), "token_usage": total_token_usage, "provider": economic_analysis._get_provider(), } @@ -308,4 +408,20 @@ def backtest_generate_investment_recommendations( context.log.info( f"Backtest investment recommendations generation complete: {result_metadata}" ) + + if len(failed_dates) > 0: + degraded_summary = ( + f"Backtest completed with {len(failed_dates)} failure(s) out of {len(backtest_dates)} total date(s). " + f"Successful results have been saved. Asset status: DEGRADED (partial success)." + ) + context.log.warning(degraded_summary) + result_metadata["status"] = "degraded" + result_metadata["degraded_reason"] = ( + f"{len(failed_dates)} of {len(backtest_dates)} dates failed" + ) + result_metadata["degraded_failed_dates"] = [f["date"] for f in failed_dates] + result_metadata["partial_success"] = True + return dg.MaterializeResult(metadata=result_metadata) + + result_metadata["status"] = "success" return dg.MaterializeResult(metadata=result_metadata) diff --git a/macro_agents/src/macro_agents/defs/agents/backtest_optimizer.py b/macro_agents/src/macro_agents/defs/agents/backtest_optimizer.py index 1ac04f1..f600023 100644 --- a/macro_agents/src/macro_agents/defs/agents/backtest_optimizer.py +++ b/macro_agents/src/macro_agents/defs/agents/backtest_optimizer.py @@ -340,6 +340,15 @@ def optimize_dspy_modules( model_name_override=config.model_name, ) + from macro_agents.defs.agents.economy_state_analyzer import ( + _get_token_usage, + _calculate_cost, + ) + + initial_history_length = ( + len(economic_analysis._lm.history) if hasattr(economic_analysis, "_lm") else 0 + ) + results = {} personalities_to_test = [] @@ -704,7 +713,26 @@ def optimize_dspy_modules( "error": str(e), } - return dg.MaterializeResult(metadata={"optimization_results": results}) + token_usage = _get_token_usage(economic_analysis, initial_history_length, context) + + if "total_cost_usd" not in token_usage or token_usage.get("total_cost_usd", 0) == 0: + provider = economic_analysis._get_provider() + model_name = economic_analysis._get_model_name() + cost_data = _calculate_cost( + provider=provider, + model_name=model_name, + prompt_tokens=token_usage.get("prompt_tokens", 0), + completion_tokens=token_usage.get("completion_tokens", 0), + ) + token_usage.update(cost_data) + + return dg.MaterializeResult( + metadata={ + "optimization_results": results, + "token_usage": token_usage, + "provider": economic_analysis._get_provider(), + } + ) @dg.asset( diff --git a/macro_agents/src/macro_agents/defs/agents/backtest_utils.py b/macro_agents/src/macro_agents/defs/agents/backtest_utils.py index 46f580a..88a3c36 100644 --- a/macro_agents/src/macro_agents/defs/agents/backtest_utils.py +++ b/macro_agents/src/macro_agents/defs/agents/backtest_utils.py @@ -33,14 +33,26 @@ def extract_recommendations(recommendations_content: str) -> List[Dict[str, Any] confidence_match = re.search( r"confidence[:\s]+([0-9.]+)", context, re.IGNORECASE ) - confidence = float(confidence_match.group(1)) if confidence_match else None - if confidence and confidence > 1: - confidence = confidence / 100 + confidence = None + if confidence_match: + confidence_str = confidence_match.group(1).rstrip(".,;") + try: + confidence = float(confidence_str) + if confidence > 1: + confidence = confidence / 100 + except (ValueError, AttributeError): + confidence = None return_match = re.search( - r"(?:expected|return)[:\s]+([0-9.]+)%?", context, re.IGNORECASE + r"(?:expected|return)[:\s]+([-0-9.]+)%?", context, re.IGNORECASE ) - expected_return = float(return_match.group(1)) if return_match else None + expected_return = None + if return_match: + return_str = return_match.group(1).rstrip(".,;%") + try: + expected_return = float(return_str) + except (ValueError, AttributeError): + expected_return = None recommendations.append( { @@ -66,14 +78,26 @@ def extract_recommendations(recommendations_content: str) -> List[Dict[str, Any] confidence_match = re.search( r"confidence[:\s]+([0-9.]+)", context, re.IGNORECASE ) - confidence = float(confidence_match.group(1)) if confidence_match else None - if confidence and confidence > 1: - confidence = confidence / 100 + confidence = None + if confidence_match: + confidence_str = confidence_match.group(1).rstrip(".,;") + try: + confidence = float(confidence_str) + if confidence > 1: + confidence = confidence / 100 + except (ValueError, AttributeError): + confidence = None return_match = re.search( - r"(?:expected|return)[:\s]+([0-9.]+)%?", context, re.IGNORECASE + r"(?:expected|return)[:\s]+([-0-9.]+)%?", context, re.IGNORECASE ) - expected_return = float(return_match.group(1)) if return_match else None + expected_return = None + if return_match: + return_str = return_match.group(1).rstrip(".,;%") + try: + expected_return = float(return_str) + except (ValueError, AttributeError): + expected_return = None recommendations.append( { diff --git a/macro_agents/src/macro_agents/defs/agents/economy_state_analyzer.py b/macro_agents/src/macro_agents/defs/agents/economy_state_analyzer.py index d4a6bbd..2eaec09 100644 --- a/macro_agents/src/macro_agents/defs/agents/economy_state_analyzer.py +++ b/macro_agents/src/macro_agents/defs/agents/economy_state_analyzer.py @@ -1434,6 +1434,9 @@ def extract_economy_state_summary(analysis_content: str) -> Dict[str, Any]: """Extract key insights from economy state analysis for metadata.""" summary = {} + if not analysis_content: + return summary + cycle_match = re.search( r"(?:Current Economic Cycle Position|Cycle Position|Economic Cycle):\s*([^.\n]+)", analysis_content, diff --git a/macro_agents/src/macro_agents/defs/agents/investment_recommendations.py b/macro_agents/src/macro_agents/defs/agents/investment_recommendations.py index c6ad9b2..438178d 100644 --- a/macro_agents/src/macro_agents/defs/agents/investment_recommendations.py +++ b/macro_agents/src/macro_agents/defs/agents/investment_recommendations.py @@ -146,6 +146,11 @@ def extract_recommendations_summary(recommendations_content: str) -> Dict[str, A """Extract key insights from investment recommendations for metadata.""" summary = {} + if not recommendations_content: + summary["total_overweight_count"] = 0 + summary["total_underweight_count"] = 0 + return summary + outlook_match = re.search( r"(?:market outlook|outlook)[:\s]+(bullish|bearish|neutral|positive|negative)", recommendations_content, diff --git a/macro_agents/tests/test_dspy_modules.py b/macro_agents/tests/test_dspy_modules.py index dd22283..404197b 100644 --- a/macro_agents/tests/test_dspy_modules.py +++ b/macro_agents/tests/test_dspy_modules.py @@ -473,3 +473,174 @@ def test_asset_class_relationship_module_with_mock_lm(self): assert result.relationship_analysis == "Strong correlation between tech and GDP" assert module.analyze_relationships.called + + +class TestExtractFunctionsWithEmptyContent: + """Test cases for extract functions handling None/empty content.""" + + def test_extract_economy_state_summary_with_none(self): + """Test extract_economy_state_summary handles None content.""" + from macro_agents.defs.agents.economy_state_analyzer import ( + extract_economy_state_summary, + ) + + result = extract_economy_state_summary(None) + assert isinstance(result, dict) + assert len(result) == 0 + + def test_extract_economy_state_summary_with_empty_string(self): + """Test extract_economy_state_summary handles empty string.""" + from macro_agents.defs.agents.economy_state_analyzer import ( + extract_economy_state_summary, + ) + + result = extract_economy_state_summary("") + assert isinstance(result, dict) + assert len(result) == 0 + + def test_extract_economy_state_summary_with_valid_content(self): + """Test extract_economy_state_summary with valid content.""" + from macro_agents.defs.agents.economy_state_analyzer import ( + extract_economy_state_summary, + ) + + content = """ + Current Economic Cycle Position: Expansion + Confidence: 0.75 + Risk Factors: Inflation concerns, geopolitical tensions + """ + result = extract_economy_state_summary(content) + assert isinstance(result, dict) + assert "economic_cycle_position" in result + assert result["economic_cycle_position"] == "Expansion" + assert "confidence_level" in result + assert result["confidence_level"] == 0.75 + + def test_extract_relationship_summary_with_none(self): + """Test extract_relationship_summary handles None content.""" + from macro_agents.defs.agents.asset_class_relationship_analyzer import ( + extract_relationship_summary, + ) + + result = extract_relationship_summary(None) + assert isinstance(result, dict) + assert len(result) == 0 + + def test_extract_relationship_summary_with_empty_string(self): + """Test extract_relationship_summary handles empty string.""" + from macro_agents.defs.agents.asset_class_relationship_analyzer import ( + extract_relationship_summary, + ) + + result = extract_relationship_summary("") + assert isinstance(result, dict) + assert len(result) == 0 + + def test_extract_recommendations_summary_with_none(self): + """Test extract_recommendations_summary handles None content.""" + from macro_agents.defs.agents.investment_recommendations import ( + extract_recommendations_summary, + ) + + result = extract_recommendations_summary(None) + assert isinstance(result, dict) + assert "total_overweight_count" in result + assert "total_underweight_count" in result + assert result["total_overweight_count"] == 0 + assert result["total_underweight_count"] == 0 + + def test_extract_recommendations_summary_with_empty_string(self): + """Test extract_recommendations_summary handles empty string.""" + from macro_agents.defs.agents.investment_recommendations import ( + extract_recommendations_summary, + ) + + result = extract_recommendations_summary("") + assert isinstance(result, dict) + assert "total_overweight_count" in result + assert "total_underweight_count" in result + assert result["total_overweight_count"] == 0 + assert result["total_underweight_count"] == 0 + + +class TestExtractRecommendationsFloatParsing: + """Test cases for extract_recommendations handling trailing punctuation in floats.""" + + def test_extract_recommendations_with_trailing_period(self): + """Test extract_recommendations handles confidence with trailing period.""" + from macro_agents.defs.agents.backtest_utils import extract_recommendations + + content = "OVERWEIGHT XLK with confidence 0.6. Expected return 5.2%." + recommendations = extract_recommendations(content) + + assert len(recommendations) > 0 + xlk_rec = next((r for r in recommendations if r["symbol"] == "XLK"), None) + assert xlk_rec is not None + assert xlk_rec["direction"] == "OVERWEIGHT" + assert xlk_rec["confidence"] == 0.6 + assert xlk_rec["expected_return"] == 5.2 + + def test_extract_recommendations_with_trailing_comma(self): + """Test extract_recommendations handles confidence with trailing comma.""" + from macro_agents.defs.agents.backtest_utils import extract_recommendations + + content = "OVERWEIGHT SPY with confidence 0.75, expected return 8.3%" + recommendations = extract_recommendations(content) + + assert len(recommendations) > 0 + spy_rec = next((r for r in recommendations if r["symbol"] == "SPY"), None) + assert spy_rec is not None + assert spy_rec["confidence"] == 0.75 + assert spy_rec["expected_return"] == 8.3 + + def test_extract_recommendations_with_trailing_semicolon(self): + """Test extract_recommendations handles confidence with trailing semicolon.""" + from macro_agents.defs.agents.backtest_utils import extract_recommendations + + content = "UNDERWEIGHT XLE with confidence 0.4; expected return -2.1%" + recommendations = extract_recommendations(content) + + assert len(recommendations) > 0 + xle_rec = next((r for r in recommendations if r["symbol"] == "XLE"), None) + assert xle_rec is not None + assert xle_rec["direction"] == "UNDERWEIGHT" + assert xle_rec["confidence"] == 0.4 + assert xle_rec["expected_return"] == -2.1 + + def test_extract_recommendations_with_percent_sign(self): + """Test extract_recommendations handles return values with percent sign.""" + from macro_agents.defs.agents.backtest_utils import extract_recommendations + + content = "OVERWEIGHT QQQ confidence 0.8 expected return 12.5%" + recommendations = extract_recommendations(content) + + assert len(recommendations) > 0 + qqq_rec = next((r for r in recommendations if r["symbol"] == "QQQ"), None) + assert qqq_rec is not None + assert qqq_rec["expected_return"] == 12.5 + + def test_extract_recommendations_with_invalid_float_gracefully(self): + """Test extract_recommendations handles invalid float strings gracefully.""" + from macro_agents.defs.agents.backtest_utils import extract_recommendations + + content = "OVERWEIGHT XLK with confidence invalid. Expected return also.invalid" + recommendations = extract_recommendations(content) + + assert len(recommendations) > 0 + xlk_rec = next((r for r in recommendations if r["symbol"] == "XLK"), None) + assert xlk_rec is not None + assert xlk_rec["confidence"] is None + assert xlk_rec["expected_return"] is None + + def test_extract_recommendations_with_multiple_trailing_punctuation(self): + """Test extract_recommendations handles multiple trailing punctuation.""" + from macro_agents.defs.agents.backtest_utils import extract_recommendations + + content = "OVERWEIGHT DIA confidence 0.9., expected return 6.7%." + recommendations = extract_recommendations(content) + + assert len(recommendations) > 0 + dia_rec = next((r for r in recommendations if r["symbol"] == "DIA"), None) + assert dia_rec is not None + assert dia_rec["confidence"] == 0.9 + assert dia_rec["expected_return"] == 6.7