Fix EvoTraders homepage url; fix baseline ,dashboard property, search line items and memory (#80)

This commit is contained in:
Wu Yue
2025-12-15 10:47:39 +08:00
committed by GitHub
parent 6036268aff
commit b3ef20055d
16 changed files with 536 additions and 119 deletions

View File

@@ -5,7 +5,7 @@
<h2 align="center">EvoTraders: A Self-Evolving Multi-Agent Trading System</h2> <h2 align="center">EvoTraders: A Self-Evolving Multi-Agent Trading System</h2>
<p align="center"> <p align="center">
📌 <a href="https://trading.evoagents.com">Visit us at EvoTraders website !</a> 📌 <a href="https://trading.evoagents.cn">Visit us at EvoTraders website !</a>
</p> </p>
![System Demo](./docs/assets/evotraders_demo.gif) ![System Demo](./docs/assets/evotraders_demo.gif)

View File

@@ -6,7 +6,7 @@
<p align="center"> <p align="center">
📌 <a href="https://trading.evoagents.com">Visit us at EvoTraders website !</a> 📌 <a href="https://trading.evoagents.cn">Visit us at EvoTraders website !</a>
</p> </p>
![系统演示](./docs/assets/evotraders_demo.gif) ![系统演示](./docs/assets/evotraders_demo.gif)

View File

@@ -112,7 +112,7 @@ class AnalystAgent(ReActAgent):
""" """
ticker = None ticker = None
if x and hasattr(x, "metadata") and x.metadata: if x and hasattr(x, "metadata") and x.metadata:
ticker = x.metadata.get("ticker") ticker = x.metadata.get("tickers")
if ticker: if ticker:
progress.update_status( progress.update_status(

View File

@@ -199,6 +199,7 @@ class TradingPipeline:
market_caps=market_caps, market_caps=market_caps,
agent_portfolio=execution_result.get("portfolio", {}), agent_portfolio=execution_result.get("portfolio", {}),
analyst_results=analyst_results, analyst_results=analyst_results,
pm_decisions=decisions,
) )
) )

View File

@@ -299,6 +299,37 @@ class StateSync:
"momentum_return" "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(summary_data, persist=True)
await self.emit( await self.emit(
@@ -350,6 +381,17 @@ class StateSync:
persist=False, 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 ========== # ========== System Events ==========
async def on_system_message(self, content: str): async def on_system_message(self, content: str):

View File

@@ -202,9 +202,11 @@ class Gateway:
return return
dates = data.get("dates", []) dates = data.get("dates", [])
if dates and self._backtest_task is None: if dates and self._backtest_task is None:
self._backtest_task = asyncio.create_task( task = asyncio.create_task(
self._run_backtest_dates(dates), 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]): async def broadcast(self, message: Dict[str, Any]):
"""Broadcast message to all connected clients""" """Broadcast message to all connected clients"""
@@ -414,6 +416,11 @@ class Gateway:
holdings = self.storage.load_file("holdings") or [] holdings = self.storage.load_file("holdings") or []
trades = self.storage.load_file("trades") 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( self._dashboard.update(
date=date, date=date,
status="Running", status="Running",
@@ -504,26 +511,46 @@ class Gateway:
f"Starting backtest - {len(dates)} trading days", f"Starting backtest - {len(dates)} trading days",
) )
for i, date in enumerate(dates): try:
self._dashboard.update(days_completed=i) for i, date in enumerate(dates):
await self.on_strategy_trigger(date=date) self._dashboard.update(days_completed=i)
await asyncio.sleep(0.1) await self.on_strategy_trigger(date=date)
await asyncio.sleep(0.1)
await self.state_sync.on_system_message( await self.state_sync.on_system_message(
f"Backtest complete - {len(dates)} days", f"Backtest complete - {len(dates)} days",
) )
# Update dashboard with final state # Update dashboard with final state
summary = self.storage.load_file("summary") or {} summary = self.storage.load_file("summary") or {}
self._dashboard.update( self._dashboard.update(
status="Complete", status="Complete",
portfolio=summary, portfolio=summary,
days_completed=len(dates), days_completed=len(dates),
) )
self._dashboard.stop() self._dashboard.stop()
self._dashboard.print_final_summary() 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]): def set_backtest_dates(self, dates: List[str]):
self.state_sync.set_backtest_dates(dates) self.state_sync.set_backtest_dates(dates)

View File

