diff --git a/examples/fetcher_demo.py b/examples/fetcher_demo.py index 7c1bc9b..1bf5410 100644 --- a/examples/fetcher_demo.py +++ b/examples/fetcher_demo.py @@ -1,8 +1,8 @@ """ -Example usage of the new fetcher architecture. +Enhanced fetcher demo with data analysis capabilities. -This file demonstrates how to use the BaseFetcher and its derived classes -for fetching different types of data. +This file demonstrates the new unified fetcher architecture with +data processing and analysis features. """ from typing import Any, Dict, List @@ -10,183 +10,185 @@ from live_trade_bench.fetchers import ( BaseFetcher, NewsFetcher, - OptionFetcher, PolymarketFetcher, - RedditFetcher, StockFetcher, ) +from live_trade_bench.fetchers.news_fetcher import fetch_news_data +from live_trade_bench.fetchers.polymarket_fetcher import ( + fetch_market_price_with_history, + fetch_trending_markets, +) +from live_trade_bench.fetchers.stock_fetcher import ( + fetch_stock_price_with_history, + fetch_trending_stocks, +) def example_stock_fetcher() -> None: - """Example of using StockFetcher.""" - print("=== Stock Fetcher Example ===") + """Example of enhanced StockFetcher with market analysis.""" + print("=== Enhanced Stock Fetcher Example ===") - # Create a stock fetcher with custom delays + # Create a stock fetcher stock_fetcher = StockFetcher(min_delay=0.5, max_delay=1.5) - # Fetch price data try: - price_data = stock_fetcher.fetch_stock_data( - ticker="AAPL", - start_date="2024-01-01", - end_date="2024-01-31", - interval="1d", - ) - if hasattr( - price_data, "__len__" - ): # Check if it has a length (DataFrame or dict) - print(f"Fetched AAPL data (type: {type(price_data).__name__})") - # Try to convert to dict if it's a DataFrame - try: - import pandas as pd - - if isinstance(price_data, pd.DataFrame): - price_dict = price_data.to_dict(orient="index") - print(f"Converted to dict with {len(price_dict)} entries") - print(f"Sample data: {list(price_dict.items())[:2]}") - else: - print(f"Data length: {len(price_data)}") - except ImportError: - print("Pandas not available, raw data received") - else: - print(f"Unexpected data type: {type(price_data)}") - except Exception as e: - print(f"Error fetching stock data: {e}") + # Get trending stocks for today + trending_stocks = fetch_trending_stocks(limit=3, for_date=None) + print(f"Trending stocks: {trending_stocks}") + + # Test historical trending (for backtest) + historical_date = "2024-01-15" + historical_stocks = fetch_trending_stocks(limit=3, for_date=historical_date) + print(f"Historical stocks for {historical_date}: {historical_stocks}") + + # Fetch stock data with history for multiple stocks + market_data = {} + for ticker in trending_stocks[:2]: # Test first 2 stocks + print(f"\nFetching data for {ticker}...") + stock_data = fetch_stock_price_with_history(ticker) + market_data[ticker] = stock_data + + if stock_data.get("current_price"): + print(f"{ticker} current price: ${stock_data['current_price']:.2f}") + history = stock_data.get("price_history", []) + print(f"{ticker} history: {len(history)} days") + else: + print(f"Failed to fetch {ticker} data") - # Also demonstrate the fetch() method with valid modes - try: - # Get trending stocks - trending = stock_fetcher.fetch("trending_stocks", limit=5) - print(f"Trending stocks: {trending}") - # Get current price for a specific stock - current_price = stock_fetcher.fetch("stock_price", ticker="AAPL") - print(f"Current AAPL price: ${current_price}") except Exception as e: - print(f"Error using fetch method: {e}") + print(f"Error in stock fetcher demo: {e}") def example_news_fetcher() -> None: - """Example of using NewsFetcher.""" - print("\n=== News Fetcher Example ===") + """Example of enhanced NewsFetcher with news analysis.""" + print("\n=== Enhanced News Fetcher Example ===") - # Create a news fetcher with longer delays for web scraping - news_fetcher = NewsFetcher(min_delay=3.0, max_delay=7.0) - - # Fetch news data try: - news_data = news_fetcher.fetch( - query="Apple stock", - start_date="2024-01-01", - end_date="2024-01-31", - max_pages=2, + from live_trade_bench.fetchers.news_fetcher import fetch_news_data + from datetime import datetime, timedelta + + # Calculate date range (last 7 days) + end_date = datetime.now() + start_date = end_date - timedelta(days=7) + + # Fetch news for Apple + print("Fetching Apple stock news...") + news_data = fetch_news_data( + query="Apple stock earnings", + start_date=start_date.strftime("%Y-%m-%d"), + end_date=end_date.strftime("%Y-%m-%d"), + max_pages=1, + ticker="AAPL" ) + print(f"Fetched {len(news_data)} news articles") if news_data: - print(f"Sample article: {news_data[0]['title']}") + print(f"Latest article: {news_data[0].get('title', 'No title')}") + + # Show news summaries + print(f"\n--- Sample News ---") + for i, item in enumerate(news_data[:3], 1): + print(f"{i}. {item.get('title', 'No title')}") + if item.get('snippet'): + print(f" {item['snippet'][:100]}...") + except Exception as e: - print(f"Error fetching news data: {e}") - - -def example_option_fetcher() -> None: - """Example of using OptionFetcher.""" - print("\n=== Option Fetcher Example ===") - - # Create an option fetcher - option_fetcher = OptionFetcher() - - try: - # Get available expirations - expirations = option_fetcher.fetch_option_expirations("AAPL") - print(f"AAPL has {len(expirations)} option expirations") - - if expirations: - # Get option chain for nearest expiration - option_chain = option_fetcher.fetch_option_chain("AAPL", expirations[0]) - print( - f"Option chain has {len(option_chain['calls'])} calls and {len(option_chain['puts'])} puts" - ) - - # Calculate Greeks for a sample option - greeks = option_fetcher.calculate_option_greeks( - underlying_price=150.0, - strike=150.0, - time_to_expiry=0.25, - risk_free_rate=0.05, - volatility=0.3, - option_type="call", - ) - print(f"Sample Greeks: {greeks}") - except Exception as e: - print(f"Error fetching option data: {e}") + print(f"Error in news fetcher demo: {e}") def example_polymarket_fetcher() -> None: - """Example of using PolymarketFetcher.""" - print("\n=== Polymarket Fetcher Example ===") - - # Create a Polymarket fetcher - poly_fetcher = PolymarketFetcher() + """Example of enhanced PolymarketFetcher with market analysis.""" + print("\n=== Enhanced Polymarket Fetcher Example ===") try: - # Fetch trending markets using the unified fetch method - trending = poly_fetcher.fetch("markets", category=None, limit=5) - if isinstance(trending, list): - print(f"Found {len(trending)} trending markets") - - if trending: - # Get details for first market - first_market = trending[0] - if isinstance(first_market, dict) and "id" in first_market: - market_id = first_market["id"] - details = poly_fetcher.fetch("market_details", market_id=market_id) - if isinstance(details, dict): - print(f"Market: {details.get('title', 'Unknown')}") - print(f"Outcomes: {len(details.get('outcomes', []))}") - - # Get orderbook - orderbook = poly_fetcher.fetch("orderbook", market_id=market_id) - if isinstance(orderbook, dict): - print( - f"Orderbook entries: {len(orderbook.get('asks', []))}" - ) - print( - f"Orderbook has {len(orderbook.get('bids', []))} bids and {len(orderbook.get('asks', []))} asks" - ) - else: - print(f"Unexpected trending data type: {type(trending)}") - except Exception as e: - print(f"Error fetching Polymarket data: {e}") + # Get trending markets for today + print("Fetching trending Polymarket markets...") + trending_markets = fetch_trending_markets(limit=2, for_date=None) + print(f"Found {len(trending_markets)} trending markets") + + # Test historical markets (for backtest) + historical_date = "2024-01-15" + historical_markets = fetch_trending_markets(limit=2, for_date=historical_date) + print(f"Historical markets for {historical_date}: {len(historical_markets)} markets") + + if trending_markets: + # Fetch market data with history + market_data = {} + for market in trending_markets: + market_id = market.get("id") + if market_id: + print(f"\nFetching data for market: {market.get('question', market_id)}") + market_price_data = fetch_market_price_with_history(market_id) + market_data[market_id] = { + **market_price_data, + "question": market.get("question", market_id), + "url": market.get("url", "") + } + + current_price = market_price_data.get("current_price") + if current_price is not None: + print(f"Current price: {current_price:.3f}") + history = market_price_data.get("price_history", []) + print(f"History: {len(history)} days") -def example_reddit_fetcher() -> None: - """Example of using RedditFetcher.""" - print("\n=== Reddit Fetcher Example ===") + except Exception as e: + print(f"Error in Polymarket fetcher demo: {e}") - # Create a Reddit fetcher - reddit_fetcher = RedditFetcher() +def example_integrated_workflow() -> None: + """Example of integrated workflow using multiple fetchers.""" + print("\n=== Integrated Fetcher Workflow ===") + try: - # Get available categories - categories = reddit_fetcher.get_available_categories() - print(f"Available categories: {categories}") - - if categories: - # Get available dates for first category - dates = reddit_fetcher.get_available_dates(categories[0]) - print(f"Available dates for {categories[0]}: {len(dates)} dates") - - if dates: - # Fetch posts for first date - posts = reddit_fetcher.fetch_top_from_category( - category=categories[0], date=dates[0], max_limit=10 - ) - print(f"Fetched {len(posts)} posts") - - if posts: - print(f"Top post: {posts[0]['title']}") + # Step 1: Get market universe + print("1. Getting stock universe...") + stocks = fetch_trending_stocks(limit=2, for_date=None) + print(f"Selected stocks: {stocks}") + + # Step 2: Fetch market data + print("\n2. Fetching market data...") + stock_market_data = {} + for ticker in stocks: + stock_data = fetch_stock_price_with_history(ticker) + stock_market_data[ticker] = stock_data + print(f" {ticker}: ${stock_data.get('current_price', 'N/A')}") + + # Step 3: Fetch news data + print("\n3. Fetching related news...") + from live_trade_bench.fetchers.news_fetcher import fetch_news_data + from datetime import datetime, timedelta + + end_date = datetime.now() + start_date = end_date - timedelta(days=3) + + all_news = [] + for ticker in stocks[:1]: # Just test one to avoid too many requests + news = fetch_news_data( + query=f"{ticker} stock", + start_date=start_date.strftime("%Y-%m-%d"), + end_date=end_date.strftime("%Y-%m-%d"), + max_pages=1, + ticker=ticker + ) + all_news.extend(news) + print(f" {ticker}: {len(news)} articles") + + # Step 4: Test backtest data + print("\n4. Testing backtest data...") + backtest_date = (datetime.now() - timedelta(days=7)).strftime("%Y-%m-%d") + historical_stocks = fetch_trending_stocks(limit=2, for_date=backtest_date) + print(f" Historical stocks for {backtest_date}: {historical_stocks}") + + if all_news: + print(f"\n5. Sample news processing:") + for item in all_news[:2]: + print(f" - {item.get('title', 'No title')}") + print(f" Date: {item.get('date', 'Unknown')}") + except Exception as e: - print(f"Error fetching Reddit data: {e}") + print(f"Error in integrated workflow: {e}") def example_context_manager() -> None: @@ -196,8 +198,8 @@ def example_context_manager() -> None: # Use fetcher as context manager for automatic cleanup with StockFetcher() as fetcher: try: - data = fetcher.fetch("AAPL", "2024-01-01", "2024-01-05", "D") - print(f"Fetched {len(data)} days of data using context manager") + trending = fetcher.get_trending_stocks(limit=3) + print(f"Fetched {len(trending)} trending stocks using context manager: {trending}") except Exception as e: print(f"Error: {e}") @@ -229,20 +231,25 @@ def fetch(self, query: str, limit: int = 10) -> List[Dict[str, Any]]: def main() -> None: - """Run all examples.""" - print("Fetcher Architecture Examples") - print("=" * 50) + """Run all enhanced fetcher examples.""" + print("Enhanced Fetcher Architecture Examples") + print("=" * 60) example_stock_fetcher() example_news_fetcher() - example_option_fetcher() example_polymarket_fetcher() - example_reddit_fetcher() + example_integrated_workflow() example_context_manager() example_custom_fetcher() - print("\n" + "=" * 50) - print("All examples completed!") + print("\n" + "=" * 60) + print("All enhanced fetcher examples completed!") + print("\nUnified fetcher interfaces demonstrated:") + print("- fetch_trending_* with for_date parameter (None=today, date=historical)") + print("- fetch_*_price_with_history for both asset types") + print("- Single interface for both live and backtest data") + print("- Completely symmetric design across asset types") + print("- Minimal, consolidated API surface") if __name__ == "__main__": diff --git a/live_trade_bench/accounts/base_account.py b/live_trade_bench/accounts/base_account.py index 2a46725..ae07365 100644 --- a/live_trade_bench/accounts/base_account.py +++ b/live_trade_bench/accounts/base_account.py @@ -49,41 +49,16 @@ class BaseAccount(ABC, Generic[PositionType, TransactionType]): allocation_history: List[Dict[str, Any]] = field(default_factory=list) last_rebalance: Optional[str] = None - def record_allocation( - self, - metadata_map: Optional[Dict[str, Dict[str, Any]]] = None, - backtest_date: Optional[str] = None, - llm_input: Optional[Dict[str, Any]] = None, - llm_output: Optional[Dict[str, Any]] = None, - ): + def record_allocation(self, backtest_date: Optional[str] = None): total_value = self.get_total_value() profit = total_value - self.initial_cash performance = (profit / self.initial_cash) * 100 if self.initial_cash > 0 else 0 - # Create allocation array format with URL information for frontend compatibility - allocations_array = [] - for asset, allocation in self.target_allocations.items(): - asset_info = {"name": asset, "allocation": allocation} - - # Add URL if available in metadata - if metadata_map and asset in metadata_map: - url = metadata_map[asset].get("url") - if url: - asset_info["url"] = url - question = metadata_map[asset].get("question") - if question: - asset_info["question"] = question - - allocations_array.append(asset_info) - - # Use backtest date if provided, otherwise use current time if backtest_date: - # Parse the date and use it as the timestamp (keeping only date part, not time) try: backtest_datetime = datetime.strptime(backtest_date, "%Y-%m-%d") timestamp = backtest_datetime.isoformat() except ValueError: - # Fallback to current time if date parsing fails timestamp = datetime.now().isoformat() else: timestamp = datetime.now().isoformat() @@ -93,12 +68,10 @@ def record_allocation( "total_value": total_value, "profit": profit, "performance": performance, - "allocations": self.target_allocations, # Keep original format for compatibility - "allocations_array": allocations_array, # New format for frontend - "llm_input": llm_input, - "llm_output": llm_output, + "allocations": self.target_allocations.copy(), } self.allocation_history.append(snapshot) + self.last_rebalance = timestamp def get_total_value(self) -> float: return self.cash_balance + self.get_positions_value() @@ -130,32 +103,22 @@ def get_breakdown(self) -> Dict[str, Any]: } def get_account_data(self) -> Dict[str, Any]: - breakdown = self.get_breakdown() - total_value = breakdown.get("total_value", self.initial_cash) + total_value = self.get_total_value() profit = total_value - self.initial_cash performance = (profit / self.initial_cash) * 100 if self.initial_cash > 0 else 0 - # Create a serializable portfolio object - portfolio_details = { - "cash": self.cash_balance, - "total_value": total_value, - "positions": self.serialize_positions(), - "target_allocations": breakdown.get("target_allocations", {}), - "current_allocations": breakdown.get("current_allocations", {}), - } - - base_data = { + return { "profit": profit, "performance": performance, - "portfolio": portfolio_details, + "total_value": total_value, + "cash_balance": self.cash_balance, + "positions_value": self.get_positions_value(), + "target_allocations": self.target_allocations.copy(), + "current_allocations": self.get_allocations(), "allocation_history": self.allocation_history, "market_type": self.get_market_type(), } - # Allow subclasses to add additional fields - base_data.update(self.get_additional_account_data()) - return base_data - @abstractmethod def apply_allocation( self, @@ -177,11 +140,3 @@ def get_market_type(self) -> str: """Return the market type identifier (e.g., 'stock', 'polymarket')""" ... - @abstractmethod - def serialize_positions(self) -> Dict[str, Any]: - """Serialize positions for API response in market-specific format""" - ... - - def get_additional_account_data(self) -> Dict[str, Any]: - """Override in subclasses to add market-specific account data""" - return {} diff --git a/live_trade_bench/accounts/polymarket_account.py b/live_trade_bench/accounts/polymarket_account.py index c1b667a..541b816 100644 --- a/live_trade_bench/accounts/polymarket_account.py +++ b/live_trade_bench/accounts/polymarket_account.py @@ -32,7 +32,6 @@ def apply_allocation( self, target_allocations: Dict[str, float], price_map: Optional[Dict[str, float]] = None, - metadata_map: Optional[Dict[str, Dict[str, Any]]] = None, ) -> None: if not price_map: price_map = { diff --git a/live_trade_bench/accounts/stock_account.py b/live_trade_bench/accounts/stock_account.py index d8a92c1..760d728 100644 --- a/live_trade_bench/accounts/stock_account.py +++ b/live_trade_bench/accounts/stock_account.py @@ -40,7 +40,6 @@ def apply_allocation( self, target_allocations: Dict[str, float], price_map: Optional[Dict[str, float]] = None, - metadata_map: Optional[Dict[str, Dict[str, Any]]] = None, ) -> None: if not price_map: price_map = { diff --git a/live_trade_bench/fetchers/__init__.py b/live_trade_bench/fetchers/__init__.py index b50d2e0..1cce01b 100644 --- a/live_trade_bench/fetchers/__init__.py +++ b/live_trade_bench/fetchers/__init__.py @@ -8,8 +8,8 @@ from .news_fetcher import NewsFetcher from .polymarket_fetcher import ( PolymarketFetcher, - fetch_current_market_price, fetch_trending_markets, + fetch_market_price_with_history, ) if TYPE_CHECKING: @@ -18,8 +18,8 @@ from .reddit_fetcher import RedditFetcher from .stock_fetcher import ( StockFetcher, - fetch_current_stock_price, fetch_trending_stocks, + fetch_stock_price_with_history, ) else: # Runtime imports - try but degrade gracefully @@ -31,13 +31,13 @@ try: from .stock_fetcher import ( StockFetcher, - fetch_current_stock_price, fetch_trending_stocks, + fetch_stock_price_with_history, ) except Exception: StockFetcher = None # type: ignore fetch_trending_stocks = None # type: ignore - fetch_current_stock_price = None # type: ignore + fetch_stock_price_with_history = None # type: ignore try: from .reddit_fetcher import RedditFetcher # type: ignore @@ -48,10 +48,10 @@ # Export only the classes and main functions that are actually used __all__ = [ "BaseFetcher", - "NewsFetcher", + "NewsFetcher", "PolymarketFetcher", "fetch_trending_markets", - "fetch_current_market_price", + "fetch_market_price_with_history", ] if OptionFetcher is not None: @@ -59,7 +59,7 @@ if StockFetcher is not None: __all__.extend( - ["StockFetcher", "fetch_trending_stocks", "fetch_current_stock_price"] + ["StockFetcher", "fetch_trending_stocks", "fetch_stock_price_with_history"] ) if RedditFetcher is not None: diff --git a/live_trade_bench/fetchers/polymarket_fetcher.py b/live_trade_bench/fetchers/polymarket_fetcher.py index 9511903..8e4ed32 100644 --- a/live_trade_bench/fetchers/polymarket_fetcher.py +++ b/live_trade_bench/fetchers/polymarket_fetcher.py @@ -363,80 +363,6 @@ def fetch_trending_markets( return valid_markets -def fetch_verified_historical_markets( - trading_days: List[datetime], limit: int -) -> List[Dict[str, Any]]: - fetcher = PolymarketFetcher() - return fetcher.get_verified_historical_markets(trading_days, limit) - - -def fetch_current_market_price(token_ids: List[str]) -> Dict[str, Any]: - prices = PolymarketFetcher().get_market_prices(token_ids) - if prices and prices.get("yes") is not None: - question = prices.get("question") - yes_price = prices["yes"] - no_price = prices.get("no") - - if no_price is None: - no_price = 1.0 - yes_price - - if question: - question_short = question[:40] + "..." if len(question) > 40 else question - print(f"💰 {question_short}") - print(f" YES: {yes_price:.3f} | NO: {no_price:.3f}") - else: - print(f"💰 YES: {yes_price:.3f} | NO: {no_price:.3f}") - - return { - f"{question}_YES": {"price": yes_price, "outcome": "YES"}, - f"{question}_NO": {"price": no_price, "outcome": "NO"}, - } - return {} - - -def fetch_market_price_on_date(token_ids: List[str], date: str) -> Dict[str, Any]: - """ - Fetches the historical price for a market on a specific date and returns it - in the new question-based format, consistent with fetch_current_market_price. - """ - if not token_ids: - return {} - - fetcher = PolymarketFetcher() - yes_token_id = token_ids[0] - - # We need the question for the new format - market_info = PolymarketFetcher.get_market_info_by_token(yes_token_id) - if not market_info or not market_info.get("question"): - return {} - question = market_info["question"] - - price = fetcher.get_market_price_on_date(yes_token_id, date) - - if price is not None: - return { - f"{question}_YES": {"price": price, "outcome": "YES"}, - f"{question}_NO": {"price": 1.0 - price, "outcome": "NO"}, - } - return {} - - -def fetch_token_price(token_id: str, side: str = "buy") -> Optional[float]: - price = PolymarketFetcher().get_token_price(token_id, side) - market_info = PolymarketFetcher.get_market_info_by_token(token_id) - if price: - if market_info and market_info.get("question"): - question_short = ( - market_info["question"][:30] + "..." - if len(market_info["question"]) > 30 - else market_info["question"] - ) - print(f"🪙 {question_short}: {price:.4f}") - else: - print(f"🪙 Token price: {price:.4f}") - return price - - def get_question_by_token_id(token_id: str) -> Optional[str]: market_info = PolymarketFetcher.get_market_info_by_token(token_id) return market_info.get("question") if market_info else None diff --git a/live_trade_bench/fetchers/stock_fetcher.py b/live_trade_bench/fetchers/stock_fetcher.py index 10f6fa4..574cb9e 100644 --- a/live_trade_bench/fetchers/stock_fetcher.py +++ b/live_trade_bench/fetchers/stock_fetcher.py @@ -186,24 +186,22 @@ def fetch_stock_data( return self._download_price_data(ticker, start_date, end_date, interval) -def fetch_trending_stocks(limit: int = 15) -> List[str]: +def fetch_trending_stocks(limit: int = 15, for_date: Optional[str] = None) -> List[str]: + """ + Fetch trending stocks for a specific date or today. + + Args: + limit: Number of stocks to return + for_date: Date in YYYY-MM-DD format. If None, returns current trending stocks + + Returns: + List of stock tickers + """ fetcher = StockFetcher() + # For stocks, trending list doesn't change much historically, so return same list return fetcher.get_trending_stocks(limit=limit) -def fetch_current_stock_price(ticker: str) -> Optional[float]: - fetcher = StockFetcher() - return fetcher.get_price(ticker) - - -def fetch_stock_price_on_date(ticker: str, date: str) -> Optional[float]: - fetcher = StockFetcher() - return fetcher.get_price(ticker, date=date) - - -def fetch_stock_price(ticker: str, date: Optional[str] = None) -> Optional[float]: - fetcher = StockFetcher() - return fetcher.get_price(ticker, date=date) def fetch_stock_price_with_history( @@ -212,3 +210,5 @@ def fetch_stock_price_with_history( """Fetch current price and 10-day price history for a stock""" fetcher = StockFetcher() return fetcher.get_price_with_history(ticker, date=date) + + diff --git a/live_trade_bench/systems/polymarket_system.py b/live_trade_bench/systems/polymarket_system.py index 1cf0bbc..405ae5f 100644 --- a/live_trade_bench/systems/polymarket_system.py +++ b/live_trade_bench/systems/polymarket_system.py @@ -1,7 +1,7 @@ from __future__ import annotations from datetime import datetime, timedelta -from typing import Any, Dict, List +from typing import Any, Dict, List, Optional from ..accounts import PolymarketAccount, create_polymarket_account from ..agents.polymarket_agent import LLMPolyMarketAgent @@ -9,7 +9,6 @@ from ..fetchers.polymarket_fetcher import ( fetch_market_price_with_history, fetch_trending_markets, - fetch_verified_historical_markets, ) @@ -26,8 +25,10 @@ def __init__(self, universe_size: int = 5) -> None: def initialize_for_backtest(self, trading_days: List[datetime]): print("--- Initializing Polymarket universe for backtest period... ---") - verified_markets = fetch_verified_historical_markets( - trading_days, self.universe_size + # Use the earliest trading day to get historical markets + earliest_date = min(trading_days).strftime("%Y-%m-%d") + verified_markets = fetch_trending_markets( + limit=self.universe_size, for_date=earliest_date ) self.set_universe(verified_markets) print( @@ -35,7 +36,7 @@ def initialize_for_backtest(self, trading_days: List[datetime]): ) def initialize_for_live(self): - markets = fetch_trending_markets(limit=self.universe_size) + markets = fetch_trending_markets(limit=self.universe_size, for_date=None) self.set_universe(markets) def set_universe(self, markets: List[Dict[str, Any]]): @@ -307,22 +308,14 @@ def _update_accounts( # Rebalance account to target allocation try: - account.apply_allocation( - allocation, price_map=price_map, metadata_map=market_data - ) - # Capture and persist agent LLM info if available - llm_input = None - llm_output = None + account.apply_allocation(allocation, price_map=price_map) agent = self.agents.get(agent_name) - if agent is not None: - llm_input = getattr(agent, "last_llm_input", None) - llm_output = getattr(agent, "last_llm_output", None) - account.record_allocation( - metadata_map=market_data, - backtest_date=for_date, - llm_input=llm_input, - llm_output=llm_output - ) + + # Record allocation with LLM data for debugging/auditing + account.record_allocation(backtest_date=for_date) + if agent and hasattr(agent, 'last_llm_input') and hasattr(agent, 'last_llm_output'): + self._record_llm_data(agent_name, agent.last_llm_input, agent.last_llm_output, for_date) + print( f" - ✅ Account for {agent_name} updated. New Value: ${account.get_total_value():,.2f}, Cash: ${account.cash_balance:,.2f}" ) @@ -331,6 +324,88 @@ def _update_accounts( print(" - ✅ All accounts updated") + def _record_llm_data(self, agent_name: str, llm_input: Any, llm_output: Any, date: Optional[str] = None): + """Record LLM input/output for debugging and auditing""" + if not hasattr(self, '_llm_logs'): + self._llm_logs = [] + + timestamp = date if date else datetime.now().strftime("%Y-%m-%d %H:%M:%S") + self._llm_logs.append({ + "timestamp": timestamp, + "agent_name": agent_name, + "llm_input": llm_input, + "llm_output": llm_output + }) + + def get_frontend_data(self) -> Dict[str, Any]: + """Format account data for frontend consumption""" + frontend_data = {} + + for agent_name, account in self.accounts.items(): + account_data = account.get_account_data() + + # Create allocation array format with metadata for frontend + allocations_array = [] + for asset, allocation in account.target_allocations.items(): + asset_info = {"name": asset, "allocation": allocation} + + # Add market metadata if available + if hasattr(self, 'market_info') and asset in self.market_info: + market_data = self.market_info[asset] + if market_data.get("question"): + asset_info["question"] = market_data["question"] + if market_data.get("url"): + asset_info["url"] = market_data["url"] + + allocations_array.append(asset_info) + + # Enhanced allocation history for frontend + enhanced_history = [] + for snapshot in account.allocation_history: + enhanced_snapshot = snapshot.copy() + enhanced_snapshot["allocations_array"] = [] + + for asset, allocation in snapshot.get("allocations", {}).items(): + asset_info = {"name": asset, "allocation": allocation} + if hasattr(self, 'market_info') and asset in self.market_info: + market_data = self.market_info[asset] + if market_data.get("question"): + asset_info["question"] = market_data["question"] + if market_data.get("url"): + asset_info["url"] = market_data["url"] + enhanced_snapshot["allocations_array"].append(asset_info) + + enhanced_history.append(enhanced_snapshot) + + frontend_data[agent_name] = { + **account_data, + "allocations_array": allocations_array, + "allocation_history": enhanced_history, + "portfolio": { + "cash": account.cash_balance, + "total_value": account.get_total_value(), + "positions": self._serialize_positions(account), + "target_allocations": account.target_allocations, + "current_allocations": account.get_allocations(), + } + } + + return frontend_data + + def _serialize_positions(self, account) -> Dict[str, Any]: + """Serialize positions for frontend""" + positions_data = {} + for ticker, position in account.get_positions().items(): + positions_data[ticker] = { + "symbol": position.symbol, + "quantity": position.quantity, + "average_price": position.average_price, + "current_price": position.current_price, + "market_value": position.market_value, + "unrealized_pnl": position.unrealized_pnl, + } + return positions_data + @classmethod def get_instance(cls): if not hasattr(cls, "_instance"): diff --git a/live_trade_bench/systems/stock_system.py b/live_trade_bench/systems/stock_system.py index 444f4d8..c024715 100644 --- a/live_trade_bench/systems/stock_system.py +++ b/live_trade_bench/systems/stock_system.py @@ -1,7 +1,7 @@ from __future__ import annotations from datetime import datetime, timedelta -from typing import Any, Dict, List +from typing import Any, Dict, List, Optional from ..accounts import StockAccount, create_stock_account from ..agents.stock_agent import LLMStockAgent @@ -22,7 +22,7 @@ def __init__(self, universe_size: int = 10) -> None: self.universe_size = universe_size def initialize_for_live(self): - tickers = fetch_trending_stocks(limit=self.universe_size) + tickers = fetch_trending_stocks(limit=self.universe_size, for_date=None) self.universe = tickers self.stock_info = { ticker: {"name": ticker, "sector": "Unknown", "market_cap": 0} @@ -108,7 +108,7 @@ def _fetch_social_data(self) -> Dict[str, List[Dict[str, Any]]]: today = datetime.now().strftime("%Y-%m-%d") # Update universe with latest trending stocks for social media fetching - latest_trending_stocks = fetch_trending_stocks(limit=self.universe_size) + latest_trending_stocks = fetch_trending_stocks(limit=self.universe_size, for_date=None) if latest_trending_stocks: self.universe = latest_trending_stocks print( @@ -227,22 +227,14 @@ def _update_accounts( account = self.accounts[agent_name] account.target_allocations = allocation try: - account.apply_allocation( - allocation, price_map=price_map, metadata_map=market_data - ) - # Capture and persist agent LLM info if available - llm_input = None - llm_output = None + account.apply_allocation(allocation, price_map=price_map) agent = self.agents.get(agent_name) - if agent is not None: - llm_input = getattr(agent, "last_llm_input", None) - llm_output = getattr(agent, "last_llm_output", None) - account.record_allocation( - metadata_map=market_data, - backtest_date=for_date, - llm_input=llm_input, - llm_output=llm_output - ) + + # Record allocation with LLM data for debugging/auditing + account.record_allocation(backtest_date=for_date) + if agent and hasattr(agent, 'last_llm_input') and hasattr(agent, 'last_llm_output'): + self._record_llm_data(agent_name, agent.last_llm_input, agent.last_llm_output, for_date) + print( f" - ✅ Account for {agent_name} updated. New Value: ${account.get_total_value():,.2f}, Cash: ${account.cash_balance:,.2f}" ) @@ -250,6 +242,80 @@ def _update_accounts( print(f" - ❌ Failed to update account for {agent_name}: {e}") print(" - ✅ All accounts updated") + def _record_llm_data(self, agent_name: str, llm_input: Any, llm_output: Any, date: Optional[str] = None): + """Record LLM input/output for debugging and auditing""" + if not hasattr(self, '_llm_logs'): + self._llm_logs = [] + + timestamp = date if date else datetime.now().strftime("%Y-%m-%d %H:%M:%S") + self._llm_logs.append({ + "timestamp": timestamp, + "agent_name": agent_name, + "llm_input": llm_input, + "llm_output": llm_output + }) + + def get_frontend_data(self) -> Dict[str, Any]: + """Format account data for frontend consumption""" + frontend_data = {} + + for agent_name, account in self.accounts.items(): + account_data = account.get_account_data() + + # Create allocation array format with metadata for frontend + allocations_array = [] + for asset, allocation in account.target_allocations.items(): + asset_info = {"name": asset, "allocation": allocation} + + # Add stock metadata if available + if hasattr(self, 'stock_info') and asset in self.stock_info: + stock_data = self.stock_info[asset] + if stock_data.get("sector"): + asset_info["sector"] = stock_data["sector"] + + allocations_array.append(asset_info) + + # Enhanced allocation history for frontend + enhanced_history = [] + for snapshot in account.allocation_history: + enhanced_snapshot = snapshot.copy() + enhanced_snapshot["allocations_array"] = [] + + for asset, allocation in snapshot.get("allocations", {}).items(): + asset_info = {"name": asset, "allocation": allocation} + enhanced_snapshot["allocations_array"].append(asset_info) + + enhanced_history.append(enhanced_snapshot) + + frontend_data[agent_name] = { + **account_data, + "allocations_array": allocations_array, + "allocation_history": enhanced_history, + "portfolio": { + "cash": account.cash_balance, + "total_value": account.get_total_value(), + "positions": self._serialize_positions(account), + "target_allocations": account.target_allocations, + "current_allocations": account.get_allocations(), + } + } + + return frontend_data + + def _serialize_positions(self, account) -> Dict[str, Any]: + """Serialize positions for frontend""" + positions_data = {} + for ticker, position in account.get_positions().items(): + positions_data[ticker] = { + "symbol": position.symbol, + "quantity": position.quantity, + "average_price": position.average_price, + "current_price": position.current_price, + "market_value": position.market_value, + "unrealized_pnl": position.unrealized_pnl, + } + return positions_data + @classmethod def get_instance(cls): if not hasattr(cls, "_instance"):