Skip to content

Why Did Atlas-GIC's AI Trading System Underperform the S&P500?

I spent 18 months building a sophisticated 25-agent AI trading system. It generated a respectable +22% return over 378 trading days. But when I compared it to the S&P500’s +24% return over the same period, I had to face an uncomfortable truth: all that complexity underperformed a simple buy-and-hold strategy.

This is the postmortem of what went wrong and the hard lessons I learned about optimizing for the wrong metrics.

The Uncomfortable Numbers

Let me start with the raw comparison:

performance-comparison.txt
┌─────────────────────────────────────────────────────────────┐
│ PERFORMANCE SUMMARY │
├─────────────────────────────────────────────────────────────┤
│ AI Trading System (25 agents) │
│ • Total Return: +22% │
│ • Period: 378 trading days (~18 months) │
│ • Trading Activity: Constant daily trades │
│ • Computational Cost: Significant API tokens │
│ │
│ S&P500 Index (Buy and Hold) │
│ • Total Return: +24% │
│ • Period: Same 378 trading days │
│ • Trading Activity: 0 trades (buy once, hold) │
│ • Computational Cost: $0 │
├─────────────────────────────────────────────────────────────┤
│ VERDICT: AI system underperformed by 2 percentage points │
│ despite massive complexity │
└─────────────────────────────────────────────────────────────┘

The Reddit comment that stung the most:

“Despite all of these tokens spent, it does not appear to have gained a better total return than the index”

They were right. I had optimized for the wrong metric.

The Sharpe Ratio Trap

Here’s where I went wrong. I had been optimizing for Sharpe ratio, not total return.

sharpe_ratio_analysis.py
import numpy as np
from dataclasses import dataclass
from typing import List
def calculate_sharpe_ratio(returns: List[float], risk_free_rate: float = 0.04) -> float:
"""
Sharpe ratio = (Return - Risk-Free Rate) / Standard Deviation
A higher Sharpe means better risk-adjusted returns, NOT higher absolute returns.
"""
returns_arr = np.array(returns)
excess_returns = returns_arr - (risk_free_rate / 252) # Daily risk-free rate
# Annualized Sharpe
sharpe = np.sqrt(252) * np.mean(excess_returns) / np.std(excess_returns)
return sharpe
# Compare two scenarios
scenario_a = {
"name": "AI Trading System",
"total_return": 0.22, # +22%
"volatility": 0.12, # Lower volatility
"sharpe_ratio": 1.50 # Good risk-adjusted return
}
scenario_b = {
"name": "S&P500 Index",
"total_return": 0.24, # +24%
"volatility": 0.18, # Higher volatility
"sharpe_ratio": 1.11 # Lower risk-adjusted return
}
# The AI system has BETTER Sharpe but WORSE absolute return!
# This is the trap of optimizing for the wrong metric.

My system had a Sharpe ratio of ~1.50 versus the S&P500’s ~1.11. By academic standards, my system looked better. But for an individual trader managing their own capital, total return is what matters.

sharpe-vs-return-diagram.txt
Total Return Sharpe Ratio
───────────── ─────────────
S&P500: 24% ████████████ S&P500: 1.11 ████████████
AI Sys: 22% ███████████ AI Sys: 1.50 ████████████████
↑ ↑
│ │
BETTER for WORSE for
individual traders individual traders

The problem? I was solving the wrong problem. I had designed my system for institutional metrics when I should have been designing for individual wealth creation.

The Hidden Costs of Active Trading

Every trade costs money. In my multi-agent system, the agents were constantly debating, rebalancing, and adjusting positions. This activity created a significant cost burden:

transaction_cost_analysis.py
from dataclasses import dataclass
@dataclass
class TradingCosts:
commission_per_trade: float = 0.0005 # 5 bps per trade
bid_ask_spread: float = 0.001 # 10 bps spread
market_impact: float = 0.0002 # 2 bps market impact
slippage: float = 0.0003 # 3 bps slippage
total_per_round_trip: float = 0.004 # ~40 bps per round trip
def calculate_breakeven_alpha(
trades_per_day: int,
trading_days: int = 378,
cost_per_trade: float = 0.004
) -> dict:
"""
Calculate the minimum alpha needed to break even after costs.
For a system trading multiple times per day, the cost burden is massive.
"""
total_trades = trades_per_day * trading_days
total_cost = total_trades * cost_per_trade
# Annualized cost
annualized_cost = total_cost * (252 / trading_days)
return {
"total_trades": total_trades,
"total_cost_pct": total_cost * 100,
"annualized_cost_pct": annualized_cost * 100,
"breakeven_alpha_needed": annualized_cost
}
# Example: If system trades 5 times per day
result = calculate_breakeven_alpha(trades_per_day=5)
# Total cost: ~7.5% of portfolio value
# The system needs to generate 7.5% alpha JUST to break even!
print(f"Breakeven alpha needed: {result['breakeven_alpha_needed']*100:.2f}%")