@@ -19,8 +19,12 @@ def test_baseline_equal_weight():
tickers = ["AAPL", "MSFT", "GOOGL"] tickers = ["AAPL", "MSFT", "GOOGL"]
prices = {"AAPL": 150.0, "MSFT": 300.0, "GOOGL": 120.0} prices = {"AAPL": 150.0, "MSFT": 300.0, "GOOGL": 120.0}
openprices = {"AAPL": 160.0, "MSFT": 310.0, "GOOGL": 110.0}
value = calculator.calculate_equal_weight_value(tickers, prices) value = calculator.calculate_equal_weight_value(
tickers,
openprices,
prices,
)
assert value > 0 assert value > 0
assert calculator.equal_weight_initialized is True assert calculator.equal_weight_initialized is True
@@ -32,10 +36,12 @@ def test_baseline_market_cap_weighted():
tickers = ["AAPL", "MSFT", "GOOGL"] tickers = ["AAPL", "MSFT", "GOOGL"]
prices = {"AAPL": 150.0, "MSFT": 300.0, "GOOGL": 120.0} 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} market_caps = {"AAPL": 3e12, "MSFT": 2e12, "GOOGL": 1.5e12}
value = calculator.calculate_market_cap_weighted_value( value = calculator.calculate_market_cap_weighted_value(
tickers, tickers,
openprices,
prices, prices,
market_caps, market_caps,
) )

View File

