From b3ef20055dc4b2806b25a631638aacd9523dafe1 Mon Sep 17 00:00:00 2001 From: Wu Yue <143110833+1mycell@users.noreply.github.com> Date: Mon, 15 Dec 2025 10:47:39 +0800 Subject: [PATCH] Fix EvoTraders homepage url; fix baseline ,dashboard property, search line items and memory (#80) --- evotraders/README.md | 2 +- evotraders/README_zh.md | 2 +- evotraders/backend/agents/analyst.py | 2 +- evotraders/backend/core/pipeline.py | 1 + evotraders/backend/core/state_sync.py | 42 +++++ evotraders/backend/services/gateway.py | 63 +++++-- evotraders/backend/tests/test_settlement.py | 10 +- evotraders/backend/tools/data_tools.py | 64 ++++--- evotraders/backend/utils/analyst_tracker.py | 174 +++++++++++++++++- evotraders/backend/utils/baselines.py | 93 +++++++--- evotraders/backend/utils/settlement.py | 40 +++- evotraders/frontend/src/App.jsx | 11 +- .../frontend/src/components/AgentCard.jsx | 8 +- .../frontend/src/components/AgentFeed.jsx | 4 +- .../src/components/StatisticsView.jsx | 137 ++++++++++---- evotraders/pyproject.toml | 2 +- 16 files changed, 536 insertions(+), 119 deletions(-) diff --git a/evotraders/README.md b/evotraders/README.md index 52b6f85..e2271d6 100644 --- a/evotraders/README.md +++ b/evotraders/README.md @@ -5,7 +5,7 @@

EvoTraders: A Self-Evolving Multi-Agent Trading System

- 📌 Visit us at EvoTraders website ! + 📌 Visit us at EvoTraders website !

![System Demo](./docs/assets/evotraders_demo.gif) diff --git a/evotraders/README_zh.md b/evotraders/README_zh.md index 5a6c1f9..5791a2f 100644 --- a/evotraders/README_zh.md +++ b/evotraders/README_zh.md @@ -6,7 +6,7 @@

- 📌 Visit us at EvoTraders website ! + 📌 Visit us at EvoTraders website !

![įģŧįŧŸæž”įĪš](./docs/assets/evotraders_demo.gif) diff --git a/evotraders/backend/agents/analyst.py b/evotraders/backend/agents/analyst.py index a524a11..6676e1c 100644 --- a/evotraders/backend/agents/analyst.py +++ b/evotraders/backend/agents/analyst.py @@ -112,7 +112,7 @@ class AnalystAgent(ReActAgent): """ ticker = None if x and hasattr(x, "metadata") and x.metadata: - ticker = x.metadata.get("ticker") + ticker = x.metadata.get("tickers") if ticker: progress.update_status( diff --git a/evotraders/backend/core/pipeline.py b/evotraders/backend/core/pipeline.py index aa6fdcf..357a704 100644 --- a/evotraders/backend/core/pipeline.py +++ b/evotraders/backend/core/pipeline.py @@ -199,6 +199,7 @@ class TradingPipeline: market_caps=market_caps, agent_portfolio=execution_result.get("portfolio", {}), analyst_results=analyst_results, + pm_decisions=decisions, ) ) diff --git a/evotraders/backend/core/state_sync.py b/evotraders/backend/core/state_sync.py index 5f67323..92a4a40 100644 --- a/evotraders/backend/core/state_sync.py +++ b/evotraders/backend/core/state_sync.py @@ -299,6 +299,37 @@ class StateSync: "momentum_return" ] + if "portfolio" not in self._state: + self._state["portfolio"] = {} + + self._state["portfolio"].update( + { + "total_value": summary_data["balance"], + "pnl_percent": summary_data["pnlPct"], + "equity": summary_data["equity"], + "baseline": summary_data["baseline"], + "baseline_vw": summary_data["baseline_vw"], + "momentum": summary_data["momentum"], + }, + ) + + if summary_data.get("equity_return"): + self._state["portfolio"]["equity_return"] = summary_data[ + "equity_return" + ] + if summary_data.get("baseline_return"): + self._state["portfolio"]["baseline_return"] = summary_data[ + "baseline_return" + ] + if summary_data.get("baseline_vw_return"): + self._state["portfolio"]["baseline_vw_return"] = summary_data[ + "baseline_vw_return" + ] + if summary_data.get("momentum_return"): + self._state["portfolio"]["momentum_return"] = summary_data[ + "momentum_return" + ] + await self.emit(summary_data, persist=True) await self.emit( @@ -350,6 +381,17 @@ class StateSync: persist=False, ) + async def on_leaderboard_update(self, leaderboard: List[Dict]): + """Called when leaderboard is updated""" + self._state["leaderboard"] = leaderboard + await self.emit( + { + "type": "team_leaderboard", + "data": leaderboard, + }, + persist=False, + ) + # ========== System Events ========== async def on_system_message(self, content: str): diff --git a/evotraders/backend/services/gateway.py b/evotraders/backend/services/gateway.py index 3fdd744..b456c32 100644 --- a/evotraders/backend/services/gateway.py +++ b/evotraders/backend/services/gateway.py @@ -202,9 +202,11 @@ class Gateway: return dates = data.get("dates", []) if dates and self._backtest_task is None: - self._backtest_task = asyncio.create_task( + task = asyncio.create_task( self._run_backtest_dates(dates), ) + task.add_done_callback(self._handle_backtest_exception) + self._backtest_task = task async def broadcast(self, message: Dict[str, Any]): """Broadcast message to all connected clients""" @@ -414,6 +416,11 @@ class Gateway: holdings = self.storage.load_file("holdings") or [] trades = self.storage.load_file("trades") or [] + leaderboard = self.storage.load_file("leaderboard") or [] + + if leaderboard: + await self.state_sync.on_leaderboard_update(leaderboard) + self._dashboard.update( date=date, status="Running", @@ -504,26 +511,46 @@ class Gateway: f"Starting backtest - {len(dates)} trading days", ) - for i, date in enumerate(dates): - self._dashboard.update(days_completed=i) - await self.on_strategy_trigger(date=date) - await asyncio.sleep(0.1) + try: + for i, date in enumerate(dates): + self._dashboard.update(days_completed=i) + await self.on_strategy_trigger(date=date) + await asyncio.sleep(0.1) - await self.state_sync.on_system_message( - f"Backtest complete - {len(dates)} days", - ) + await self.state_sync.on_system_message( + f"Backtest complete - {len(dates)} days", + ) - # Update dashboard with final state - summary = self.storage.load_file("summary") or {} - self._dashboard.update( - status="Complete", - portfolio=summary, - days_completed=len(dates), - ) - self._dashboard.stop() - self._dashboard.print_final_summary() + # Update dashboard with final state + summary = self.storage.load_file("summary") or {} + self._dashboard.update( + status="Complete", + portfolio=summary, + days_completed=len(dates), + ) + self._dashboard.stop() + self._dashboard.print_final_summary() + except Exception as e: + error_msg = f"Backtest failed: {type(e).__name__}: {str(e)}" + logger.error(error_msg, exc_info=True) + await self.state_sync.on_system_message(error_msg) + self._dashboard.update(status=f"Failed: {str(e)}") + self._dashboard.stop() + raise + finally: + self._backtest_task = None - self._backtest_task = None + def _handle_backtest_exception(self, task: asyncio.Task): + """Handle exceptions from backtest task""" + try: + task.result() + except asyncio.CancelledError: + logger.info("Backtest task was cancelled") + except Exception as e: + logger.error( + f"Backtest task failed with exception:{type(e).__name__}:{e}", + exc_info=True, + ) def set_backtest_dates(self, dates: List[str]): self.state_sync.set_backtest_dates(dates) diff --git a/evotraders/backend/tests/test_settlement.py b/evotraders/backend/tests/test_settlement.py index e8e17c9..13a3717 100644 --- a/evotraders/backend/tests/test_settlement.py +++ b/evotraders/backend/tests/test_settlement.py @@ -19,8 +19,12 @@ def test_baseline_equal_weight(): tickers = ["AAPL", "MSFT", "GOOGL"] prices = {"AAPL": 150.0, "MSFT": 300.0, "GOOGL": 120.0} - - value = calculator.calculate_equal_weight_value(tickers, prices) + openprices = {"AAPL": 160.0, "MSFT": 310.0, "GOOGL": 110.0} + value = calculator.calculate_equal_weight_value( + tickers, + openprices, + prices, + ) assert value > 0 assert calculator.equal_weight_initialized is True @@ -32,10 +36,12 @@ def test_baseline_market_cap_weighted(): tickers = ["AAPL", "MSFT", "GOOGL"] prices = {"AAPL": 150.0, "MSFT": 300.0, "GOOGL": 120.0} + openprices = {"AAPL": 160.0, "MSFT": 310.0, "GOOGL": 110.0} market_caps = {"AAPL": 3e12, "MSFT": 2e12, "GOOGL": 1.5e12} value = calculator.calculate_market_cap_weighted_value( tickers, + openprices, prices, market_caps, ) diff --git a/evotraders/backend/tools/data_tools.py b/evotraders/backend/tools/data_tools.py index 498441d..d66850b 100644 --- a/evotraders/backend/tools/data_tools.py +++ b/evotraders/backend/tools/data_tools.py @@ -34,6 +34,7 @@ from backend.data.schema import ( Price, PriceResponse, ) +from backend.utils.settlement import logger # Global cache instance _cache = get_cache() @@ -366,30 +367,51 @@ def search_line_items( period: str = "ttm", limit: int = 10, ) -> list[LineItem]: - """Fetch line items from Financial Datasets API (only supported source).""" - api_key = get_api_key() - headers = {"X-API-KEY": api_key} + """ + Fetch line items from Financial Datasets API (only supported source). - url = "https://api.financialdatasets.ai/financials/search/line-items" - body = { - "tickers": [ticker], - "line_items": line_items, - "end_date": end_date, - "period": period, - "limit": limit, - } - response = _make_api_request(url, headers, method="POST", json_data=body) - if response.status_code != 200: - raise ValueError( - f"Error fetching data: {ticker} - {response.status_code} - {response.text}", + Returns empty list on API errors to allow graceful degradation. + """ + try: + api_key = get_api_key() + headers = {"X-API-KEY": api_key} + + url = "https://api.financialdatasets.ai/financials/search/line-items" + body = { + "tickers": [ticker], + "line_items": line_items, + "end_date": end_date, + "period": period, + "limit": limit, + } + response = _make_api_request( + url, + headers, + method="POST", + json_data=body, ) - data = response.json() - response_model = LineItemResponse(**data) - search_results = response_model.search_results - if not search_results: - return [] - return search_results[:limit] + if response.status_code != 200: + logger.info( + f"Warning: Failed to fetch line items for {ticker}: " + f"{response.status_code} - {response.text}", + ) + return [] + + data = response.json() + response_model = LineItemResponse(**data) + search_results = response_model.search_results + + if not search_results: + return [] + + return search_results[:limit] + + except Exception as e: + logger.info( + f"Warning: Exception while fetching line items for {ticker}: {str(e)}", + ) + return [] def _fetch_finnhub_insider_trades( diff --git a/evotraders/backend/utils/analyst_tracker.py b/evotraders/backend/utils/analyst_tracker.py index 6942afd..c51d6b6 100644 --- a/evotraders/backend/utils/analyst_tracker.py +++ b/evotraders/backend/utils/analyst_tracker.py @@ -5,7 +5,7 @@ Tracks analyst predictions and calculates win rates for leaderboard """ import logging -from typing import Any, Dict, List, Optional +from typing import Any, Dict, List, Optional, Tuple logger = logging.getLogger(__name__) @@ -214,6 +214,178 @@ class AnalystPerformanceTracker: """Clear predictions after evaluation""" self.daily_predictions = {} + def _process_single_pm_decision( + self, + _ticker: str, + decision: Dict, + open_price: float, + close_price: float, + _date: str, + ) -> Tuple[str, Optional[bool], str]: + """ + Process a single PM decision and evaluate correctness + + Returns: + Tuple of (prediction, is_correct, signal_type) + """ + action = decision.get("action", "hold") + + # Convert action to prediction format + if action in ["buy", "long"]: + prediction = "long" + elif action in ["sell", "short"]: + prediction = "short" + else: + prediction = "hold" + + signal_display_map = { + "long": "bull", + "short": "bear", + "hold": "neutral", + } + signal_type = signal_display_map.get(prediction, "neutral") + + # Handle invalid prices + if open_price <= 0 or close_price <= 0: + return prediction, None, signal_type + + # Evaluate correctness + actual_return = (close_price - open_price) / open_price + + if prediction == "long": + is_correct = actual_return > 0 + elif prediction == "short": + is_correct = actual_return < 0 + else: # hold + is_correct = None + + return prediction, is_correct, signal_type + + def evaluate_pm_decisions( + self, + pm_decisions: Dict[str, Dict], + open_prices: Optional[Dict[str, float]], + close_prices: Dict[str, float], + date: str, + ) -> Dict[str, Dict[str, Any]]: + """ + Evaluate PM's trading decisions against actual market moves + + Args: + pm_decisions: PM decisions {ticker: {action, quantity, ...}} + open_prices: Opening prices for each ticker + close_prices: Closing prices for each ticker + date: Trading date string (YYYY-MM-DD) + + Returns: + Dict with 'portfolio_manager' key containing evaluation results + """ + if not pm_decisions or not open_prices or not close_prices: + return {} + + correct_long = 0 + correct_short = 0 + incorrect_long = 0 + incorrect_short = 0 + unknown_long = 0 + unknown_short = 0 + hold_count = 0 + + individual_signals: List[Dict[str, Any]] = [] + + for ticker, decision in pm_decisions.items(): + open_price = open_prices.get(ticker, 0) + close_price = close_prices.get(ticker, 0) + + ( + prediction, + is_correct, + signal_type, + ) = self._process_single_pm_decision( + ticker, + decision, + open_price, + close_price, + date, + ) + + if is_correct is None and (open_price <= 0 or close_price <= 0): + if prediction == "long": + unknown_long += 1 + elif prediction == "short": + unknown_short += 1 + individual_signals.append( + { + "ticker": ticker, + "signal": signal_type, + "date": date, + "is_correct": "unknown", + }, + ) + elif prediction == "hold": + hold_count += 1 + individual_signals.append( + { + "ticker": ticker, + "signal": signal_type, + "date": date, + "is_correct": None, + }, + ) + else: + if prediction == "long": + if is_correct: + correct_long += 1 + else: + incorrect_long += 1 + else: + if is_correct: + correct_short += 1 + else: + incorrect_short += 1 + + individual_signals.append( + { + "ticker": ticker, + "signal": signal_type, + "date": date, + "is_correct": is_correct, + }, + ) + + total_long = correct_long + incorrect_long + unknown_long + total_short = correct_short + incorrect_short + unknown_short + evaluated_long = correct_long + incorrect_long + evaluated_short = correct_short + incorrect_short + total_evaluated = evaluated_long + evaluated_short + correct_predictions = correct_long + correct_short + + win_rate = ( + correct_predictions / total_evaluated + if total_evaluated > 0 + else None + ) + + return { + "portfolio_manager": { + "total_predictions": total_evaluated, + "correct_predictions": correct_predictions, + "win_rate": win_rate, + "bull": { + "n": total_long, + "win": correct_long, + "unknown": unknown_long, + }, + "bear": { + "n": total_short, + "win": correct_short, + "unknown": unknown_short, + }, + "hold": hold_count, + "signals": individual_signals, + }, + } + def update_leaderboard_with_evaluations( leaderboard: List[Dict[str, Any]], diff --git a/evotraders/backend/utils/baselines.py b/evotraders/backend/utils/baselines.py index 424039c..b732aeb 100644 --- a/evotraders/backend/utils/baselines.py +++ b/evotraders/backend/utils/baselines.py @@ -43,27 +43,39 @@ class BaselineCalculator: def calculate_equal_weight_value( self, tickers: List[str], - prices: Dict[str, float], + open_prices: Dict[str, float], + close_prices: Dict[str, float], ) -> float: """ Calculate equal-weight portfolio value - On first call, initialize positions with equal allocation - Subsequently, mark-to-market existing positions + On first call, initialize positions with equal allocation using + open prices. Subsequently, mark-to-market existing positions + using close prices. + + Args: + tickers: List of stock tickers + open_prices: Opening prices (used for initial purchase) + close_prices: Closing prices (used for valuation) """ if not self.equal_weight_initialized: allocation_per_ticker = self.initial_capital / len(tickers) self.equal_weight_portfolio["cash"] = 0.0 for ticker in tickers: - price = prices.get(ticker, 0) + price = open_prices.get(ticker, 0) # Use OPEN price for buying if price > 0: shares = allocation_per_ticker / price self.equal_weight_portfolio["positions"][ticker] = shares + logger.info( + f"Equal Weight: Initialized {ticker} with " + f"{shares:.2f} shares @ ${price:.2f} (open)", + ) self.equal_weight_initialized = True total_value = self.equal_weight_portfolio["cash"] - for ticker, shares in self.equal_weight_portfolio["positions"].items(): - price = prices.get(ticker, 0) + positions: Dict[str, float] = self.equal_weight_portfolio["positions"] + for ticker, shares in positions.items(): + price = close_prices.get(ticker, 0) total_value += shares * price return total_value @@ -71,35 +83,53 @@ class BaselineCalculator: def calculate_market_cap_weighted_value( self, tickers: List[str], - prices: Dict[str, float], + open_prices: Dict[str, float], + close_prices: Dict[str, float], market_caps: Dict[str, float], ) -> float: """ Calculate market-cap-weighted portfolio value - On first call, initialize positions weighted by market cap - Subsequently, mark-to-market existing positions + On first call, initialize positions weighted by market cap using + open prices. Subsequently, mark-to-market existing positions + using close prices. + + Args: + tickers: List of stock tickers + open_prices: Opening prices (used for initial purchase) + close_prices: Closing prices (used for valuation) + market_caps: Market capitalization for each ticker """ if not self.market_cap_initialized: total_market_cap = sum(market_caps.get(t, 0) for t in tickers) if total_market_cap <= 0: logger.warning("No market cap data, using equal weight") - return self.calculate_equal_weight_value(tickers, prices) + return self.calculate_equal_weight_value( + tickers, + open_prices, + close_prices, + ) self.market_cap_portfolio["cash"] = 0.0 for ticker in tickers: market_cap = market_caps.get(ticker, 0) - price = prices.get(ticker, 0) + price = open_prices.get(ticker, 0) # Use OPEN price for buying if market_cap > 0 and price > 0: weight = market_cap / total_market_cap allocation = self.initial_capital * weight shares = allocation / price self.market_cap_portfolio["positions"][ticker] = shares + logger.info( + f"Market Cap Weighted: Initialized {ticker} with " + f"{shares:.2f} shares @ ${price:.2f} (open), " + f"weight={weight:.2%}", + ) self.market_cap_initialized = True total_value = self.market_cap_portfolio["cash"] - for ticker, shares in self.market_cap_portfolio["positions"].items(): - price = prices.get(ticker, 0) + positions: Dict[str, float] = self.market_cap_portfolio["positions"] + for ticker, shares in positions.items(): + price = close_prices.get(ticker, 0) total_value += shares * price return total_value @@ -107,7 +137,8 @@ class BaselineCalculator: def calculate_momentum_value( self, tickers: List[str], - prices: Dict[str, float], + open_prices: Dict[str, float], + close_prices: Dict[str, float], momentum_scores: Dict[str, float], date: str, rebalance: bool = False, @@ -122,7 +153,8 @@ class BaselineCalculator: Args: tickers: List of tickers - prices: Current prices + open_prices: Opening prices (used for rebalancing trades) + close_prices: Closing prices (used for valuation) momentum_scores: Momentum scores for each ticker date: Current date (YYYY-MM-DD) rebalance: Force rebalance if True @@ -145,14 +177,15 @@ class BaselineCalculator: if should_rebalance: self._rebalance_momentum_portfolio( tickers, - prices, + open_prices, momentum_scores, ) self.momentum_last_rebalance_date = date total_value = self.momentum_portfolio["cash"] - for ticker, shares in self.momentum_portfolio["positions"].items(): - price = prices.get(ticker, 0) + positions: Dict[str, float] = self.momentum_portfolio["positions"] + for ticker, shares in positions.items(): + price = close_prices.get(ticker, 0) total_value += shares * price return total_value @@ -201,7 +234,8 @@ class BaselineCalculator: def get_all_baseline_values( self, tickers: List[str], - prices: Dict[str, float], + open_prices: Dict[str, float], + close_prices: Dict[str, float], market_caps: Dict[str, float], momentum_scores: Dict[str, float], date: str, @@ -210,18 +244,33 @@ class BaselineCalculator: """ Get all baseline portfolio values in one call + Args: + tickers: List of stock tickers + open_prices: Opening prices (used for initial purchase/rebalancing) + close_prices: Closing prices (used for valuation) + market_caps: Market caps for each ticker + momentum_scores: Momentum scores for rebalancing + date: Current date + rebalance_momentum: Whether to rebalance momentum portfolio + Returns: Dict with keys: equal_weight, market_cap_weighted, momentum """ - equal_weight_value = self.calculate_equal_weight_value(tickers, prices) + equal_weight_value = self.calculate_equal_weight_value( + tickers, + open_prices, + close_prices, + ) market_cap_value = self.calculate_market_cap_weighted_value( tickers, - prices, + open_prices, + close_prices, market_caps, ) momentum_value = self.calculate_momentum_value( tickers, - prices, + open_prices, + close_prices, momentum_scores, date, rebalance_momentum, diff --git a/evotraders/backend/utils/settlement.py b/evotraders/backend/utils/settlement.py index a091302..d4f714e 100644 --- a/evotraders/backend/utils/settlement.py +++ b/evotraders/backend/utils/settlement.py @@ -70,12 +70,17 @@ class SettlementCoordinator: if saved_price_history: # Convert saved format back to list of tuples for ticker, history in saved_price_history.items(): - self.price_history[ticker] = [ - (entry["date"], entry["price"]) - if isinstance(entry, dict) - else tuple(entry) - for entry in history - ] + converted_history = [] + for entry in history: + if isinstance(entry, dict): + converted_history.append( + (entry["date"], entry["price"]), + ) + elif isinstance(entry, (list, tuple)) and len(entry) >= 2: + converted_history.append((entry[0], entry[1])) + else: + continue + self.price_history[ticker] = converted_history logger.info( f"Restored price history for {len(self.price_history)} tickers", ) @@ -159,6 +164,7 @@ class SettlementCoordinator: market_caps: Dict[str, float], agent_portfolio: Dict[str, Any], analyst_results: List[Dict[str, Any]], # pylint: disable=W0613 + pm_decisions: Optional[Dict[str, Dict]] = None, ) -> Dict[str, Any]: """ Run complete daily settlement @@ -171,6 +177,7 @@ class SettlementCoordinator: market_caps: Market caps for each ticker agent_portfolio: Current agent portfolio state analyst_results: Analyst analysis results + pm_decisions: PM's trading decisions Returns: Settlement results including all portfolio values and evaluations @@ -189,13 +196,16 @@ class SettlementCoordinator: baseline_values = self.baseline_calculator.get_all_baseline_values( tickers=tickers, - prices=close_prices, + open_prices=open_prices if open_prices else close_prices, + close_prices=close_prices, market_caps=market_caps, momentum_scores=momentum_scores, date=date, rebalance_momentum=rebalance_momentum, ) + logger.info(f"Baseline values calculated: {baseline_values}") + agent_value = self.storage.calculate_portfolio_value( agent_portfolio, close_prices, @@ -207,10 +217,21 @@ class SettlementCoordinator: date, ) + pm_evaluations = {} + if pm_decisions: + pm_evaluations = self.analyst_tracker.evaluate_pm_decisions( + pm_decisions, + open_prices, + close_prices, + date, + ) + + all_evaluations = {**analyst_evaluations, **pm_evaluations} + leaderboard = self.storage.load_file("leaderboard") or [] updated_leaderboard = update_leaderboard_with_evaluations( leaderboard, - analyst_evaluations, + all_evaluations, ) self.storage.save_file("leaderboard", updated_leaderboard) @@ -301,11 +322,13 @@ class SettlementCoordinator: equal_weight = self.baseline_calculator.calculate_equal_weight_value( tickers, current_prices, + current_prices, ) market_cap = ( self.baseline_calculator.calculate_market_cap_weighted_value( tickers, current_prices, + current_prices, market_caps, ) ) @@ -325,6 +348,7 @@ class SettlementCoordinator: momentum = self.baseline_calculator.calculate_momentum_value( tickers, current_prices, + current_prices, momentum_scores, date=last_date, rebalance=False, diff --git a/evotraders/frontend/src/App.jsx b/evotraders/frontend/src/App.jsx index bf2f071..234e6cf 100644 --- a/evotraders/frontend/src/App.jsx +++ b/evotraders/frontend/src/App.jsx @@ -332,10 +332,12 @@ export default function LiveTradingApp() { })); } - if (state.holdings) setHoldings(state.holdings); - if (state.trades) setTrades(state.trades); - if (state.stats) setStats(state.stats); - if (state.leaderboard) setLeaderboard(state.leaderboard); + if (state.dashboard) { + if (state.dashboard.holdings) setHoldings(state.dashboard.holdings); + if (state.dashboard.trades) setTrades(state.dashboard.trades); + if (state.dashboard.stats) setStats(state.dashboard.stats); + if (state.dashboard.leaderboard) setLeaderboard(state.dashboard.leaderboard); + } if (state.realtime_prices) updateTickersFromPrices(state.realtime_prices); // Load and process historical feed data @@ -1004,6 +1006,7 @@ export default function LiveTradingApp() { stats={stats} baseline_vw={portfolioData.baseline_vw} equity={portfolioData.equity} + leaderboard={leaderboard} /> diff --git a/evotraders/frontend/src/components/AgentCard.jsx b/evotraders/frontend/src/components/AgentCard.jsx index cbe3c69..dfc6aff 100644 --- a/evotraders/frontend/src/components/AgentCard.jsx +++ b/evotraders/frontend/src/components/AgentCard.jsx @@ -398,13 +398,13 @@ export default function AgentCard({ agent, onClose, isClosing }) { let resultColor = '#555555'; let resultFontSize = 18; - if (isUnknown) { + if (isNeutral) { + resultDisplay = '-'; + resultColor = '#555555'; // Gray for neutral + } else if (isUnknown) { resultDisplay = '?'; resultColor = '#FFA726'; // Orange for unknown resultFontSize = 14; // Smaller font for text - } else if (isNeutral) { - resultDisplay = '-'; - resultColor = '#555555'; // Gray for neutral } else { resultDisplay = isCorrect ? '✓' : '✗'; resultColor = isCorrect ? '#00C853' : '#FF1744'; // Green for correct, Red for wrong diff --git a/evotraders/frontend/src/components/AgentFeed.jsx b/evotraders/frontend/src/components/AgentFeed.jsx index c2aed02..9db90f4 100644 --- a/evotraders/frontend/src/components/AgentFeed.jsx +++ b/evotraders/frontend/src/components/AgentFeed.jsx @@ -252,7 +252,7 @@ AgentFeed.displayName = 'AgentFeed'; export default AgentFeed; function SystemDivider({ message, itemId }) { - const content = String(message.content || '').substring(0, 100); + const content = String(message.content || ''); return (
diff --git a/evotraders/frontend/src/components/StatisticsView.jsx b/evotraders/frontend/src/components/StatisticsView.jsx index cbe6dd0..588e615 100644 --- a/evotraders/frontend/src/components/StatisticsView.jsx +++ b/evotraders/frontend/src/components/StatisticsView.jsx @@ -8,7 +8,7 @@ import { formatNumber, formatDateTime } from '../utils/formatters'; * Left: Performance Overview (35%) | Right: Holdings + Trades (65%) * No scrolling - content fits within viewport with pagination */ -export default function StatisticsView({ trades, holdings, stats, baseline_vw, equity }) { +export default function StatisticsView({ trades, holdings, stats, baseline_vw, equity, leaderboard }) { const [holdingsPage, setHoldingsPage] = useState(1); const [tradesPage, setTradesPage] = useState(1); const holdingsPerPage = 5; @@ -62,6 +62,49 @@ export default function StatisticsView({ trades, holdings, stats, baseline_vw, e const excessReturnData = calculateExcessReturn(); + // Calculate Portfolio Manager's win rate (similar logic to AgentCard) + const calculatePortfolioManagerWinRate = () => { + if (!leaderboard || !Array.isArray(leaderboard)) { + return null; + } + + // Find portfolio_manager in leaderboard + const pmData = leaderboard.find(agent => agent.agentId === 'portfolio_manager'); + + if (!pmData) { + return null; + } + + // Extract bull and bear data + const bullTotal = pmData.bull?.n || 0; + const bullWins = pmData.bull?.win || 0; + const bullUnknown = pmData.bull?.unknown || 0; + const bearTotal = pmData.bear?.n || 0; + const bearWins = pmData.bear?.win || 0; + const bearUnknown = pmData.bear?.unknown || 0; + + // Calculate evaluated counts (exclude unknown) + const evaluatedBull = Math.max(bullTotal - bullUnknown, 0); + const evaluatedBear = Math.max(bearTotal - bearUnknown, 0); + const evaluatedTotal = evaluatedBull + evaluatedBear; + + // Calculate win rate + const totalWins = bullWins + bearWins; + const winRate = evaluatedTotal > 0 ? (totalWins / evaluatedTotal) : null; + + return { + winRate, + totalWins, + evaluatedTotal, + bullWins, + bearWins, + evaluatedBull, + evaluatedBear + }; + }; + + const pmWinRateData = calculatePortfolioManagerWinRate(); + // Reset to page 1 when data changes useEffect(() => { setHoldingsPage(1); @@ -195,11 +238,23 @@ export default function StatisticsView({ trades, holdings, stats, baseline_vw, e
- {Math.round(stats.winRate * 100)}% + {pmWinRateData?.winRate != null + ? `${(pmWinRateData.winRate * 100).toFixed(1)}%` + : 'N/A'}
+ {pmWinRateData && ( +
+ {pmWinRateData.totalWins}Win / {pmWinRateData.evaluatedTotal}Eval +
+ )}
{/* 3. Absolute Return */} @@ -303,22 +358,30 @@ export default function StatisticsView({ trades, holdings, stats, baseline_vw, e gap: 8, maxHeight: 120 }}> - {Object.entries(stats.tickerWeights).map(([ticker, weight]) => ( -
- {ticker} - {(weight * 100).toFixed(1)}% -
- ))} + {Object.entries(stats.tickerWeights).map(([ticker, weight]) => { + const weightValue = Number(weight); + const isNegative = weightValue < 0; + const displayWeight = (weightValue * 100).toFixed(1); + + return ( +
+ {ticker} + + {displayWeight}% + +
+ ); + })} )} @@ -401,20 +464,28 @@ export default function StatisticsView({ trades, holdings, stats, baseline_vw, e - {currentHoldings.map(h => ( - - -
- {h.ticker !== 'CASH' && } - {h.ticker} -
- - {h.ticker === 'CASH' ? '-' : h.quantity} - {h.ticker === 'CASH' ? '-' : `$${Number(h.currentPrice).toFixed(2)}`} - ${formatNumber(h.marketValue)} - {(Number(h.weight) * 100).toFixed(2)}% - - ))} + {currentHoldings.map(h => { + // For short positions, quantity should be negative and weight should also be negative + const isShort = h.ticker !== 'CASH' && Number(h.quantity) < 0; + const displayWeight = isShort ? -Math.abs(Number(h.weight)) : Number(h.weight); + + return ( + + +
+ {h.ticker !== 'CASH' && } + {h.ticker} +
+ + {h.ticker === 'CASH' ? '-' : h.quantity} + {h.ticker === 'CASH' ? '-' : `$${Number(h.currentPrice).toFixed(2)}`} + ${formatNumber(h.marketValue)} + + {(displayWeight * 100).toFixed(2)}% + + + ); + })} diff --git a/evotraders/pyproject.toml b/evotraders/pyproject.toml index bc48298..468218c 100644 --- a/evotraders/pyproject.toml +++ b/evotraders/pyproject.toml @@ -27,7 +27,7 @@ classifiers = [ dependencies = [ "agentscope>=1.0.8", - "reme-ai>=0.2.0.3", + "reme-ai>=0.2.0.4", "asyncio>=3.4.3", "rich>=13.6.0", "websockets>=12.0",