The calculation revealed another hidden cost layer:

cost-breakdown.txt
┌────────────────────────────────────────────────────────┐
│ HIDDEN COSTS OF ACTIVE TRADING │
├────────────────────────────────────────────────────────┤
│ │
│ Transaction Costs (5 trades/day × 378 days) │
│ • Round-trip cost: ~40 bps per trade │
│ • Total: ~7.5% of portfolio │
│ │
│ Computational Costs (25 agents × daily analysis) │
│ • Tokens per analysis: ~50,000 │
│ • Cost per 1k tokens: $0.003 │
│ • Monthly: ~$82.50 │
│ • 18-month total: ~$1,485 │
│ │
│ Time Cost (Development & Maintenance) │
│ • Initial development: ~200 hours │
│ • Ongoing maintenance: ~5 hours/week │
│ • Total: ~500+ hours over 18 months │
│ │
├────────────────────────────────────────────────────────┤
│ TOTAL COST BURDEN: ~10%+ of returns │
└────────────────────────────────────────────────────────┘

The Single Outlier Problem

One comment on the Reddit discussion caught my attention:

“with AVGO called at $152 for +128%”

I went back to analyze my trade attribution:

outlier_analysis.py
from dataclasses import dataclass
from typing import List
@dataclass
class PerformanceAttribution:
"""
Analyzes whether performance comes from skill or a single lucky bet.
"""
trades: List[dict]
def analyze_outlier_impact(self) -> dict:
"""
Check if one trade accounts for most of the returns.
"""
sorted_trades = sorted(self.trades, key=lambda x: x["return"], reverse=True)
total_pnl = sum(t["pnl"] for t in self.trades)
best_trade_pnl = sorted_trades[0]["pnl"]
top_3_pnl = sum(t["pnl"] for t in sorted_trades[:3])
return {
"total_trades": len(self.trades),
"best_trade_pct_of_total": best_trade_pnl / total_pnl,
"top_3_trades_pct_of_total": top_3_pnl / total_pnl,
"outlier_dependency": best_trade_pnl / total_pnl > 0.3 # Warning if > 30%
}
# If AVGO contributed 128% gain but overall portfolio only made 22%,
# it means other trades were net negative!
# This is a red flag for strategy robustness

The analysis revealed that one trade (AVGO) contributed a disproportionate amount of the returns. Without that single position, my system would have significantly underperformed even more.

What I Should Have Done Differently

1. Define the Right Objective Function

correct_objective.py
import numpy as np
# WRONG: Optimize for Sharpe (what I did)
def wrong_objective(returns: list, benchmark: list) -> float:
"""Sharpe ratio can be gamed and doesn't match user goals."""
excess_returns = np.array(returns) - 0.04/252
return np.sqrt(252) * np.mean(excess_returns) / np.std(excess_returns)
# RIGHT: Optimize for user-relevant metrics (what I should do)
def right_objective(returns: list, benchmark: list, costs: dict) -> float:
"""
Multi-objective optimization that considers:
1. Absolute return vs benchmark
2. Risk-adjusted return
3. After-cost performance
4. Drawdown limits
"""
absolute_return = (1 + np.array(returns)).prod() - 1
benchmark_return = (1 + np.array(benchmark)).prod() - 1
alpha = absolute_return - benchmark_return
# Penalize underperformance heavily
if alpha < 0:
return alpha * 2 # Double penalty for underperformance
# Reward outperformance with diminishing returns
excess_returns = np.array(returns) - 0.04/252
sharpe = np.sqrt(252) * np.mean(excess_returns) / np.std(excess_returns)
return np.log(1 + max(0, alpha)) + sharpe * 0.1

2. Build Complexity Incrementally

The mistake was jumping straight to 25 agents. I should have validated each stage:

incremental-complexity.txt
Stage 1: Single Agent (3 months)
└── Benchmark: Beat S&P500 by 2%+ after costs
└── If FAIL: Stop, don't add complexity
└── If PASS: Proceed to Stage 2
Stage 2: 3 Agents with Sector Analysis (3 months)
└── Benchmark: Improve on Stage 1 by 1%+
└── If FAIL: Revert to Stage 1
└── If PASS: Proceed to Stage 3
Stage 3: 5 Agents with Debate Layer (3 months)
└── Benchmark: Reduce max drawdown by 20%+
└── If FAIL: Revert to Stage 2
└── If PASS: Proceed to Stage 4
Stage 4: 25 Agents Full System (6 months)
└── Benchmark: Beat S&P500 by 5%+ with lower volatility
└── If FAIL: Investigate why complexity doesn't add value

3. Track Comprehensive Metrics (Not Just Sharpe)

performance_dashboard.py
from dataclasses import dataclass
@dataclass
class PerformanceDashboard:
"""
Track everything that matters, not just Sharpe.
"""
# Return metrics
total_return: float
benchmark_return: float
alpha: float # THIS is what matters most
# Risk metrics
sharpe_ratio: float
sortino_ratio: float
max_drawdown: float
# Cost metrics
total_transaction_costs: float
total_api_costs: float
developer_time_hours: float
total_cost_pct: float
# Complexity metrics
number_of_agents: int
daily_trades: int
def is_worthwhile(self) -> dict:
"""
Answer: Would passive investing have been better?
"""
passive_return = self.benchmark_return
active_effort = self.total_return - self.total_cost_pct
benefit = active_effort - passive_return
return {
"passive_would_have_returned": passive_return,
"active_actually_returned": active_effort,
"benefit_of_complexity": benefit,
"verdict": "WORTHWHILE" if benefit > 0.02 else "NOT_WORTHWHILE",
"hourly_rate": benefit * 100000 / max(self.developer_time_hours, 1)
}

4. Implement Automatic Kill Switches

kill_switch.py
import numpy as np
class PerformanceKillSwitch:
"""
Automatic shutdown when system underperforms.
"""
def __init__(self, benchmark_return: float, max_underperformance: float = -0.02):
self.benchmark = benchmark_return
self.max_underperformance = max_underperformance
self.rolling_returns = []
self.window = 63 # 3 months of trading days
def check_performance(self, current_return: float) -> dict:
"""
Check if system should be shut down.
"""
self.rolling_returns.append(current_return)
if len(self.rolling_returns) < self.window:
return {"status": "monitoring", "action": "continue"}
recent_return = (1 + np.array(self.rolling_returns[-self.window:])).prod() - 1
underperformance = recent_return - self.benchmark
if underperformance < self.max_underperformance:
return {
"status": "kill_switch_triggered",
"action": "halt_trading",
"reason": f"Underperforming by {abs(underperformance)*100:.1f}%",
"recommendation": "Revert to passive index investing"
}
return {"status": "operational", "action": "continue"}

The Hard Truth

After 18 months and hundreds of hours of development, I had to face reality:

final-comparison.txt
┌────────────────────────────────────────────────────────────┐
│ THE BRUTAL COMPARISON │
├────────────────────────────────────────────────────────────┤
│ │
│ Option A: Build 25-Agent AI Trading System │
│ • Time invested: 500+ hours │
│ • API costs: ~$1,500 │
│ • Return: +22% │
│ • Stress level: HIGH │
│ │
│ Option B: Buy S&P500 Index Fund │
│ • Time invested: 10 minutes │
│ • Costs: ~$10 (expense ratio) │
│ • Return: +24% │
│ • Stress level: ZERO │
│ │
├────────────────────────────────────────────────────────────┤
│ CONCLUSION: Option B wins on every metric that matters │
└────────────────────────────────────────────────────────────┘

Key Lessons for AI Trading System Builders

Before building a multi-agent trading system, ask yourself:

  1. What’s my benchmark? If you can’t beat passive index investing, why build the system?

  2. What return do I need to justify the complexity? Add up all costs (transaction, API, time) and set a minimum return threshold.

  3. Am I optimizing for the right metric? Sharpe ratio is great for institutions. Total return is what matters for individuals.

  4. How will I know if I should shut it down? Define kill switches before you start.

The burden of proof is on the system builder. Complexity must earn its keep.

References

Final Words + More Resources

My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me

Here are also the most important links from this article along with some further resources that will help you in this scope:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments