TradingAgents/centillion_portfolio_tracke...

282 lines
9.6 KiB
Python

"""
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()