Multi-Agent System Architecture
Multi-Factor (Smart Beta) Screening
Instead of relying on a single metric, multi-factor models combine several distinct characteristics to score and rank a universe of stocks. A typical model creates a composite score blending Value, Quality, Momentum, and Low Volatility.
How it works: Each stock is given a percentile rank across all factors. Stocks scoring in the top decile or quintile form the portfolio, which is rebalanced periodically (e.g., monthly or quarterly).
Why it's valuable: Factors often behave cyclically (value might outperform in one economic regime while momentum leads in another). Combining them provides diversification and smoother risk-adjusted returns.
The pipeline uses a coordinated supervisor-worker pattern implemented in Python using the google-genai SDK.
Orchestrator Agent (Gemini)
1. Data Ingestion Agent (Fetches prices, SEC fundamentals, market caps)
2. Data Cleaning & Validation Agent (Handles missing values, outliers, winsorization)
3. Factor Calculation Agent (Computes Value, Quality, Momentum, Low-Vol metrics)
4. Scoring & Portfolio Construction Agent (Normalizes, weights, and ranks top S&P 500 stocks
Agent Roles & Responsibilities:
Orchestrator Agent: Manages state, triggers sequence steps, and validates that all 500 S&P tickers have successfully cleared each pipeline stage.
Data Ingestion Agent: Pulls historical price series and fundamental attributes (via yfinance or BigQuery/Cloud SQL).
Data Cleaning Agent: Identifies anomalous entries, checks for survivorship bias, and handles corporate actions (splits, dividends).
Factor Calculation Agent: Executes vectorized mathematical routines to construct raw factor tables.
Scoring & Ranking Agent: Applies cross-sectional z-scoring or percentile ranking, calculates the composite score, and outputs the final decile basket.
Comprehensive Field & Metric Dictionary
To power the 4-factor model across the S&P 500, the database schema and extraction pipeline must track specific fields:
Identifiers & Market Data Fields
ticker: Stock symbol (e.g., AAPL, MSFT)
date: Observation date (daily/monthly frequency)
market_cap: Share Price Total Shares Outstanding (used for liquidity and universe filtering)
adj_close: Split- and dividend-adjusted closing price (crucial for momentum calculations)
sector / industry: GICS classification (used for sector-neutral ranking if desired)
Factor 1: Value Fields
enterprise_value (EV): Market Cap + Total Debt - Cash & Cash Equivalents
ebitda: Earnings Before Interest, Taxes, Depreciation, and Amortization (trailing twelve months - TTM)
free_cash_flow (FCF): Operating Cash Flow minus Capital Expenditures
book_value_per_share: Total Shareholders' Equity / Shares Outstanding
Factor 2: Quality Fields
net_income: TTM Net Income
total_assets: Balance sheet total assets
gross_profit: Total Revenue - Cost of Goods Sold (COGS)
total_debt: Short-term debt + Long-term debt
shareholders_equity: Total assets minus total liabilities
Factor 3: Momentum Fields
return_12m: Total percentage return over the trailing 12 months
return_1m: Total percentage return over the most recent month (subtracted from 12M to avoid short-term reversal noise)
sma_200: 200-day Simple Moving Average (used for absolute trend filtering)
Factor 4: Low Volatility Fields
daily_returns: Series of daily percentage changes over the trailing 252 trading sessions
volatility_1yr: Annualized standard deviation of daily returns (252)
beta: Covariance of stock returns with the S&P 500 index variance over 1 years
Implementation Blueprint (google-genai & Python)
The following blueprint demonstrates how to orchestrate factor computations and leverage Gemini via the google-genai SDK to inspect data health and validate factor outputs.
1. Project Directory Structure
Organize your workspace to separate data connectors, agent logic, factor math, and execution scripts:

2. Dependencies (requirements.txt)
Ensure your environment includes the latest Google GenAI SDK and data science libraries:
Plaintext
google-genai>=1.0.0
pandas>=2.0.0
numpy>=1.24.0
yfinance>=0.2.30
sqlalchemy>=2.0.0
pymysql>=1.1.0
python-dotenv>=1.0.0
Install them in your terminal:
pip install -r requirements.txt
3. Core Implementation
A. Environment & Configuration (.env)
Store your Gemini API key securely:
GEMINI_API_KEY=your_actual_api_key_here
B. Gemini Validator Agent (agents/validator_agent.py)
This agent inspects raw financial metrics and factor distributions for skewness, missing values, or outliers before cross-sectional z-scoring is applied.
import os
import time
from google import genai
from google.genai import types
from google.genai.errors import ServerError
import pandas as pd
class FactorValidatorAgent:
def __init__(self):
# Automatically picks up GEMINI_API_KEY from environment variables
self.client = genai.Client()
self.model_name = "gemini-2.5-flash"
def audit_factor_distributions(self, df_summary: str, max_retries: int = 3, initial_delay: int = 2) -> str:
"""
Sends statistical summaries of the factor table to Gemini with
built-in retry and exponential backoff for 503 UNAVAILABLE errors.
"""
prompt = f"""
You are an expert quantitative risk and data engineering agent.
Analyze the following descriptive statistics of our S&P 500 factor model metrics:
{df_summary}
Identify any extreme outliers, heavy skewness, or negative valuation anomalies
(e.g., negative EV/EBITDA caused by operating losses) that would distort cross-sectional Z-scores.
Provide specific recommendations for winsorization percentiles (e.g., 1st and 99th percentile caps).
"""
delay = initial_delay
for attempt in range(1, max_retries + 1):
try:
response = self.client.models.generate_content(
model=self.model_name,
contents=prompt,
config=types.GenerateContentConfig(
temperature=0.1, # Low temperature for analytical consistency
system_instruction="You are a rigorous quantitative finance auditor."
)
)
return response.text
except ServerError as e:
print(f"[Warning] Gemini API unavailable (503) on attempt {attempt}/{max_retries}. Retrying in {delay}s...")
if attempt == max_retries:
return f"[Error] Validation audit bypassed: Gemini API remained unavailable after {max_retries} attempts. Error: {e}"
time.sleep(delay)
delay *= 2 # Exponential backoff
except Exception as e:
return f"[Error] Unexpected error during Gemini validation: {e}"
C. Factor Calculation & Scoring Engine (agents/factor_agent.py)
import pandas as pd
import numpy as np
class FactorCalculationEngine:
@staticmethod
def compute_factors(df: pd.DataFrame) -> pd.DataFrame:
"""
Computes raw metrics across the 4 core factor buckets.
"""
df = df.copy()
# 1. Value Factors (Lower is better for EV/EBITDA, higher for FCF yield)
df['ev_ebitda'] = df['enterprise_value'] / df['ebitda']
df['fcf_yield'] = df['free_cash_flow'] / df['market_cap']
# 2. Quality Factors (Higher is better)
df['roic'] = df['net_income'] / (df['total_debt'] + df['shareholders_equity'])
df['gross_profitability'] = df['gross_profit'] / df['total_assets']
# 3. Momentum Factor (12m return minus most recent month to avoid reversal)
df['momentum_score'] = df['return_12m'] - df['return_1m']
# 4. Low Volatility Factor (Inverse of annualized standard deviation)
df['inv_volatility'] = 1.0 / df['volatility_1yr']
return df
@staticmethod
def standardize_and_score(df: pd.DataFrame) -> pd.DataFrame:
"""
Winsorizes outliers, standardizes via Z-scores, and calculates composite score.
"""
df = df.copy()
# Helper for Z-score calculation
def get_zscore(series, invert=False):
mean = series.mean()
std = series.std()
if std == 0 or pd.isna(std):
return pd.Series(0, index=series.index)
z = (series - mean) / std
return -z if invert else z
# Winsorize raw values at 1st and 99th percentiles to protect against outliers
for col in ['ev_ebitda', 'fcf_yield', 'roic', 'gross_profitability', 'momentum_score', 'inv_volatility']:
lower = df[col].quantile(0.01)
upper = df[col].quantile(0.99)
df[col] = df[col].clip(lower, upper)
# Calculate Z-scores for individual metrics (Invert EV/EBITDA since lower valuation is better)
df['z_ev_ebitda'] = get_zscore(df['ev_ebitda'], invert=True)
df['z_fcf_yield'] = get_zscore(df['fcf_yield'])
df['z_value'] = (df['z_ev_ebitda'] + df['z_fcf_yield']) / 2.0
df['z_roic'] = get_zscore(df['roic'])
df['z_gp'] = get_zscore(df['gross_profitability'])
df['z_quality'] = (df['z_roic'] + df['z_gp']) / 2.0
df['z_momentum'] = get_zscore(df['momentum_score'])
df['z_low_vol'] = get_zscore(df['inv_volatility'])
# Equal-Weighted Composite Score (25% weight per factor bucket)
df['composite_score'] = (
0.25 * df['z_value'] +
0.25 * df['z_quality'] +
0.25 * df['z_momentum'] +
0.25 * df['z_low_vol']
)
# Sort descending to find top-ranking stocks
return df.sort_values(by='composite_score', ascending=False).reset_index(drop=True)
This module handles vectorized calculations for Value, Quality, Momentum, and Low Volatility, followed by cross-sectional standardization.
john_iacovacci1@cloudshell:~/sp500_quant_pipeline (sentiment-analysis-379200)$ python main.py
=== S&P 500 Multi-Factor Screening Pipeline Initialized ===
Loaded 8 securities for screening.
--- Running Gemini Factor Validation Audit ---
[Warning] Gemini API unavailable (503) on attempt 1/3. Retrying in 2s...
As an expert quantitative risk and data engineering agent, I've analyzed the provided descriptive statistics for your S&P 500 factor model metrics. It's crucial to note upfront that the sample size of **n=8** is extremely small. This limits the robustness of any conclusions regarding "heavy skewness" or "extreme outliers" as the descriptive statistics themselves may not be fully representative of the underlying distribution. However, I will proceed with the analysis based on the provided data and offer recommendations as requested, assuming these statistics are indicative of a larger, ongoing dataset.
---
### Overall Assessment
The small sample size makes it challenging to definitively identify "heavy" skewness or "extreme" outliers with high confidence. However, we can observe tendencies and potential issues. The primary goal is to ensure that cross-sectional Z-scores, which rely on mean and standard deviation, are not unduly distorted by a few extreme observations or highly asymmetric distributions.
---
### Factor-Specific Analysis and Anomalies
#### 1. `ev_ebitda` (Enterprise Value to EBITDA)
* **Statistics:**
* `mean`: 17.84, `median` (50%): 18.72
* `std`: 5.10
* `min`: 8.73, `max`: 24.23
* `25%`: 14.64, `75%`: 20.51
* **Skewness:** The mean (17.84) is slightly less than the median (18.72), and the lower quartile range (18.72 - 14.64 = 4.08) is larger than the upper quartile range (20.51 - 18.72 = 1.79). This suggests a **slight negative skewness**.
* **Outliers:** Using the 1.5 * IQR rule (IQR = 20.51 - 14.64 = 5.87):
* Lower bound: 14.64 - 1.5 * 5.87 = 5.84. The `min` (8.73) is above this.
* Upper bound: 20.51 + 1.5 * 5.87 = 29.32. The `max` (24.23) is below this.
* **Conclusion:** No strong evidence of extreme outliers in this small sample.
* **Negative Valuation Anomalies:** All values are positive. However, `ev_ebitda` can be negative if EBITDA is negative (i.e., the company is operating at a loss). Negative EV/EBITDA values are problematic for Z-scoring because they would typically be interpreted as extremely "cheap" (low Z-score), which is counter-intuitive for a company with negative operating profit. Such values would severely distort the distribution and Z-scores.
#### 2. `roic` (Return on Invested Capital)
* **Statistics:**
* `mean`: 0.231, `median` (50%): 0.184
* `std`: 0.138
* `min`: 0.080, `max`: 0.528
* `25%`: 0.162, `75%`: 0.260
* **Skewness:** The mean (0.231) is significantly greater than the median (0.184), and the upper quartile range (0.260 - 0.184 = 0.076) is much larger than the lower quartile range (0.184 - 0.162 = 0.022). This indicates **clear positive skewness**.
* **Outliers:** Using the 1.5 * IQR rule (IQR = 0.260 - 0.162 = 0.098):
* Lower bound: 0.162 - 1.5 * 0.098 = 0.015. The `min` (0.080) is above this.
* Upper bound: 0.260 + 1.5 * 0.098 = 0.407. The `max` (0.528) is **above this upper bound**.
* **Conclusion:** The maximum value of `0.528` is a **potential extreme outlier** on the high side, which is contributing to the positive skewness.
* **Negative Valuation Anomalies:** All values are positive. ROIC can be negative if the company's Net Operating Profit After Tax (NOPAT) is negative. Negative ROIC values would indicate value destruction and should typically be ranked very low or handled specifically to avoid misinterpretation in a quality factor.
#### 3. `momentum_score`
* **Statistics:**
* `mean`: 0.179, `median` (50%): 0.185
* `std`: 0.092
* `min`: 0.060, `max`: 0.300
* `25%`: 0.103, `75%`: 0.253
* **Skewness:** The mean (0.179) is slightly less than the median (0.185), and the lower quartile range (0.185 - 0.103 = 0.082) is slightly larger than the upper quartile range (0.253 - 0.185 = 0.068). This suggests a **slight negative skewness**.
* **Outliers:** Using the 1.5 * IQR rule (IQR = 0.253 - 0.103 = 0.150):
* Lower bound: 0.103 - 1.5 * 0.150 = -0.122. The `min` (0.060) is above this.
* Upper bound: 0.253 + 1.5 * 0.150 = 0.478. The `max` (0.300) is below this.
* **Conclusion:** No strong evidence of extreme outliers in this small sample.
#### 4. `inv_volatility` (Inverse Volatility)
* **Statistics:**
* `mean`: 5.032, `median` (50%): 4.773
* `std`: 1.222
* `min`: 3.571, `max`: 7.143
* `25%`: 4.222, `75%`: 5.729
* **Skewness:** The mean (5.032) is slightly greater than the median (4.773), and the upper quartile range (5.729 - 4.773 = 0.956) is larger than the lower quartile range (4.773 - 4.222 = 0.551). This suggests **slight positive skewness**.
* **Outliers:** Using the 1.5 * IQR rule (IQR = 5.729 - 4.222 = 1.507):
* Lower bound: 4.222 - 1.5 * 1.507 = 1.962. The `min` (3.571) is above this.
* Upper bound: 5.729 + 1.5 * 1.507 = 7.989. The `max` (7.143) is below this.
* **Conclusion:** No strong evidence of extreme outliers in this small sample.
---
### Distortion of Cross-Sectional Z-Scores
* **Outliers:** The potential outlier in `roic` (max value of 0.528) will significantly inflate the standard deviation for that factor. This inflation will compress the Z-scores of all other observations towards zero, reducing their relative spread and discriminatory power.
* **Skewness:** Skewed distributions mean that Z-scores will not accurately reflect percentiles. For positively skewed data (like `roic` and `inv_volatility`), a Z-score of +1 might correspond to a much higher percentile than 84%, and a Z-score of -1 might correspond to a much lower percentile than 16%. This can lead to misranking of securities.
* **Negative Valuation Anomalies (e.g., negative EV/EBITDA):** If negative values were present for `ev_ebitda` or `roic`, they would cause severe distortion. A negative EV/EBITDA, for instance, would result in a very low (negative) Z-score, implying extreme "cheapness" when in reality it signifies operational distress. This would fundamentally misrepresent the factor's intent.
---
### Recommendations for Winsorization Percentiles
Winsorization is a robust technique to mitigate the impact of extreme values and heavy skewness without discarding data points entirely. Given the small sample size (n=8), the specific percentiles (e.g., 1st or 99th) would effectively correspond to the min/max values within this tiny dataset. However, assuming these statistics are a snapshot of a larger, ongoing data stream, I recommend standard winsorization percentiles as a general best practice for factor construction.
**General Recommendation:** For all factors, I recommend winsorizing at the **1st and 99th percentiles**. This approach is widel
* **Winsorize at the 1st and 99th percentiles.** This will cap extremely high inverse volatility (meaning extremely low actual volatility) and extremely low inverse volatility (meaning extremely high actual volatility), ensuring that the Z-scores are not overly influenced by these extremes.
---
**Important Caveat:** The recommendations are based on the assumption that these 8 data points are a small sample from a much larger, ongoing dataset. If `n=8` represents the *entire* universe of data, then applying 1st/99th percentile winsorization would effectively cap the min/max of these 8 points, which is not the typical use case for winsorization. For such a tiny dataset, a more direct approach might be to manually inspect and adjust the single outlier (e.g., for `roic`) or consider non-parametric ranking methods instead of Z-scores. However, for a robust factor model, winsorization on a larger dataset is standard.
--- Top Screened Stocks (Composite Score Ranking) ---
ticker composite_score z_value z_quality z_momentum z_low_vol
0 AAPL 0.175839 -1.133893 1.672763 0.564533 -0.400046
1 GOOGL 0.130102 -0.217981 0.518525 0.784089 -0.564226
2 MSFT 0.089019 -1.059942 0.544584 0.893867 -0.022431
3 AMZN 0.023677 -0.443991 0.217450 1.302240 -0.980991
4 JNJ -0.040789 -0.253001 -0.329662 -1.286323 1.705830
john_iacovacci1@cloudshell:~/sp500_quant_pipeline (sentiment-analysis-379200)$
john_iacovacci1@cloudshell:~/sp500_quant_pipeline (sentiment-analysis-379200)$
No comments:
Post a Comment