@@ -34,6 +34,7 @@ from backend.data.schema import (
Price, Price,
PriceResponse, PriceResponse,
) )
from backend.utils.settlement import logger
# Global cache instance # Global cache instance
_cache = get_cache() _cache = get_cache()
@@ -366,30 +367,51 @@ def search_line_items(
period: str = "ttm", period: str = "ttm",
limit: int = 10, limit: int = 10,
) -> list[LineItem]: ) -> list[LineItem]:
"""Fetch line items from Financial Datasets API (only supported source).""" """
api_key = get_api_key() Fetch line items from Financial Datasets API (only supported source).
headers = {"X-API-KEY": api_key}
url = "https://api.financialdatasets.ai/financials/search/line-items" Returns empty list on API errors to allow graceful degradation.
body = { """
"tickers": [ticker], try:
"line_items": line_items, api_key = get_api_key()
"end_date": end_date, headers = {"X-API-KEY": api_key}
"period": period,
"limit": limit, url = "https://api.financialdatasets.ai/financials/search/line-items"
} body = {
response = _make_api_request(url, headers, method="POST", json_data=body) "tickers": [ticker],
if response.status_code != 200: "line_items": line_items,
raise ValueError( "end_date": end_date,
f"Error fetching data: {ticker} - {response.status_code} - {response.text}", "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( def _fetch_finnhub_insider_trades(

View File

@@ -5,7 +5,7 @@ Tracks analyst predictions and calculates win rates for leaderboard
""" """
import logging import logging
from typing import Any, Dict, List, Optional from typing import Any, Dict, List, Optional, Tuple
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -214,6 +214,178 @@ class AnalystPerformanceTracker:
"""Clear predictions after evaluation""" """Clear predictions after evaluation"""
self.daily_predictions = {} 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( def update_leaderboard_with_evaluations(
leaderboard: List[Dict[str, Any]], leaderboard: List[Dict[str, Any]],

View File

@@ -43,27 +43,39 @@ class BaselineCalculator:
def calculate_equal_weight_value( def calculate_equal_weight_value(
self, self,
tickers: List[str], tickers: List[str],
prices: Dict[str, float], open_prices: Dict[str, float],
close_prices: Dict[str, float],
) -> float: ) -> float:
""" """
Calculate equal-weight portfolio value Calculate equal-weight portfolio value
On first call, initialize positions with equal allocation On first call, initialize positions with equal allocation using
Subsequently, mark-to-market existing positions 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: if not self.equal_weight_initialized:
allocation_per_ticker = self.initial_capital / len(tickers) allocation_per_ticker = self.initial_capital / len(tickers)
self.equal_weight_portfolio["cash"] = 0.0 self.equal_weight_portfolio["cash"] = 0.0
for ticker in tickers: for ticker in tickers:
price = prices.get(ticker, 0) price = open_prices.get(ticker, 0) # Use OPEN price for buying
if price > 0: if price > 0:
shares = allocation_per_ticker / price shares = allocation_per_ticker / price
self.equal_weight_portfolio["positions"][ticker] = shares 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 self.equal_weight_initialized = True
total_value = self.equal_weight_portfolio["cash"] total_value = self.equal_weight_portfolio["cash"]
for ticker, shares in self.equal_weight_portfolio["positions"].items(): positions: Dict[str, float] = self.equal_weight_portfolio["positions"]
price = prices.get(ticker, 0) for ticker, shares in positions.items():
price = close_prices.get(ticker, 0)
total_value += shares * price total_value += shares * price
return total_value return total_value
@@ -71,35 +83,53 @@ class BaselineCalculator:
def calculate_market_cap_weighted_value( def calculate_market_cap_weighted_value(
self, self,
tickers: List[str], tickers: List[str],
prices: Dict[str, float], open_prices: Dict[str, float],
close_prices: Dict[str, float],
market_caps: Dict[str, float], market_caps: Dict[str, float],
) -> float: ) -> float:
""" """
Calculate market-cap-weighted portfolio value Calculate market-cap-weighted portfolio value
On first call, initialize positions weighted by market cap On first call, initialize positions weighted by market cap using
Subsequently, mark-to-market existing positions 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: if not self.market_cap_initialized:
total_market_cap = sum(market_caps.get(t, 0) for t in tickers) total_market_cap = sum(market_caps.get(t, 0) for t in tickers)
if total_market_cap <= 0: if total_market_cap <= 0:
logger.warning("No market cap data, using equal weight") 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 self.market_cap_portfolio["cash"] = 0.0
for ticker in tickers: for ticker in tickers:
market_cap = market_caps.get(ticker, 0) 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: if market_cap > 0 and price > 0:
weight = market_cap / total_market_cap weight = market_cap / total_market_cap
allocation = self.initial_capital * weight allocation = self.initial_capital * weight
shares = allocation / price shares = allocation / price
self.market_cap_portfolio["positions"][ticker] = shares 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 self.market_cap_initialized = True
total_value = self.market_cap_portfolio["cash"] total_value = self.market_cap_portfolio["cash"]
for ticker, shares in self.market_cap_portfolio["positions"].items(): positions: Dict[str, float] = self.market_cap_portfolio["positions"]
price = prices.get(ticker, 0) for ticker, shares in positions.items():
price = close_prices.get(ticker, 0)
total_value += shares * price total_value += shares * price
return total_value return total_value
@@ -107,7 +137,8 @@ class BaselineCalculator:
def calculate_momentum_value( def calculate_momentum_value(
self, self,
tickers: List[str], tickers: List[str],
prices: Dict[str, float], open_prices: Dict[str, float],
close_prices: Dict[str, float],
momentum_scores: Dict[str, float], momentum_scores: Dict[str, float],
date: str, date: str,
rebalance: bool = False, rebalance: bool = False,
@@ -122,7 +153,8 @@ class BaselineCalculator:
Args: Args:
tickers: List of tickers 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 momentum_scores: Momentum scores for each ticker
date: Current date (YYYY-MM-DD) date: Current date (YYYY-MM-DD)
rebalance: Force rebalance if True rebalance: Force rebalance if True
@@ -145,14 +177,15 @@ class BaselineCalculator:
if should_rebalance: if should_rebalance:
self._rebalance_momentum_portfolio( self._rebalance_momentum_portfolio(
tickers, tickers,
prices, open_prices,
momentum_scores, momentum_scores,
) )
self.momentum_last_rebalance_date = date self.momentum_last_rebalance_date = date
total_value = self.momentum_portfolio["cash"] total_value = self.momentum_portfolio["cash"]
for ticker, shares in self.momentum_portfolio["positions"].items(): positions: Dict[str, float] = self.momentum_portfolio["positions"]
price = prices.get(ticker, 0) for ticker, shares in positions.items():
price = close_prices.get(ticker, 0)
total_value += shares * price total_value += shares * price
return total_value return total_value
@@ -201,7 +234,8 @@ class BaselineCalculator:
def get_all_baseline_values( def get_all_baseline_values(
self, self,
tickers: List[str], tickers: List[str],
prices: Dict[str, float], open_prices: Dict[str, float],
close_prices: Dict[str, float],
market_caps: Dict[str, float], market_caps: Dict[str, float],
momentum_scores: Dict[str, float], momentum_scores: Dict[str, float],
date: str, date: str,
@@ -210,18 +244,33 @@ class BaselineCalculator:
""" """
Get all baseline portfolio values in one call 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: Returns:
Dict with keys: equal_weight, market_cap_weighted, momentum 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( market_cap_value = self.calculate_market_cap_weighted_value(
tickers, tickers,
prices, open_prices,
close_prices,
market_caps, market_caps,
) )
momentum_value = self.calculate_momentum_value( momentum_value = self.calculate_momentum_value(
tickers, tickers,
prices, open_prices,
close_prices,
momentum_scores, momentum_scores,
date, date,
rebalance_momentum, rebalance_momentum,

View File

@@ -70,12 +70,17 @@ class SettlementCoordinator:
if saved_price_history: if saved_price_history:
# Convert saved format back to list of tuples # Convert saved format back to list of tuples
for ticker, history in saved_price_history.items(): for ticker, history in saved_price_history.items():
self.price_history[ticker] = [ converted_history = []
(entry["date"], entry["price"]) for entry in history:
if isinstance(entry, dict) if isinstance(entry, dict):
else tuple(entry) converted_history.append(
for entry in history (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( logger.info(
f"Restored price history for {len(self.price_history)} tickers", f"Restored price history for {len(self.price_history)} tickers",
) )
@@ -159,6 +164,7 @@ class SettlementCoordinator:
market_caps: Dict[str, float], market_caps: Dict[str, float],
agent_portfolio: Dict[str, Any], agent_portfolio: Dict[str, Any],
analyst_results: List[Dict[str, Any]], # pylint: disable=W0613 analyst_results: List[Dict[str, Any]], # pylint: disable=W0613
pm_decisions: Optional[Dict[str, Dict]] = None,
) -> Dict[str, Any]: ) -> Dict[str, Any]:
""" """
Run complete daily settlement Run complete daily settlement
@@ -171,6 +177,7 @@ class SettlementCoordinator:
market_caps: Market caps for each ticker market_caps: Market caps for each ticker
agent_portfolio: Current agent portfolio state agent_portfolio: Current agent portfolio state
analyst_results: Analyst analysis results analyst_results: Analyst analysis results
pm_decisions: PM's trading decisions
Returns: Returns:
Settlement results including all portfolio values and evaluations Settlement results including all portfolio values and evaluations
@@ -189,13 +196,16 @@ class SettlementCoordinator:
baseline_values = self.baseline_calculator.get_all_baseline_values( baseline_values = self.baseline_calculator.get_all_baseline_values(
tickers=tickers, tickers=tickers,
prices=close_prices, open_prices=open_prices if open_prices else close_prices,
close_prices=close_prices,
market_caps=market_caps, market_caps=market_caps,
momentum_scores=momentum_scores, momentum_scores=momentum_scores,
date=date, date=date,
rebalance_momentum=rebalance_momentum, rebalance_momentum=rebalance_momentum,
) )
logger.info(f"Baseline values calculated: {baseline_values}")
agent_value = self.storage.calculate_portfolio_value( agent_value = self.storage.calculate_portfolio_value(
agent_portfolio, agent_portfolio,
close_prices, close_prices,
@@ -207,10 +217,21 @@ class SettlementCoordinator:
date, 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 [] leaderboard = self.storage.load_file("leaderboard") or []
updated_leaderboard = update_leaderboard_with_evaluations( updated_leaderboard = update_leaderboard_with_evaluations(
leaderboard, leaderboard,
analyst_evaluations, all_evaluations,
) )
self.storage.save_file("leaderboard", updated_leaderboard) self.storage.save_file("leaderboard", updated_leaderboard)
@@ -301,11 +322,13 @@ class SettlementCoordinator:
equal_weight = self.baseline_calculator.calculate_equal_weight_value( equal_weight = self.baseline_calculator.calculate_equal_weight_value(
tickers, tickers,
current_prices, current_prices,
current_prices,
) )
market_cap = ( market_cap = (
self.baseline_calculator.calculate_market_cap_weighted_value( self.baseline_calculator.calculate_market_cap_weighted_value(
tickers, tickers,
current_prices, current_prices,
current_prices,
market_caps, market_caps,
) )
) )
@@ -325,6 +348,7 @@ class SettlementCoordinator:
momentum = self.baseline_calculator.calculate_momentum_value( momentum = self.baseline_calculator.calculate_momentum_value(
tickers, tickers,
current_prices, current_prices,
current_prices,
momentum_scores, momentum_scores,
date=last_date, date=last_date,
rebalance=False, rebalance=False,

View File

@@ -332,10 +332,12 @@ export default function LiveTradingApp() {
})); }));
} }
if (state.holdings) setHoldings(state.holdings); if (state.dashboard) {
if (state.trades) setTrades(state.trades); if (state.dashboard.holdings) setHoldings(state.dashboard.holdings);
if (state.stats) setStats(state.stats); if (state.dashboard.trades) setTrades(state.dashboard.trades);
if (state.leaderboard) setLeaderboard(state.leaderboard); if (state.dashboard.stats) setStats(state.dashboard.stats);
if (state.dashboard.leaderboard) setLeaderboard(state.dashboard.leaderboard);
}
if (state.realtime_prices) updateTickersFromPrices(state.realtime_prices); if (state.realtime_prices) updateTickersFromPrices(state.realtime_prices);
// Load and process historical feed data // Load and process historical feed data
@@ -1004,6 +1006,7 @@ export default function LiveTradingApp() {
stats={stats} stats={stats}
baseline_vw={portfolioData.baseline_vw} baseline_vw={portfolioData.baseline_vw}
equity={portfolioData.equity} equity={portfolioData.equity}
leaderboard={leaderboard}
/> />
</div> </div>
</div> </div>

View File

@@ -398,13 +398,13 @@ export default function AgentCard({ agent, onClose, isClosing }) {
let resultColor = '#555555'; let resultColor = '#555555';
let resultFontSize = 18; let resultFontSize = 18;
if (isUnknown) { if (isNeutral) {
resultDisplay = '-';
resultColor = '#555555'; // Gray for neutral
} else if (isUnknown) {
resultDisplay = '?'; resultDisplay = '?';
resultColor = '#FFA726'; // Orange for unknown resultColor = '#FFA726'; // Orange for unknown
resultFontSize = 14; // Smaller font for text resultFontSize = 14; // Smaller font for text
} else if (isNeutral) {
resultDisplay = '-';
resultColor = '#555555'; // Gray for neutral
} else { } else {
resultDisplay = isCorrect ? '' : ''; resultDisplay = isCorrect ? '' : '';
resultColor = isCorrect ? '#00C853' : '#FF1744'; // Green for correct, Red for wrong resultColor = isCorrect ? '#00C853' : '#FF1744'; // Green for correct, Red for wrong

View File

@@ -252,7 +252,7 @@ AgentFeed.displayName = 'AgentFeed';
export default AgentFeed; export default AgentFeed;
function SystemDivider({ message, itemId }) { function SystemDivider({ message, itemId }) {
const content = String(message.content || '').substring(0, 100); const content = String(message.content || '');
return ( return (
<div <div
@@ -268,7 +268,7 @@ function SystemDivider({ message, itemId }) {
<span style={{ <span style={{
fontSize: '11px', fontSize: '11px',
color: '#888', color: '#888',
whiteSpace: 'nowrap', whiteSpace: 'normal',
fontWeight: 500, fontWeight: 500,
letterSpacing: '0.3px', letterSpacing: '0.3px',
}}> }}>

View File

@@ -8,7 +8,7 @@ import { formatNumber, formatDateTime } from '../utils/formatters';
* Left: Performance Overview (35%) | Right: Holdings + Trades (65%) * Left: Performance Overview (35%) | Right: Holdings + Trades (65%)
* No scrolling - content fits within viewport with pagination * 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 [holdingsPage, setHoldingsPage] = useState(1);
const [tradesPage, setTradesPage] = useState(1); const [tradesPage, setTradesPage] = useState(1);
const holdingsPerPage = 5; const holdingsPerPage = 5;
@@ -62,6 +62,49 @@ export default function StatisticsView({ trades, holdings, stats, baseline_vw, e
const excessReturnData = calculateExcessReturn(); 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 // Reset to page 1 when data changes
useEffect(() => { useEffect(() => {
setHoldingsPage(1); setHoldingsPage(1);
@@ -195,11 +238,23 @@ export default function StatisticsView({ trades, holdings, stats, baseline_vw, e
<div style={{ <div style={{
fontSize: 28, fontSize: 28,
fontWeight: 700, fontWeight: 700,
color: '#000000', color: pmWinRateData?.winRate != null ? '#00C853' : '#000000',
fontFamily: '"Courier New", monospace' fontFamily: '"Courier New", monospace'
}}> }}>
{Math.round(stats.winRate * 100)}% {pmWinRateData?.winRate != null
? `${(pmWinRateData.winRate * 100).toFixed(1)}%`
: 'N/A'}
</div> </div>
{pmWinRateData && (
<div style={{
fontSize: 7,
color: '#999999',
marginTop: 4,
fontFamily: '"Courier New", monospace'
}}>
{pmWinRateData.totalWins}Win / {pmWinRateData.evaluatedTotal}Eval
</div>
)}
</div> </div>
{/* 3. Absolute Return */} {/* 3. Absolute Return */}
@@ -303,22 +358,30 @@ export default function StatisticsView({ trades, holdings, stats, baseline_vw, e
gap: 8, gap: 8,
maxHeight: 120 maxHeight: 120
}}> }}>
{Object.entries(stats.tickerWeights).map(([ticker, weight]) => ( {Object.entries(stats.tickerWeights).map(([ticker, weight]) => {
<div key={ticker} style={{ const weightValue = Number(weight);
padding: '6px 10px', const isNegative = weightValue < 0;
background: '#fafafa', const displayWeight = (weightValue * 100).toFixed(1);
border: '1px solid #e0e0e0',
fontSize: 10, return (
fontWeight: 700, <div key={ticker} style={{
display: 'flex', padding: '6px 10px',
justifyContent: 'space-between', background: '#fafafa',
alignItems: 'center', border: '1px solid #e0e0e0',
fontFamily: '"Courier New", monospace' fontSize: 10,
}}> fontWeight: 700,
<span style={{ color: '#000000' }}>{ticker}</span> display: 'flex',
<span style={{ color: '#00C853' }}>{(weight * 100).toFixed(1)}%</span> justifyContent: 'space-between',
</div> alignItems: 'center',
))} fontFamily: '"Courier New", monospace'
}}>
<span style={{ color: '#000000' }}>{ticker}</span>
<span style={{ color: isNegative ? '#FF1744' : '#00C853' }}>
{displayWeight}%
</span>
</div>
);
})}
</div> </div>
</div> </div>
)} )}
@@ -401,20 +464,28 @@ export default function StatisticsView({ trades, holdings, stats, baseline_vw, e
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{currentHoldings.map(h => ( {currentHoldings.map(h => {
<tr key={h.ticker}> // For short positions, quantity should be negative and weight should also be negative
<td> const isShort = h.ticker !== 'CASH' && Number(h.quantity) < 0;
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}> const displayWeight = isShort ? -Math.abs(Number(h.weight)) : Number(h.weight);
{h.ticker !== 'CASH' && <StockLogo ticker={h.ticker} size={18} />}
<span style={{ fontWeight: 700, color: '#000000' }}>{h.ticker}</span> return (
</div> <tr key={h.ticker}>
</td> <td>
<td>{h.ticker === 'CASH' ? '-' : h.quantity}</td> <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<td>{h.ticker === 'CASH' ? '-' : `$${Number(h.currentPrice).toFixed(2)}`}</td> {h.ticker !== 'CASH' && <StockLogo ticker={h.ticker} size={18} />}
<td style={{ fontWeight: 700 }}>${formatNumber(h.marketValue)}</td> <span style={{ fontWeight: 700, color: '#000000' }}>{h.ticker}</span>
<td>{(Number(h.weight) * 100).toFixed(2)}%</td> </div>
</tr> </td>
))} <td>{h.ticker === 'CASH' ? '-' : h.quantity}</td>
<td>{h.ticker === 'CASH' ? '-' : `$${Number(h.currentPrice).toFixed(2)}`}</td>
<td style={{ fontWeight: 700 }}>${formatNumber(h.marketValue)}</td>
<td style={{ color: isShort ? '#FF1744' : '#000000' }}>
{(displayWeight * 100).toFixed(2)}%
</td>
</tr>
);
})}
</tbody> </tbody>
</table> </table>
</div> </div>

View File

@@ -27,7 +27,7 @@ classifiers = [
dependencies = [ dependencies = [
"agentscope>=1.0.8", "agentscope>=1.0.8",
"reme-ai>=0.2.0.3", "reme-ai>=0.2.0.4",
"asyncio>=3.4.3", "asyncio>=3.4.3",
"rich>=13.6.0", "rich>=13.6.0",
"websockets>=12.0", "websockets>=12.0",