From a24c2dbb279ae5569cc6e604cb612db3455427e6 Mon Sep 17 00:00:00 2001 From: Kye Gomez Date: Tue, 17 Feb 2026 13:56:27 -0800 Subject: [PATCH] [New tools] [Jupiter, polygon, massive] [Remove ticker agent] --- README.md | 1 - autohedge/__main__.py | 6 + autohedge/cli.py | 212 +++++++++++++++ autohedge/main.py | 246 +---------------- autohedge/tools/__init__.py | 23 ++ autohedge/tools/polygon_api.py | 190 ++++++++++++++ autohedge/tools/yahoo_api.py | 259 ++++++++++++++++++ autohedge/workers.py | 464 +++++++++++++++++++++++++++++++++ pyproject.toml | 6 +- requirements.txt | 1 - 10 files changed, 1173 insertions(+), 235 deletions(-) create mode 100644 autohedge/__main__.py create mode 100644 autohedge/cli.py create mode 100644 autohedge/tools/polygon_api.py create mode 100644 autohedge/tools/yahoo_api.py create mode 100644 autohedge/workers.py diff --git a/README.md b/README.md index fd59de9..67200f6 100644 --- a/README.md +++ b/README.md @@ -107,7 +107,6 @@ MIT License. See [LICENSE](LICENSE) for details. ## Acknowledgments - [Swarms](https://swarms.ai) for the AI agent framework -- [Tickr Agent](https://github.com/The-Swarm-Corporation/tickr-agent) for market data integration --- diff --git a/autohedge/__main__.py b/autohedge/__main__.py new file mode 100644 index 0000000..6e0a8fe --- /dev/null +++ b/autohedge/__main__.py @@ -0,0 +1,6 @@ +"""Run AutoHedge CLI with: python -m autohedge""" + +from autohedge.cli import main + +if __name__ == "__main__": + main() diff --git a/autohedge/cli.py b/autohedge/cli.py new file mode 100644 index 0000000..1903002 --- /dev/null +++ b/autohedge/cli.py @@ -0,0 +1,212 @@ +""" +AutoHedge CLI — welcome screen and interactive REPL. +""" + +import os +import sys +from pathlib import Path + +from rich.console import Console +from rich.panel import Panel +from rich.text import Text +from rich.columns import Columns +from rich import box + +try: + from importlib.metadata import version as _version + + VERSION = _version("autohedge") +except Exception: + VERSION = "0.1.2" + +console = Console() + +# ASCII art: minimal "hedge" / chart vibe +BANNER_ART = r""" + ▄▄▄▄▄▄▄ + █████████ + ▐▀▄▄▄▄▄▀▌ + ▀ ▀ + ▄▄ ▀▄▀ ▄▄ +""" + +TIPS = [ + "Enter a task to run (e.g. 'Analyze NVDA for 50k allocation')", + "Type 'stocks AAPL,MSFT' to set tickers, then run a task", + "Type 'quit' or 'exit' to leave", + "Type 'help' or '?' for commands", +] + +RECENT_FILE = Path.home() / ".autohedge" / "recent_tasks.txt" +MAX_RECENT = 5 + + +def _get_recent_tasks() -> list[str]: + if not RECENT_FILE.exists(): + return [] + try: + lines = RECENT_FILE.read_text().strip().splitlines() + return [ + ln.strip() for ln in lines[-MAX_RECENT:] if ln.strip() + ] + except Exception: + return [] + + +def _append_recent(task: str) -> None: + try: + RECENT_FILE.parent.mkdir(parents=True, exist_ok=True) + recent = _get_recent_tasks() + if task in recent: + recent.remove(task) + recent.append(task) + RECENT_FILE.write_text("\n".join(recent[-MAX_RECENT:])) + except Exception: + pass + + +def _welcome() -> None: + cwd = Path.cwd() + try: + cwd_str = cwd.relative_to(Path.home()) + cwd_display = f"~/{cwd_str}" + except ValueError: + cwd_display = str(cwd) + + welcome = Text("Welcome to AutoHedge", style="bold orange1") + subtitle = Text( + f"v{VERSION} · {cwd_display}", + style="dim", + ) + + tips_text = Text( + "Tips for getting started\n", style="bold orange1" + ) + tips_text.append(" — ".join(TIPS[:2]) + "\n", style="dim") + tips_text.append(" — ".join(TIPS[2:]), style="dim") + + recent = _get_recent_tasks() + recent_heading = Text("Recent activity\n", style="bold orange1") + if recent: + recent_body = Text("\n".join(recent[-3:]), style="dim") + else: + recent_body = Text("No recent activity", style="dim") + + left = Text() + left.append(welcome) + left.append("\n\n") + left.append(BANNER_ART, style="cyan") + left.append("\n") + left.append(subtitle) + + right = Text() + right.append(tips_text) + right.append("\n") + right.append("─" * 50 + "\n", style="dim") + right.append(recent_heading) + right.append(recent_body) + + # Two-column layout inside one panel (Claude Code style) + left_panel = Panel( + left, + box=box.MINIMAL, + padding=(0, 1), + border_style="dim", + expand=False, + ) + right_panel = Panel( + right, + box=box.MINIMAL, + padding=(0, 1), + border_style="dim", + expand=True, + ) + cols = Columns( + [left_panel, right_panel], expand=True, equal=False + ) + console.print( + Panel( + cols, + title=f"[bold]AutoHedge v{VERSION}[/]", + title_align="left", + border_style="cyan", + padding=(0, 1), + ) + ) + + +def run_repl(default_stocks: list[str] | None = None) -> None: + stocks = default_stocks or ["NVDA"] + _welcome() + + while True: + try: + prompt = Text("> ", style="bold cyan") + console.print(prompt, end="") + line = input().strip() + except (EOFError, KeyboardInterrupt): + console.print("\n[dim]Goodbye.[/]") + break + + if not line: + continue + + lower = line.lower() + if lower in ("quit", "exit", "q"): + console.print("[dim]Goodbye.[/]") + break + + if lower in ("help", "?", "h"): + for t in TIPS: + console.print(f" [dim]·[/] {t}") + continue + + if lower.startswith("stocks "): + raw = line[6:].strip() + stocks = [ + s.strip().upper() + for s in raw.replace(",", " ").split() + if s.strip() + ] + console.print( + f"[dim]Stocks set to: {', '.join(stocks)}[/]" + ) + continue + + # Treat as task + task = line + _append_recent(task) + try: + from autohedge import AutoHedge + + system = AutoHedge(stocks=stocks) + console.print("[dim]Running...[/]") + result = system.run(task=task) + console.print( + Panel( + str(result)[:2000], + title="Result", + border_style="green", + ) + ) + except Exception as e: + console.print(f"[red]Error: {e}[/]") + + +def main() -> None: + """Entry point for the AutoHedge CLI.""" + default_stocks = os.environ.get("AUTOHEDGE_STOCKS") + if default_stocks: + stocks = [ + s.strip().upper() + for s in default_stocks.split(",") + if s.strip() + ] + else: + stocks = None + run_repl(default_stocks=stocks) + sys.exit(0) + + +if __name__ == "__main__": + main() diff --git a/autohedge/main.py b/autohedge/main.py index 0990a16..67928fa 100644 --- a/autohedge/main.py +++ b/autohedge/main.py @@ -1,34 +1,17 @@ import uuid from datetime import datetime from pathlib import Path -from typing import Dict, List, Optional +from typing import List, Optional from loguru import logger from pydantic import BaseModel -from swarms import Agent, Conversation -from tickr_agent.main import TickrAgent +from swarms import Conversation -from autohedge.prompts import ( - DIRECTOR_PROMPT, - EXECUTION_ORDER_PROMPT, - EXECUTION_PROMPT, - QUANT_ANALYSIS_PROMPT, - QUANT_PROMPT, - RISK_ASSESSMENT_PROMPT, - RISK_PROMPT, - SENTIMENT_PROMPT, - DIRECTOR_THESIS_PROMPT, - DIRECTOR_DECISION_PROMPT, -) - -sentiment_agent = Agent( - agent_name="Sentiment-Agent", - system_prompt=SENTIMENT_PROMPT, - model_name="gpt-4o-mini", - output_type="str", - max_loops=1, - verbose=True, - context_length=16000, +from autohedge.workers import ( + ExecutionAgent, + QuantAnalyst, + RiskManager, + TradingDirector, ) @@ -52,209 +35,6 @@ class AutoHedgeOutputMain(BaseModel): logs: List[AutoHedgeOutput] = None -class RiskManager: - def __init__(self): - self.risk_agent = Agent( - agent_name="Risk-Manager", - system_prompt=RISK_PROMPT, - model_name="groq/deepseek-r1-distill-llama-70b", - output_type="str", - max_loops=1, - verbose=True, - context_length=16000, - ) - - def assess_risk( - self, stock: str, thesis: str, quant_analysis: str - ) -> str: - prompt = RISK_ASSESSMENT_PROMPT.format( - stock=stock, thesis=thesis, quant_analysis=quant_analysis - ) - assessment = self.risk_agent.run(prompt) - - return assessment - - -class ExecutionAgent: - def __init__(self): - self.execution_agent = Agent( - agent_name="Execution-Agent", - system_prompt=EXECUTION_PROMPT, - model_name="groq/deepseek-r1-distill-llama-70b", - output_type="str", - max_loops=1, - verbose=True, - context_length=16000, - ) - - def generate_order( - self, stock: str, thesis: Dict, risk_assessment: Dict - ) -> str: - prompt = EXECUTION_ORDER_PROMPT.format( - stock=stock, - thesis=thesis, - risk_assessment=risk_assessment, - ) - order = self.execution_agent.run(prompt) - return order - - -class TradingDirector: - """ - Trading Director Agent responsible for generating trading theses and coordinating strategy. - - Attributes: - director_agent (Agent): Swarms agent for thesis generation - tickr (TickrAgent): Agent for market data collection - output_dir (Path): Directory for storing outputs - - Methods: - generate_thesis: Generates trading thesis for a given stock - save_output: Saves thesis to JSON file - """ - - def __init__( - self, - stocks: List[str], - output_dir: str = "outputs", - cryptos: List[str] = None, - ): - - logger.info("Initializing Trading Director") - self.director_agent = Agent( - agent_name="Trading-Director", - system_prompt=DIRECTOR_PROMPT, - model_name="groq/deepseek-r1-distill-llama-70b", - output_type="str", - max_loops=1, - verbose=True, - context_length=16000, - ) - - # self.crypto_agent = CryptoAgentWrapper() - - def generate_thesis( - self, - task: str = "Generate a thesis for the stock", - stock: str = None, - crypto: str = None, - ) -> str: - """ - Generate trading thesis for a given stock. - - Args: - stock (str): Stock ticker symbol - - Returns: - TradingThesis: Generated thesis - """ - logger.info(f"Generating thesis for {stock}") - - self.tickr = TickrAgent( - stocks=[stock], - max_loops=1, - workers=10, - retry_attempts=1, - context_length=16000, - ) - - try: - market_data = self.tickr.run( - f"{task} Analyze current market conditions and key metrics for {stock}" - ) - - prompt = DIRECTOR_THESIS_PROMPT.format( - task=task, stock=stock, market_data=market_data - ) - thesis = self.director_agent.run(prompt) - return thesis, market_data - - except Exception as e: - logger.error( - f"Error generating thesis for {stock}: {str(e)}" - ) - raise - - def make_decision(self, task: str, thesis: str, *args, **kwargs): - return self.director_agent.run( - DIRECTOR_DECISION_PROMPT.format(thesis=thesis, task=task) - ) - - def generate_thesis_crypto( - self, - task: str = None, - crypto: str = None, - ): - logger.info(f"Generating thesis for {crypto}") - try: - market_data = self.crypto_agent.run( - crypto, - f"{task} Analyze current market conditions and key metrics for {crypto}", - ) - - prompt = DIRECTOR_THESIS_PROMPT.format( - task=task, stock=crypto, market_data=market_data - ) - thesis = self.director_agent.run(prompt) - return thesis - - except Exception as e: - logger.error( - f"Error generating thesis for {crypto}: {str(e)}" - ) - raise - - -class QuantAnalyst: - """ - Quantitative Analysis Agent responsible for technical and statistical analysis. - - Attributes: - quant_agent (Agent): Swarms agent for analysis - output_dir (Path): Directory for storing outputs - """ - - def __init__(self, output_dir: str = "outputs"): - self.output_dir = Path(output_dir) - self.output_dir.mkdir(exist_ok=True) - - logger.info("Initializing Quant Analyst") - self.quant_agent = Agent( - agent_name="Quant-Analyst", - system_prompt=QUANT_PROMPT, - model_name="groq/deepseek-r1-distill-llama-70b", - output_type="str", - max_loops=1, - verbose=True, - context_length=16000, - ) - - def analyze(self, stock: str, thesis: str) -> str: - """ - Perform quantitative analysis for a stock. - - Args: - stock (str): Stock ticker symbol - thesis (TradingThesis): Trading thesis - - Returns: - QuantAnalysis: Quantitative analysis results - """ - logger.info(f"Performing quant analysis for {stock}") - try: - prompt = QUANT_ANALYSIS_PROMPT.format( - stock=stock, thesis=thesis - ) - analysis = self.quant_agent.run(prompt) - return analysis - - except Exception as e: - logger.error( - f"Error in quant analysis for {stock}: {str(e)}" - ) - raise - - class AutoHedge: """ Main trading system that coordinates all agents and manages the trading cycle. @@ -334,14 +114,14 @@ class AutoHedge: task=task, stock=stock ) - self.conversation.add_message( + self.conversation.add( role=self.director.agent_name, content=f"Stock: {stock}\nMarket Data: {market_data}\nThesis: {thesis}", ) # Perform analysis analysis = self.quant.analyze( - stock + market_data, thesis + stock + market_data, thesis, task=task ) # setiment_analysis = sentiment_agent.run( @@ -358,7 +138,7 @@ class AutoHedge: # Assess risk risk_assessment = self.risk.assess_risk( - stock + market_data, thesis, analysis + stock + market_data, thesis, analysis, task=task ) self.conversation.add( @@ -367,7 +147,7 @@ class AutoHedge: # # Generate order if approved order = self.execution.generate_order( - stock, thesis, risk_assessment + stock, thesis, risk_assessment, task=task ) self.conversation.add( @@ -378,7 +158,9 @@ class AutoHedge: # Final decision decision = self.director.make_decision( - order + market_data + risk_assessment, thesis + order + market_data + risk_assessment, + thesis, + user_task=task, ) self.conversation.add( diff --git a/autohedge/tools/__init__.py b/autohedge/tools/__init__.py index e69de29..92fd7f8 100644 --- a/autohedge/tools/__init__.py +++ b/autohedge/tools/__init__.py @@ -0,0 +1,23 @@ +from autohedge.tools.polygon_api import ( + get_ticker_overview, + get_balance_sheets, + get_daily_ticker_summary, +) +from autohedge.tools.jupiter_search import search_tokens +from autohedge.tools.jupiter_price import get_token_price +from autohedge.tools.ultra_tools import ( + execute_trade, + get_order, + get_holdings, +) + +__all__ = [ + "get_ticker_overview", + "get_balance_sheets", + "get_daily_ticker_summary", + "search_tokens", + "get_token_price", + "execute_trade", + "get_order", + "get_holdings", +] diff --git a/autohedge/tools/polygon_api.py b/autohedge/tools/polygon_api.py new file mode 100644 index 0000000..791c31e --- /dev/null +++ b/autohedge/tools/polygon_api.py @@ -0,0 +1,190 @@ +""" +Stocks API client for ticker overview, balance sheets, and daily OHLC. +Uses Massive API (https://massive.com/docs). Set POLYGON_API_KEY and optionally +POLYGON_BASE_URL in .env. +""" + +import json +import os +from typing import Any, Optional + +import httpx +from loguru import logger + +DEFAULT_BASE_URL = "https://api.massive.com" +BASE_URL = os.getenv("POLYGON_BASE_URL", DEFAULT_BASE_URL) + + +def _get_headers() -> dict[str, str]: + headers: dict[str, str] = {} + key = os.getenv("POLYGON_API_KEY") + if key: + headers["Authorization"] = f"Bearer {key}" + return headers + + +def _get( + path: str, + *, + params: Optional[dict[str, Any]] = None, +) -> str: + url = f"{BASE_URL.rstrip('/')}{path}" + try: + with httpx.Client(timeout=15) as client: + resp = client.get( + url, + params=params, + headers=_get_headers() or None, + ) + resp.raise_for_status() + return json.dumps(resp.json()) + except httpx.HTTPError as e: + logger.error(f"Polygon API request failed: {e}") + raise + + +def get_ticker_overview( + ticker: str, date: Optional[str] = None +) -> str: + """ + Get comprehensive details for a single ticker (company fundamentals, exchange, + identifiers, market cap, branding, etc.). + + Parameters + ---------- + ticker : str + Case-sensitive ticker symbol (e.g. AAPL for Apple Inc.). + date : str, optional + Point-in-time date (YYYY-MM-DD) for ticker info. Defaults to most recent. + + Returns + ------- + str + JSON string of the response (results object with active, address, branding, + cik, description, market_cap, name, primary_exchange, etc.). + """ + if not ticker or not ticker.strip(): + logger.warning("get_ticker_overview: ticker is empty") + return "{}" + path = f"/v3/reference/tickers/{ticker.strip()}" + params: dict[str, Any] = {} + if date: + params["date"] = date + return _get(path, params=params if params else None) + + +def get_balance_sheets( + *, + cik: Optional[str] = None, + tickers: Optional[str] = None, + tickers_any_of: Optional[str] = None, + period_end: Optional[str] = None, + period_end_gte: Optional[str] = None, + period_end_lte: Optional[str] = None, + filing_date: Optional[str] = None, + fiscal_year: Optional[float] = None, + fiscal_quarter: Optional[float] = None, + timeframe: Optional[str] = None, + limit: Optional[int] = None, + sort: Optional[str] = None, +) -> str: + """ + Get balance sheet data for public companies (quarterly/annual). Returns asset, + liability, and equity positions as of period end. + + Parameters + ---------- + cik : str, optional + SEC Central Index Key (CIK). + tickers : str, optional + Filter by ticker(s). + tickers_any_of : str, optional + Comma-separated tickers; filter for any of these. + period_end : str, optional + Period end date (YYYY-MM-DD). + period_end_gte, period_end_lte : str, optional + Period end date range (YYYY-MM-DD). + filing_date : str, optional + SEC filing date (YYYY-MM-DD). + fiscal_year, fiscal_quarter : float, optional + Fiscal year and quarter (1–4). + timeframe : str, optional + 'quarterly' or 'annual'. + limit : int, optional + Max results (default 100, max 50000). + sort : str, optional + Sort columns, e.g. 'period_end.desc'. + + Returns + ------- + str + JSON string of the response (results array and next_url if paginated). + """ + params: dict[str, Any] = {} + if cik is not None: + params["cik"] = cik + if tickers is not None: + params["tickers"] = tickers + if tickers_any_of is not None: + params["tickers.any_of"] = tickers_any_of + if period_end is not None: + params["period_end"] = period_end + if period_end_gte is not None: + params["period_end.gte"] = period_end_gte + if period_end_lte is not None: + params["period_end.lte"] = period_end_lte + if filing_date is not None: + params["filing_date"] = filing_date + if fiscal_year is not None: + params["fiscal_year"] = fiscal_year + if fiscal_quarter is not None: + params["fiscal_quarter"] = fiscal_quarter + if timeframe is not None: + params["timeframe"] = timeframe + if limit is not None: + params["limit"] = limit + if sort is not None: + params["sort"] = sort + return _get( + "/stocks/financials/v1/balance-sheets", params=params or None + ) + + +def get_daily_ticker_summary( + stocks_ticker: str, + date: str, + adjusted: Optional[bool] = None, +) -> str: + """ + Get daily open/close (OHLC) and volume for a stock ticker on a given date. + Optionally includes pre-market and after-hours prices. + + Parameters + ---------- + stocks_ticker : str + Case-sensitive ticker symbol (e.g. AAPL). + date : str + Date of the open/close in YYYY-MM-DD. + adjusted : bool, optional + If True, results are adjusted for splits; if False, not adjusted. + Default from API is adjusted. + + Returns + ------- + str + JSON string of the response (open, high, low, close, volume, afterHours, + preMarket, symbol, from, status). + """ + if not stocks_ticker or not stocks_ticker.strip(): + logger.warning( + "get_daily_ticker_summary: stocks_ticker is empty" + ) + return "{}" + if not date or not date.strip(): + logger.warning("get_daily_ticker_summary: date is empty") + return "{}" + path = f"/v1/open-close/{stocks_ticker.strip()}/{date.strip()}" + params: dict[str, Any] = {} + if adjusted is not None: + params["adjusted"] = "true" if adjusted else "false" + return _get(path, params=params if params else None) diff --git a/autohedge/tools/yahoo_api.py b/autohedge/tools/yahoo_api.py new file mode 100644 index 0000000..39e41f4 --- /dev/null +++ b/autohedge/tools/yahoo_api.py @@ -0,0 +1,259 @@ +""" +Yahoo Finance API client for stock quotes, fundamentals, and historical OHLC. +Uses the yfinance package (https://github.com/ranaroussi/yfinance). +Handles Yahoo rate limits (429) by fetching history first and returning +partial data when quoteSummary fails. +""" + +import json +import traceback +from typing import Any, Optional + +import yfinance as yf +from loguru import logger + +# Errors from yfinance when Yahoo returns 429 or non-JSON (rate limit / block) +_RATE_LIMIT_EXCEPTIONS: tuple = (json.JSONDecodeError,) +try: + import requests + + _RATE_LIMIT_EXCEPTIONS = ( + *_RATE_LIMIT_EXCEPTIONS, + requests.HTTPError, + ) +except ImportError: + pass + + +def _df_to_json_serializable(df: Any) -> Any: + """Convert DataFrame to a JSON-serializable structure (handles NaN/dates).""" + if df is None or (hasattr(df, "empty") and df.empty): + return None + try: + import pandas as pd + + if isinstance(df, pd.DataFrame): + return json.loads( + df.to_json(orient="split", date_format="iso") + ) + return df + except Exception: + return None + + +def _safe_info( + ticker: yf.Ticker, +) -> tuple[dict[str, Any], Optional[str]]: + """ + Get ticker.info; on 429/JSON error return {} and an error message. + Returns (info_dict, error_message_or_None). + """ + try: + info = ticker.info + return (info or {}), None + except _RATE_LIMIT_EXCEPTIONS as e: + logger.warning( + "Yahoo rate limit or invalid response (info): {}", + e, + ) + return ( + {}, + "Rate limited or invalid response from Yahoo (429).", + ) + except Exception as e: + logger.debug("get info failed: {}", e) + return {}, str(e) + + +def _safe_financials(ticker: yf.Ticker) -> dict[str, Any]: + """ + Get balance_sheet, income_stmt, cashflow, etc.; on failure return + dict with only keys that succeeded and an optional _error key. + """ + out: dict[str, Any] = {} + attrs = [ + ("balance_sheet", "balance_sheet"), + ("quarterly_balance_sheet", "quarterly_balance_sheet"), + ("income_stmt", "income_stmt"), + ("quarterly_income_stmt", "quarterly_income_stmt"), + ("cashflow", "cashflow"), + ("quarterly_cashflow", "quarterly_cashflow"), + ("recommendations", "recommendations"), + ("calendar", "calendar"), + ] + for name, attr in attrs: + try: + val = getattr(ticker, attr, None) + if val is not None: + if hasattr(val, "to_json"): + out[name] = _df_to_json_serializable(val) + else: + out[name] = val + except _RATE_LIMIT_EXCEPTIONS: + continue + except Exception: + continue + return out + + +def get_stock_quote(ticker: str) -> str: + """ + Get current quote for a symbol (price, volume, day range, etc.). + Fetches history first (chart API); info (quoteSummary) may be empty + if Yahoo rate-limits. + """ + if not ticker or not ticker.strip(): + logger.warning("get_stock_quote: ticker is empty") + return "{}" + symbol = ticker.strip().upper() + try: + t = yf.Ticker(symbol) + hist = t.history(period="5d", interval="1d") + info, info_err = _safe_info(t) + out: dict[str, Any] = {"symbol": symbol, "info": info} + if info_err: + out["_warning"] = info_err + if hist is not None and not hist.empty: + out["history"] = _df_to_json_serializable(hist) + return json.dumps(out, default=str) + except Exception as e: + logger.error( + "get_stock_quote failed: {}\n{}", + e, + traceback.format_exc(), + ) + return "{}" + + +def get_historical_prices( + ticker: str, + interval: str = "1d", + range_str: str = "1mo", +) -> str: + """ + Get historical OHLCV for a ticker. + + Parameters + ---------- + ticker : str + Ticker symbol (e.g. AAPL). + interval : str, optional + Candle interval: 1m, 2m, 5m, 15m, 30m, 1h, 1d, 1wk, 1mo. Default 1d. + range_str : str, optional + Range: 1d, 5d, 1mo, 3mo, 6mo, 1y, 2y, 5y, 10y, ytd, max. Default 1mo. + + Returns + ------- + str + JSON string with history (dates, open, high, low, close, volume). + """ + if not ticker or not ticker.strip(): + logger.warning("get_historical_prices: ticker is empty") + return "{}" + symbol = ticker.strip().upper() + try: + t = yf.Ticker(symbol) + hist = t.history(period=range_str, interval=interval) + out: dict[str, Any] = {"symbol": symbol} + out["history"] = _df_to_json_serializable(hist) + return json.dumps(out, default=str) + except Exception as e: + logger.error( + "get_historical_prices failed: {}\n{}", + e, + traceback.format_exc(), + ) + return "{}" + + +def get_quote_summary( + ticker: str, + modules: Optional[list[str]] = None, +) -> str: + """ + Get quote summary (fundamentals, financials, profile, balance sheet, + income, cashflow). The modules argument is ignored. Uses safe fetchers + so rate limits (429) return partial data instead of failing. + """ + if not ticker or not ticker.strip(): + logger.warning("get_quote_summary: ticker is empty") + return "{}" + symbol = ticker.strip().upper() + try: + t = yf.Ticker(symbol) + info, info_err = _safe_info(t) + out: dict[str, Any] = {"symbol": symbol, "info": info} + if info_err: + out["_warning"] = info_err + financials = _safe_financials(t) + out.update(financials) + return json.dumps(out, default=str) + except Exception as e: + logger.error( + "get_quote_summary failed: {}\n{}", + e, + traceback.format_exc(), + ) + return "{}" + + +def get_all_stock_data( + ticker: str, + include_history: bool = True, + history_range: str = "1mo", +) -> str: + """ + Get all main data for a stock: current quote (info), historical OHLC, + and quote summary. Fetches history first (chart API); if Yahoo + rate-limits quoteSummary (429), still returns history and partial data. + """ + if not ticker or not ticker.strip(): + logger.warning("get_all_stock_data: ticker is empty") + return "{}" + symbol = ticker.strip().upper() + try: + t = yf.Ticker(symbol) + # Fetch history first (chart API is less rate-limited than quoteSummary) + hist = t.history(period=history_range, interval="1d") + history_serialized = _df_to_json_serializable(hist) + + info, info_err = _safe_info(t) + if info_err: + logger.warning( + "get_all_stock_data: info fetch failed: {}", info_err + ) + + quote_data: dict[str, Any] = {"symbol": symbol, "info": info} + if info_err: + quote_data["_warning"] = info_err + if include_history and history_serialized: + quote_data["history"] = history_serialized + + summary_data: dict[str, Any] = { + "symbol": symbol, + "info": info, + } + if info_err: + summary_data["_warning"] = info_err + summary_data.update(_safe_financials(t)) + + out: dict[str, Any] = { + "symbol": symbol, + "quote": quote_data, + "quote_summary": summary_data, + } + if include_history: + out["history"] = history_serialized + + return json.dumps(out, default=str) + except Exception as e: + logger.error( + "get_all_stock_data failed: {}\n{}", + e, + traceback.format_exc(), + ) + return "{}" + + +if __name__ == "__main__": + print(get_all_stock_data("AAPL")) diff --git a/autohedge/workers.py b/autohedge/workers.py new file mode 100644 index 0000000..3587e0f --- /dev/null +++ b/autohedge/workers.py @@ -0,0 +1,464 @@ +""" +AutoHedge workers: Pydantic output models and agent classes for thesis +generation, risk assessment, execution, and quantitative analysis. +""" + +import uuid +from datetime import datetime +from pathlib import Path +from typing import Dict, List, Optional + +from loguru import logger +from pydantic import BaseModel +from swarms import Agent + +from autohedge.prompts import ( + DIRECTOR_DECISION_PROMPT, + DIRECTOR_PROMPT, + DIRECTOR_THESIS_PROMPT, + EXECUTION_ORDER_PROMPT, + EXECUTION_PROMPT, + QUANT_ANALYSIS_PROMPT, + QUANT_PROMPT, + RISK_ASSESSMENT_PROMPT, + RISK_PROMPT, + SENTIMENT_PROMPT, +) +from autohedge.tools.yahoo_api import get_all_stock_data + +sentiment_agent = Agent( + agent_name="Sentiment-Agent", + system_prompt=SENTIMENT_PROMPT, + model_name="gpt-4o-mini", + output_type="str", + max_loops=1, + verbose=True, + context_length=16000, +) + + +def _agent_context(task: Optional[str] = None) -> str: + """Build context string with current time and task to prepend to agent prompts.""" + now = datetime.now().isoformat() + task_str = task if task else "(none)" + return f"Current time: {now}\nTask: {task_str}\n\n" + + +class AutoHedgeOutput(BaseModel): + """ + Per-stock output from the AutoHedge pipeline for a single ticker. + + Attributes + ---------- + id : str + Unique run identifier (hex UUID). + thesis : str, optional + Trading thesis for the stock. + risk_assessment : str, optional + Risk assessment text from the risk manager. + order : str, optional + Generated order / execution plan. + decision : str, optional + Director decision (e.g. hold, buy, sell). + timestamp : str + ISO timestamp when this output was produced. + current_stock : str + Ticker symbol this output refers to. + """ + + id: str = uuid.uuid4().hex + thesis: Optional[str] = None + risk_assessment: Optional[str] = None + order: Optional[str] = None + decision: str = None + timestamp: str = datetime.now().isoformat() + current_stock: str + + +class AutoHedgeOutputMain(BaseModel): + """ + Top-level output from a full AutoHedge run (multiple stocks / task). + + Attributes + ---------- + name : str, optional + Run or strategy name. + description : str, optional + Human-readable description of the run. + id : str + Unique run identifier (hex UUID). + stocks : list, optional + List of ticker symbols processed. + task : str, optional + User task or instruction for the run. + timestamp : str + ISO timestamp when the run completed. + logs : list of AutoHedgeOutput, optional + Per-stock results in order of processing. + """ + + name: Optional[str] = None + description: Optional[str] = None + id: str = uuid.uuid4().hex + stocks: Optional[list] = None + task: Optional[str] = None + timestamp: str = datetime.now().isoformat() + logs: List[AutoHedgeOutput] = None + + +class RiskManager: + """ + Agent that assesses risk for a stock given a thesis and quant analysis. + + Uses a dedicated Swarms agent (RISK_PROMPT) to produce a text risk + assessment from the trading thesis and quantitative analysis. + + Attributes + ---------- + risk_agent : Agent + Swarms agent used for risk assessment. + """ + + def __init__(self): + self.risk_agent = Agent( + agent_name="Risk-Manager", + system_prompt=RISK_PROMPT, + model_name="gpt-4.1", + output_type="str", + max_loops=1, + verbose=True, + context_length=16000, + ) + + def assess_risk( + self, + stock: str, + thesis: str, + quant_analysis: str, + task: Optional[str] = None, + ) -> str: + """ + Produce a risk assessment for a stock given thesis and quant analysis. + + Parameters + ---------- + stock : str + Ticker symbol. + thesis : str + Trading thesis text. + quant_analysis : str + Quantitative analysis text. + task : str, optional + User task or instruction (included in agent context with current time). + + Returns + ------- + str + Risk assessment text from the risk agent. + """ + prompt = _agent_context(task) + RISK_ASSESSMENT_PROMPT.format( + stock=stock, thesis=thesis, quant_analysis=quant_analysis + ) + assessment = self.risk_agent.run(prompt) + + return assessment + + +class ExecutionAgent: + """ + Agent that generates execution orders from thesis and risk assessment. + + Uses a Swarms agent (EXECUTION_PROMPT) to turn a thesis and risk + assessment into a concrete order or execution plan. + + Attributes + ---------- + execution_agent : Agent + Swarms agent used for order generation. + """ + + def __init__(self): + self.execution_agent = Agent( + agent_name="Execution-Agent", + system_prompt=EXECUTION_PROMPT, + model_name="gpt-4.1", + output_type="str", + max_loops=1, + verbose=True, + context_length=16000, + ) + + def generate_order( + self, + stock: str, + thesis: Dict, + risk_assessment: Dict, + task: Optional[str] = None, + ) -> str: + """ + Generate an execution order for a stock from thesis and risk data. + + Parameters + ---------- + stock : str + Ticker symbol. + thesis : dict + Trading thesis (or serialized thesis data). + risk_assessment : dict + Risk assessment (or serialized risk data). + task : str, optional + User task or instruction (included in agent context with current time). + + Returns + ------- + str + Generated order text from the execution agent. + """ + prompt = _agent_context(task) + EXECUTION_ORDER_PROMPT.format( + stock=stock, + thesis=thesis, + risk_assessment=risk_assessment, + ) + order = self.execution_agent.run(prompt) + return order + + +class TradingDirector: + """ + Coordinates strategy and generates trading theses using market data. + + Uses a Swarms director agent and Yahoo Finance data to fetch market data, + then produces a trading thesis and can make follow-up decisions. + + Attributes + ---------- + director_agent : Agent + Swarms agent for thesis generation and decisions. + """ + + def __init__( + self, + stocks: List[str], + output_dir: str = "outputs", + cryptos: List[str] = None, + ): + """ + Parameters + ---------- + stocks : list of str + Ticker symbols the director may analyze. + output_dir : str, optional + Directory for outputs (default "outputs"). + cryptos : list of str, optional + Crypto symbols for crypto thesis (currently unused). + """ + logger.info("Initializing Trading Director") + self.director_agent = Agent( + agent_name="Trading-Director", + system_prompt=DIRECTOR_PROMPT, + model_name="gpt-4.1", + output_type="str", + max_loops=1, + verbose=True, + context_length=16000, + ) + + # self.crypto_agent = CryptoAgentWrapper() + + def generate_thesis( + self, + task: str = "Generate a thesis for the stock", + stock: str = None, + crypto: str = None, + ) -> str: + """ + Generate trading thesis for a given stock. + + Args: + stock (str): Stock ticker symbol + + Returns: + TradingThesis: Generated thesis + """ + logger.info(f"Generating thesis for {stock}") + + try: + market_data = get_all_stock_data( + stock, include_history=True + ) + + prompt = _agent_context( + task + ) + DIRECTOR_THESIS_PROMPT.format( + task=task, stock=stock, market_data=market_data + ) + thesis = self.director_agent.run(prompt) + return thesis, market_data + + except Exception as e: + logger.error( + f"Error generating thesis for {stock}: {str(e)}" + ) + raise + + def make_decision( + self, + task: str, + thesis: str, + user_task: Optional[str] = None, + *args, + **kwargs, + ): + """ + Run the director agent to make a decision given a task and thesis. + + Parameters + ---------- + task : str + Order/context to evaluate (e.g. order + market_data + risk_assessment). + thesis : str + Trading thesis text. + user_task : str, optional + User task or instruction (included in agent context with current time). + *args, **kwargs + Passed through to the agent (e.g. for future options). + + Returns + ------- + str + Director decision output. + """ + prompt = _agent_context( + user_task + ) + DIRECTOR_DECISION_PROMPT.format(thesis=thesis, task=task) + return self.director_agent.run(prompt) + + def generate_thesis_crypto( + self, + task: str = None, + crypto: str = None, + ): + """ + Generate a trading thesis for a crypto asset using the crypto agent. + + Parameters + ---------- + task : str, optional + Analysis task or instruction. + crypto : str, optional + Crypto symbol (e.g. BTC, ETH). + + Returns + ------- + str + Generated thesis text. + + Raises + ------ + Exception + If crypto_agent is not set or the run fails. + """ + logger.info(f"Generating thesis for {crypto}") + try: + market_data = self.crypto_agent.run( + crypto, + f"{task} Analyze current market conditions and key metrics for {crypto}", + ) + + prompt = _agent_context( + task + ) + DIRECTOR_THESIS_PROMPT.format( + task=task, stock=crypto, market_data=market_data + ) + thesis = self.director_agent.run(prompt) + return thesis + + except Exception as e: + logger.error( + f"Error generating thesis for {crypto}: {str(e)}" + ) + raise + + +class QuantAnalyst: + """ + Agent that performs quantitative (technical and statistical) analysis. + + Uses a Swarms agent (QUANT_PROMPT) to analyze a stock in the context + of a trading thesis and produce structured quant analysis. + + Attributes + ---------- + quant_agent : Agent + Swarms agent used for analysis. + output_dir : Path + Directory for saving outputs (created on init). + """ + + def __init__(self, output_dir: str = "outputs"): + self.output_dir = Path(output_dir) + self.output_dir.mkdir(exist_ok=True) + + logger.info("Initializing Quant Analyst") + self.quant_agent = Agent( + agent_name="Quant-Analyst", + system_prompt=QUANT_PROMPT, + model_name="gpt-4.1", + output_type="str", + max_loops=1, + verbose=True, + context_length=16000, + ) + + def analyze( + self, stock: str, thesis: str, task: Optional[str] = None + ) -> str: + """ + Perform quantitative analysis for a stock given a trading thesis. + + Parameters + ---------- + stock : str + Stock ticker symbol. + thesis : str + Trading thesis text. + task : str, optional + User task or instruction (included in agent context with current time). + + Returns + ------- + str + Quantitative analysis text from the quant agent. + """ + logger.info(f"Performing quant analysis for {stock}") + try: + prompt = _agent_context( + task + ) + QUANT_ANALYSIS_PROMPT.format( + stock=stock, thesis=thesis + ) + analysis = self.quant_agent.run(prompt) + return analysis + + except Exception as e: + logger.error( + f"Error in quant analysis for {stock}: {str(e)}" + ) + raise + + +# ----------------------------------------------------------------------------- +# Initialized workers and their agents (for discovery / iteration) +# ----------------------------------------------------------------------------- +risk_manager = RiskManager() +execution_agent_instance = ExecutionAgent() +trading_director = TradingDirector(stocks=[]) +quant_analyst = QuantAnalyst() + +ALL_AGENTS = [ + sentiment_agent, # Sentiment-Agent + risk_manager.risk_agent, # Risk-Manager + execution_agent_instance.execution_agent, # Execution-Agent + trading_director.director_agent, # Trading-Director + quant_analyst.quant_agent, # Quant-Analyst +] diff --git a/pyproject.toml b/pyproject.toml index a69ddbf..ec7d5ad 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,15 +21,19 @@ classifiers = [ "Programming Language :: Python :: 3.9" ] +[tool.poetry.scripts] +autohedge = "autohedge.cli:main" + [tool.poetry.dependencies] python = "^3.10" +rich = "*" swarms = "*" -tickr-agent="*" pydantic = "*" loguru = "*" swarm-models = "*" httpx = "*" solders = "*" +yfinance = "*" [tool.poetry.group.lint.dependencies] diff --git a/requirements.txt b/requirements.txt index 9df1fb7..b4aaabd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,4 @@ swarms -tickr-agent pydantic loguru swarm-models