Add Centillion Investment Partners trading agents setup

- centillion_config.py: Custom config for long-only SMID cap fund (US + EU),
  portfolio/watchlist tickers, benchmark definitions, Anthropic Claude as LLM
- centillion_run.py: Batch analysis runner with CLI flags for ticker/region/date
  filtering, outputs JSON results with buy/sell/hold signals
- centillion_portfolio_tracker.py: Virtual portfolio tracker for paper trading
  with position sizing, NAV tracking, and performance reporting

https://claude.ai/code/session_01Wq18jXYiJNdb9po7EzmnUZ
This commit is contained in:
Claude 2026-03-22 15:03:18 +00:00
parent f362a160c3
commit cd2236079f
No known key found for this signature in database
3 changed files with 609 additions and 0 deletions

93
centillion_config.py Normal file
View File

@ -0,0 +1,93 @@
"""
Centillion Investment Partners - TradingAgents Configuration
Tailored for a long-only SMID cap fund focused on US and European stocks.
Two modes:
1. Virtual portfolio tracking (paper trading to test alpha generation)
2. Buy/sell recommendation engine for active portfolio + watchlist
"""
import os
from tradingagents.default_config import DEFAULT_CONFIG
# ---------------------------------------------------------------------------
# Base config — inherit defaults and override for Centillion's needs
# ---------------------------------------------------------------------------
CENTILLION_CONFIG = DEFAULT_CONFIG.copy()
# LLM provider — change to "anthropic" or "google" if you prefer
CENTILLION_CONFIG["llm_provider"] = "anthropic"
CENTILLION_CONFIG["deep_think_llm"] = "claude-sonnet-4-20250514"
CENTILLION_CONFIG["quick_think_llm"] = "claude-sonnet-4-20250514"
# More thorough analysis: 2 rounds of debate for better signal quality
CENTILLION_CONFIG["max_debate_rounds"] = 2
CENTILLION_CONFIG["max_risk_discuss_rounds"] = 2
# Data vendors — yfinance is free and covers US + European equities
CENTILLION_CONFIG["data_vendors"] = {
"core_stock_apis": "yfinance",
"technical_indicators": "yfinance",
"fundamental_data": "yfinance",
"news_data": "yfinance",
}
# ---------------------------------------------------------------------------
# Portfolio & Watchlist
# ---------------------------------------------------------------------------
# US SMID cap holdings/watchlist — add your actual tickers here
US_PORTFOLIO = [
# Example SMID cap US names — replace with your actual holdings
"CVLT", # Commvault Systems
"CSWI", # CSW Industrials
"EXPO", # Exponent
"LOPE", # Grand Canyon Education
"NOVT", # Novanta
"PCVX", # Vaxcyte
"STEP", # StepStone Group
"TNET", # TriNet Group
"ACIW", # ACI Worldwide
"CADE", # Cadence Bank
]
US_WATCHLIST = [
# Stocks you're considering but don't own yet
"CORT", # Corcept Therapeutics
"ELF", # e.l.f. Beauty
"DUOL", # Duolingo
"GWRE", # Guidewire Software
"KNSL", # Kingsdale Advisors
]
# European SMID cap — use Yahoo Finance tickers (exchange suffix)
# .L = London, .AS = Amsterdam, .DE = Frankfurt, .PA = Paris, .ST = Stockholm
EU_PORTFOLIO = [
# Example European SMID names — replace with your actual holdings
"DARK.L", # Darktrace (London)
"IMCD.AS", # IMCD (Amsterdam)
"RHM.DE", # Rheinmetall (Frankfurt)
"DSY.PA", # Dassault Systèmes (Paris)
"ALFA.ST", # Alfa Laval (Stockholm)
]
EU_WATCHLIST = [
"ASM.AS", # ASM International
"BESI.AS", # BE Semiconductor
"MONY.L", # Moneysupermarket
]
# ---------------------------------------------------------------------------
# Combined universe
# ---------------------------------------------------------------------------
ALL_PORTFOLIO = US_PORTFOLIO + EU_PORTFOLIO
ALL_WATCHLIST = US_WATCHLIST + EU_WATCHLIST
ALL_TICKERS = ALL_PORTFOLIO + ALL_WATCHLIST
# ---------------------------------------------------------------------------
# Benchmarks for performance tracking
# ---------------------------------------------------------------------------
BENCHMARKS = {
"us": "^RUT", # Russell 2000
"eu": "^STOXX", # STOXX Europe 600
"combined": "ACWI", # MSCI ACWI (broad global)
}

View File

@ -0,0 +1,281 @@
"""
Centillion Investment Partners - Virtual Portfolio Tracker
Maintains a paper-trading portfolio based on TradingAgents signals.
Tracks performance over time against benchmarks to evaluate whether
the agent-driven strategy generates alpha.
Usage:
# Initialize a new virtual portfolio with $1M
python centillion_portfolio_tracker.py init --cash 1000000
# Record today's signals and update positions
python centillion_portfolio_tracker.py update
# Show current portfolio and performance
python centillion_portfolio_tracker.py status
# Show performance vs benchmarks
python centillion_portfolio_tracker.py performance
"""
import argparse
import json
import os
from datetime import datetime, timedelta
import yfinance as yf
PORTFOLIO_FILE = "results/virtual_portfolio.json"
def load_portfolio():
if not os.path.exists(PORTFOLIO_FILE):
print("No portfolio found. Run 'init' first.")
return None
with open(PORTFOLIO_FILE) as f:
return json.load(f)
def save_portfolio(portfolio):
os.makedirs(os.path.dirname(PORTFOLIO_FILE), exist_ok=True)
with open(PORTFOLIO_FILE, "w") as f:
json.dump(portfolio, f, indent=2, default=str)
def cmd_init(args):
"""Initialize a fresh virtual portfolio."""
portfolio = {
"inception_date": datetime.now().strftime("%Y-%m-%d"),
"initial_cash": args.cash,
"cash": args.cash,
"positions": {}, # ticker -> {"shares": n, "avg_cost": x, "entry_date": d}
"history": [], # list of {date, action, ticker, shares, price, total_value}
"daily_nav": [], # list of {date, nav, benchmark_values: {}}
}
save_portfolio(portfolio)
print(f"Virtual portfolio initialized with ${args.cash:,.0f} cash.")
print(f"Saved to {PORTFOLIO_FILE}")
def cmd_update(args):
"""Apply signals from the latest analysis run to the portfolio."""
portfolio = load_portfolio()
if not portfolio:
return
# Load latest analysis results
results_dir = "results"
result_files = sorted(
[f for f in os.listdir(results_dir) if f.startswith("centillion_") and f.endswith(".json") and f != "virtual_portfolio.json"],
reverse=True,
)
if not result_files:
print("No analysis results found. Run centillion_run.py first.")
return
latest_file = os.path.join(results_dir, result_files[0])
print(f"Using signals from: {latest_file}")
with open(latest_file) as f:
signals = json.load(f)
date = datetime.now().strftime("%Y-%m-%d")
position_size = portfolio["cash"] + _total_position_value(portfolio)
target_per_position = position_size * 0.05 # 5% position sizing (long-only)
actions_taken = []
for ticker, data in signals.items():
if data["status"] == "error":
continue
signal = _extract_signal(data.get("decision", ""))
if signal == "BUY" and ticker not in portfolio["positions"]:
# Buy new position — allocate ~5% of portfolio
try:
price = _get_current_price(ticker)
if price and portfolio["cash"] >= target_per_position:
shares = int(target_per_position / price)
if shares > 0:
cost = shares * price
portfolio["cash"] -= cost
portfolio["positions"][ticker] = {
"shares": shares,
"avg_cost": price,
"entry_date": date,
}
actions_taken.append(f" BUY {shares} {ticker} @ ${price:.2f} = ${cost:,.0f}")
portfolio["history"].append({
"date": date, "action": "BUY", "ticker": ticker,
"shares": shares, "price": price,
})
except Exception as e:
print(f" Could not buy {ticker}: {e}")
elif signal == "SELL" and ticker in portfolio["positions"]:
# Sell entire position (long-only fund, no shorting)
pos = portfolio["positions"][ticker]
try:
price = _get_current_price(ticker)
if price:
proceeds = pos["shares"] * price
pnl = (price - pos["avg_cost"]) * pos["shares"]
portfolio["cash"] += proceeds
actions_taken.append(
f" SELL {pos['shares']} {ticker} @ ${price:.2f} = ${proceeds:,.0f} "
f"(PnL: ${pnl:+,.0f})"
)
portfolio["history"].append({
"date": date, "action": "SELL", "ticker": ticker,
"shares": pos["shares"], "price": price, "pnl": pnl,
})
del portfolio["positions"][ticker]
except Exception as e:
print(f" Could not sell {ticker}: {e}")
# Record daily NAV
nav = portfolio["cash"] + _total_position_value(portfolio)
portfolio["daily_nav"].append({"date": date, "nav": nav})
save_portfolio(portfolio)
if actions_taken:
print(f"\nActions taken on {date}:")
print("\n".join(actions_taken))
else:
print("No actions taken (all signals were HOLD or positions unchanged).")
print(f"\nPortfolio NAV: ${nav:,.0f}")
print(f"Cash: ${portfolio['cash']:,.0f}")
print(f"Positions: {len(portfolio['positions'])}")
def cmd_status(args):
"""Show current portfolio status."""
portfolio = load_portfolio()
if not portfolio:
return
print(f"\n{'='*60}")
print(" CENTILLION VIRTUAL PORTFOLIO")
print(f"{'='*60}")
print(f" Inception: {portfolio['inception_date']}")
print(f" Cash: ${portfolio['cash']:,.0f}")
total_mkt = 0
total_cost = 0
if portfolio["positions"]:
print(f"\n {'Ticker':<10} {'Shares':<8} {'AvgCost':<10} {'Current':<10} {'MktVal':<12} {'PnL':<12}")
print(f" {'-'*62}")
for ticker, pos in sorted(portfolio["positions"].items()):
price = _get_current_price(ticker) or pos["avg_cost"]
mkt_val = pos["shares"] * price
cost_basis = pos["shares"] * pos["avg_cost"]
pnl = mkt_val - cost_basis
total_mkt += mkt_val
total_cost += cost_basis
print(
f" {ticker:<10} {pos['shares']:<8} ${pos['avg_cost']:<9.2f} ${price:<9.2f} "
f"${mkt_val:<11,.0f} ${pnl:<+11,.0f}"
)
nav = portfolio["cash"] + total_mkt
total_return = ((nav / portfolio["initial_cash"]) - 1) * 100
print(f"\n Total Market Value: ${total_mkt:,.0f}")
print(f" Total NAV: ${nav:,.0f}")
print(f" Total Return: {total_return:+.2f}%")
print(f" # Positions: {len(portfolio['positions'])}")
print(f"{'='*60}\n")
def cmd_performance(args):
"""Show performance history and comparison to benchmarks."""
portfolio = load_portfolio()
if not portfolio:
return
if not portfolio["daily_nav"]:
print("No NAV history yet. Run 'update' after each analysis.")
return
initial = portfolio["initial_cash"]
print(f"\n NAV History:")
print(f" {'Date':<12} {'NAV':<15} {'Return':<10}")
print(f" {'-'*37}")
for entry in portfolio["daily_nav"]:
ret = ((entry["nav"] / initial) - 1) * 100
print(f" {entry['date']:<12} ${entry['nav']:<14,.0f} {ret:+.2f}%")
# Trade history summary
trades = portfolio["history"]
if trades:
total_pnl = sum(t.get("pnl", 0) for t in trades if t["action"] == "SELL")
wins = sum(1 for t in trades if t["action"] == "SELL" and t.get("pnl", 0) > 0)
losses = sum(1 for t in trades if t["action"] == "SELL" and t.get("pnl", 0) <= 0)
print(f"\n Closed Trades: {wins + losses} (Wins: {wins}, Losses: {losses})")
print(f" Realized PnL: ${total_pnl:+,.0f}")
def _get_current_price(ticker):
"""Fetch current/latest price for a ticker via yfinance."""
try:
tk = yf.Ticker(ticker)
hist = tk.history(period="5d")
if not hist.empty:
return float(hist["Close"].iloc[-1])
except Exception:
pass
return None
def _total_position_value(portfolio):
"""Calculate total market value of all positions."""
total = 0
for ticker, pos in portfolio["positions"].items():
price = _get_current_price(ticker) or pos["avg_cost"]
total += pos["shares"] * price
return total
def _extract_signal(decision_text):
text_upper = decision_text.upper() if isinstance(decision_text, str) else ""
for signal in ["BUY", "SELL", "HOLD"]:
if signal in text_upper:
return signal
return "HOLD"
def main():
parser = argparse.ArgumentParser(description="Centillion Virtual Portfolio Tracker")
sub = parser.add_subparsers(dest="command")
init_p = sub.add_parser("init", help="Initialize new virtual portfolio")
init_p.add_argument("--cash", type=float, default=1_000_000, help="Starting cash (default $1M)")
sub.add_parser("update", help="Apply latest signals to portfolio")
sub.add_parser("status", help="Show current portfolio")
sub.add_parser("performance", help="Show performance history")
args = parser.parse_args()
if args.command == "init":
cmd_init(args)
elif args.command == "update":
cmd_update(args)
elif args.command == "status":
cmd_status(args)
elif args.command == "performance":
cmd_performance(args)
else:
parser.print_help()
if __name__ == "__main__":
main()

235
centillion_run.py Normal file
View File

@ -0,0 +1,235 @@
"""
Centillion Investment Partners - Portfolio Analysis Runner
Runs the TradingAgents framework across all portfolio + watchlist tickers
and outputs buy/sell/hold recommendations.
Usage:
# Analyze full portfolio + watchlist
python centillion_run.py
# Analyze a single ticker
python centillion_run.py --ticker CVLT
# Analyze only the watchlist
python centillion_run.py --watchlist-only
# Analyze only US names
python centillion_run.py --region us
# Custom analysis date
python centillion_run.py --date 2025-03-21
"""
import argparse
import json
import os
import sys
from datetime import datetime, timedelta
from dotenv import load_dotenv
load_dotenv()
from centillion_config import (
ALL_PORTFOLIO,
ALL_TICKERS,
ALL_WATCHLIST,
CENTILLION_CONFIG,
EU_PORTFOLIO,
EU_WATCHLIST,
US_PORTFOLIO,
US_WATCHLIST,
)
from tradingagents.graph.trading_graph import TradingAgentsGraph
def parse_args():
parser = argparse.ArgumentParser(
description="Centillion Investment Partners - Trading Agent Analysis"
)
parser.add_argument("--ticker", type=str, help="Analyze a single ticker")
parser.add_argument(
"--watchlist-only", action="store_true", help="Only analyze watchlist"
)
parser.add_argument(
"--portfolio-only", action="store_true", help="Only analyze current holdings"
)
parser.add_argument(
"--region",
choices=["us", "eu", "all"],
default="all",
help="Region filter (default: all)",
)
parser.add_argument(
"--date",
type=str,
default=None,
help="Analysis date in YYYY-MM-DD format (default: yesterday)",
)
parser.add_argument(
"--output",
type=str,
default=None,
help="Output file path for JSON results",
)
parser.add_argument(
"--debug", action="store_true", help="Enable debug output from agents"
)
return parser.parse_args()
def get_tickers(args):
"""Determine which tickers to analyze based on flags."""
if args.ticker:
return [args.ticker.upper()]
region = args.region
if args.portfolio_only:
if region == "us":
return US_PORTFOLIO
elif region == "eu":
return EU_PORTFOLIO
return ALL_PORTFOLIO
if args.watchlist_only:
if region == "us":
return US_WATCHLIST
elif region == "eu":
return EU_WATCHLIST
return ALL_WATCHLIST
# Default: everything
if region == "us":
return US_PORTFOLIO + US_WATCHLIST
elif region == "eu":
return EU_PORTFOLIO + EU_WATCHLIST
return ALL_TICKERS
def get_analysis_date(args):
"""Get the analysis date — defaults to yesterday (last trading day approximation)."""
if args.date:
return args.date
yesterday = datetime.now() - timedelta(days=1)
# Skip weekends
if yesterday.weekday() == 6: # Sunday
yesterday -= timedelta(days=2)
elif yesterday.weekday() == 5: # Saturday
yesterday -= timedelta(days=1)
return yesterday.strftime("%Y-%m-%d")
def run_analysis(tickers, date, debug=False):
"""Run the trading agents analysis on each ticker."""
ta = TradingAgentsGraph(debug=debug, config=CENTILLION_CONFIG)
results = {}
total = len(tickers)
for i, ticker in enumerate(tickers, 1):
print(f"\n{'='*60}")
print(f" [{i}/{total}] Analyzing {ticker} as of {date}")
print(f"{'='*60}\n")
try:
_, decision = ta.propagate(ticker, date)
results[ticker] = {
"decision": decision,
"date": date,
"status": "success",
"in_portfolio": ticker in ALL_PORTFOLIO,
}
# Print summary
print(f"\n >> {ticker}: {_extract_signal(decision)}")
except Exception as e:
print(f"\n >> {ticker}: ERROR - {e}")
results[ticker] = {
"decision": str(e),
"date": date,
"status": "error",
"in_portfolio": ticker in ALL_PORTFOLIO,
}
return results
def _extract_signal(decision_text):
"""Extract the BUY/SELL/HOLD signal from the decision text."""
text_upper = decision_text.upper() if isinstance(decision_text, str) else ""
for signal in ["BUY", "SELL", "HOLD"]:
if signal in text_upper:
return signal
return "UNCLEAR"
def print_summary(results):
"""Print a clean summary table of all results."""
print(f"\n\n{'='*70}")
print(" CENTILLION INVESTMENT PARTNERS — ANALYSIS SUMMARY")
print(f"{'='*70}\n")
buys, sells, holds, errors = [], [], [], []
for ticker, res in results.items():
if res["status"] == "error":
errors.append(ticker)
continue
signal = _extract_signal(res["decision"])
tag = " [HELD]" if res["in_portfolio"] else " [WATCH]"
if signal == "BUY":
buys.append(f" {ticker}{tag}")
elif signal == "SELL":
sells.append(f" {ticker}{tag}")
else:
holds.append(f" {ticker}{tag}")
if buys:
print("BUY signals:")
print("\n".join(buys))
if sells:
print("\nSELL signals:")
print("\n".join(sells))
if holds:
print("\nHOLD signals:")
print("\n".join(holds))
if errors:
print(f"\nErrors: {', '.join(errors)}")
print(f"\n{'='*70}\n")
def save_results(results, output_path):
"""Save results to JSON file."""
os.makedirs(os.path.dirname(output_path) or ".", exist_ok=True)
with open(output_path, "w") as f:
json.dump(results, f, indent=2, default=str)
print(f"Results saved to {output_path}")
def main():
args = parse_args()
tickers = get_tickers(args)
date = get_analysis_date(args)
print(f"\n Centillion Investment Partners — TradingAgents Analysis")
print(f" Date: {date}")
print(f" Tickers ({len(tickers)}): {', '.join(tickers)}")
print(f" LLM: {CENTILLION_CONFIG['llm_provider']} / {CENTILLION_CONFIG['deep_think_llm']}")
print()
results = run_analysis(tickers, date, debug=args.debug)
print_summary(results)
# Save results
output_path = args.output or f"results/centillion_{date}.json"
save_results(results, output_path)
if __name__ == "__main__":
main()