Spaces:
Sleeping
Sleeping
| import json | |
| import os | |
| from datetime import datetime | |
| import requests | |
| import yfinance as yf | |
| from src.core.logger import get_logger | |
| from src.core.ticker_utils import normalize_price | |
| logger = get_logger(__name__) | |
| PORTFOLIO_FILE = "paper_portfolio.json" | |
| # VPS Data API (optional — falls back to local JSON if not set) | |
| VPS_API_URL = os.getenv("VPS_API_URL", "").rstrip("/") | |
| VPS_API_KEY = os.getenv("VPS_API_KEY", "") | |
| def _vps_headers() -> dict: | |
| return {"X-API-Key": VPS_API_KEY, "Content-Type": "application/json"} | |
| def record_paper_trade( | |
| ticker: str, | |
| entry_price: float, | |
| verdict: str, | |
| source: str, | |
| structured_verdict: str | None = None, | |
| position_size: float = 0.0, | |
| ) -> None: | |
| """Save a BUY/STRONG BUY/WATCH recommendation to the paper portfolio. | |
| When *structured_verdict* is supplied (from ``InvestmentVerdict.verdict``), | |
| it is used directly, skipping brittle string matching on the full report. | |
| For US tickers with actionable verdicts, also submits to Alpaca Paper | |
| Trading (if ``ALPACA_ENABLED=true``). | |
| """ | |
| if structured_verdict: | |
| _VALID = {"STRONG BUY", "BUY", "WATCH"} | |
| trade_type = structured_verdict if structured_verdict in _VALID else None | |
| else: | |
| v_upper = verdict.strip().upper() | |
| trade_type = None | |
| if "STRONG BUY" in v_upper: | |
| trade_type = "STRONG BUY" | |
| elif " BUY" in v_upper or v_upper.startswith("BUY"): | |
| trade_type = "BUY" | |
| elif "WATCH" in v_upper: | |
| trade_type = "WATCH" | |
| if not trade_type: | |
| return | |
| today = datetime.now().strftime("%Y-%m-%d") | |
| # --- Alpaca broker execution (US equities, BUY/STRONG BUY only) --- | |
| order_id = None | |
| fill_price = None | |
| broker_status = "none" | |
| try: | |
| from src.broker.alpaca import calculate_order, submit_order, get_account | |
| acct = get_account() | |
| if acct and position_size > 0: | |
| order_params = calculate_order( | |
| ticker=ticker, | |
| verdict=trade_type, | |
| position_size_pct=position_size, | |
| account_equity=acct["equity"], | |
| ) | |
| if order_params: | |
| result = submit_order(order_params) | |
| order_id = result.order_id | |
| fill_price = result.fill_price | |
| broker_status = result.broker_status | |
| if result.success: | |
| logger.info("Alpaca order filled: %s %d shares @ %s", | |
| ticker, result.qty, fill_price or "market") | |
| else: | |
| logger.warning("Alpaca order not filled: %s — %s", | |
| ticker, result.error or broker_status) | |
| except Exception as exc: | |
| logger.warning("Alpaca execution skipped for %s: %s", ticker, exc) | |
| # --- Record to VPS --- | |
| if VPS_API_URL: | |
| try: | |
| resp = requests.post( | |
| f"{VPS_API_URL}/portfolio", | |
| headers=_vps_headers(), | |
| json={ | |
| "ticker": ticker, | |
| "entry_price": entry_price, | |
| "date": today, | |
| "verdict": trade_type, | |
| "source": source, | |
| "position_size": position_size, | |
| "order_id": order_id, | |
| "fill_price": fill_price, | |
| "broker_status": broker_status, | |
| }, | |
| timeout=5, | |
| ) | |
| resp.raise_for_status() | |
| result = resp.json() | |
| if result.get("status") == "duplicate": | |
| logger.info("Duplicate trade skipped (VPS): %s on %s", ticker, today) | |
| else: | |
| logger.info("Paper trade recorded (VPS): %s %s @ $%.2f", trade_type, ticker, entry_price) | |
| return | |
| except Exception as exc: | |
| logger.warning("VPS record_paper_trade failed, using local fallback: %s", exc) | |
| # --- Local fallback --- | |
| try: | |
| portfolio = [] | |
| if os.path.exists(PORTFOLIO_FILE): | |
| with open(PORTFOLIO_FILE, "r") as f: | |
| portfolio = json.load(f) | |
| for trade in portfolio: | |
| if trade["ticker"] == ticker and trade["date"] == today: | |
| logger.info("Duplicate trade skipped: %s on %s", ticker, today) | |
| return | |
| trade = { | |
| "ticker": ticker, | |
| "entry_price": entry_price, | |
| "date": today, | |
| "verdict": trade_type, | |
| "source": source, | |
| "position_size": position_size, | |
| "order_id": order_id, | |
| "fill_price": fill_price, | |
| "broker_status": broker_status, | |
| } | |
| portfolio.append(trade) | |
| with open(PORTFOLIO_FILE, "w") as f: | |
| json.dump(portfolio, f, indent=4) | |
| logger.info("Paper trade recorded: %s %s @ $%.2f", trade_type, ticker, entry_price) | |
| except Exception as exc: | |
| logger.error("Error recording paper trade: %s", exc) | |
| def evaluate_portfolio() -> str: | |
| """Read the paper portfolio and calculate live performance.""" | |
| # Try VPS first | |
| if VPS_API_URL: | |
| try: | |
| resp = requests.get( | |
| f"{VPS_API_URL}/portfolio/evaluate", | |
| headers=_vps_headers(), | |
| timeout=30, | |
| ) | |
| resp.raise_for_status() | |
| data = resp.json() | |
| if not data.get("trades"): | |
| return "**Paper Portfolio is empty.** The Agent hasn't tracked any stocks yet." | |
| report = "## PrimoGreedy Agent Track Record\n\n" | |
| report += "| Ticker | Date Called | Entry Price | Current Price | Return | Verdict |\n" | |
| report += "|--------|-------------|-------------|---------------|--------|--------|\n" | |
| for t in data["trades"]: | |
| if t["gain_pct"] is not None: | |
| emoji = "+" if t["gain_pct"] > 0 else "" | |
| report += ( | |
| f"| **{t['ticker']}** | {t['date']} | ${t['entry']:.2f} | " | |
| f"${t['current']:.2f} | {emoji}{t['gain_pct']:.2f}% | {t['verdict']} |\n" | |
| ) | |
| else: | |
| report += ( | |
| f"| **{t['ticker']}** | {t['date']} | ${t['entry']:.2f} | " | |
| f"Error | N/A | {t['verdict']} |\n" | |
| ) | |
| report += f"\n### Agent Performance Summary\n" | |
| report += f"- **Total Calls:** {data['total_calls']}\n" | |
| report += f"- **Win Rate:** {data['win_rate']}%\n" | |
| report += f"- **Average Return per Trade:** {data['avg_return']}%\n" | |
| return report | |
| except Exception as exc: | |
| logger.warning("VPS evaluate_portfolio failed, using local fallback: %s", exc) | |
| # Local fallback (original behavior) | |
| return _evaluate_local() | |
| def _evaluate_local() -> str: | |
| """Evaluate portfolio from local JSON file.""" | |
| if not os.path.exists(PORTFOLIO_FILE): | |
| return "**Paper Portfolio is empty.** The Agent hasn't tracked any stocks yet." | |
| try: | |
| with open(PORTFOLIO_FILE, "r") as f: | |
| portfolio = json.load(f) | |
| if not portfolio: | |
| return "**Paper Portfolio is empty.** The Agent hasn't tracked any stocks yet." | |
| total_roi = 0.0 | |
| winning_trades = 0 | |
| valid_trades = 0 | |
| report = "## PrimoGreedy Agent Track Record\n\n" | |
| report += "| Ticker | Date Called | Entry Price | Current Price | Return | Verdict |\n" | |
| report += "|--------|-------------|-------------|---------------|--------|--------|\n" | |
| for trade in portfolio: | |
| ticker = trade["ticker"] | |
| entry = trade["entry_price"] | |
| try: | |
| stock = yf.Ticker(ticker) | |
| info = stock.info | |
| price = info.get("currentPrice", 0) or info.get("regularMarketPrice", 0) or 0 | |
| currency = info.get("currency", "USD") | |
| price = normalize_price(price, ticker, currency) | |
| if price > 0 and entry > 0: | |
| gain_pct = ((price - entry) / entry) * 100 | |
| emoji = "+" if gain_pct > 0 else "" | |
| if gain_pct > 0: | |
| winning_trades += 1 | |
| total_roi += gain_pct | |
| valid_trades += 1 | |
| report += ( | |
| f"| **{ticker}** | {trade['date']} | ${entry:.2f} | " | |
| f"${price:.2f} | {emoji}{gain_pct:.2f}% | {trade['verdict']} |\n" | |
| ) | |
| else: | |
| report += ( | |
| f"| **{ticker}** | {trade['date']} | ${entry:.2f} | " | |
| f"Error | N/A | {trade['verdict']} |\n" | |
| ) | |
| except Exception as exc: | |
| logger.warning("Price fetch failed for %s: %s", ticker, exc) | |
| report += ( | |
| f"| **{ticker}** | {trade['date']} | ${entry:.2f} | " | |
| f"Error | N/A | {trade['verdict']} |\n" | |
| ) | |
| if valid_trades > 0: | |
| avg_roi = total_roi / valid_trades | |
| win_rate = (winning_trades / valid_trades) * 100 | |
| else: | |
| avg_roi = 0 | |
| win_rate = 0 | |
| report += f"\n### Agent Performance Summary\n" | |
| report += f"- **Total Calls:** {len(portfolio)}\n" | |
| report += f"- **Win Rate:** {win_rate:.1f}%\n" | |
| report += f"- **Average Return per Trade:** {avg_roi:.2f}%\n" | |
| return report | |
| except Exception as exc: | |
| logger.error("Portfolio evaluation error: %s", exc) | |
| return f"Error reading portfolio: {exc}" | |