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>
<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>
![System Demo](./docs/assets/evotraders_demo.gif)

View File

@@ -6,7 +6,7 @@
<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>
![系统演示](./docs/assets/evotraders_demo.gif)

View File

@@ -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(

View File

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

View File

@@ -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):

View File

@@ -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)

View File

@@ -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,
)

View File

@@ -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(

View File

@@ -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]],

View File

@@ -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,

View File

@@ -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,

View File

@@ -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}
/>
</div>
</div>

View File

@@ -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

View File

@@ -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 (
<div
@@ -268,7 +268,7 @@ function SystemDivider({ message, itemId }) {
<span style={{
fontSize: '11px',
color: '#888',
whiteSpace: 'nowrap',
whiteSpace: 'normal',
fontWeight: 500,
letterSpacing: '0.3px',
}}>

View File

@@ -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
<div style={{
fontSize: 28,
fontWeight: 700,
color: '#000000',
color: pmWinRateData?.winRate != null ? '#00C853' : '#000000',
fontFamily: '"Courier New", monospace'
}}>
{Math.round(stats.winRate * 100)}%
{pmWinRateData?.winRate != null
? `${(pmWinRateData.winRate * 100).toFixed(1)}%`
: 'N/A'}
</div>
{pmWinRateData && (
<div style={{
fontSize: 7,
color: '#999999',
marginTop: 4,
fontFamily: '"Courier New", monospace'
}}>
{pmWinRateData.totalWins}Win / {pmWinRateData.evaluatedTotal}Eval
</div>
)}
</div>
{/* 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]) => (
<div key={ticker} style={{
padding: '6px 10px',
background: '#fafafa',
border: '1px solid #e0e0e0',
fontSize: 10,
fontWeight: 700,
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
fontFamily: '"Courier New", monospace'
}}>
<span style={{ color: '#000000' }}>{ticker}</span>
<span style={{ color: '#00C853' }}>{(weight * 100).toFixed(1)}%</span>
</div>
))}
{Object.entries(stats.tickerWeights).map(([ticker, weight]) => {
const weightValue = Number(weight);
const isNegative = weightValue < 0;
const displayWeight = (weightValue * 100).toFixed(1);
return (
<div key={ticker} style={{
padding: '6px 10px',
background: '#fafafa',
border: '1px solid #e0e0e0',
fontSize: 10,
fontWeight: 700,
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
fontFamily: '"Courier New", monospace'
}}>
<span style={{ color: '#000000' }}>{ticker}</span>
<span style={{ color: isNegative ? '#FF1744' : '#00C853' }}>
{displayWeight}%
</span>
</div>
);
})}
</div>
</div>
)}
@@ -401,20 +464,28 @@ export default function StatisticsView({ trades, holdings, stats, baseline_vw, e
</tr>
</thead>
<tbody>
{currentHoldings.map(h => (
<tr key={h.ticker}>
<td>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
{h.ticker !== 'CASH' && <StockLogo ticker={h.ticker} size={18} />}
<span style={{ fontWeight: 700, color: '#000000' }}>{h.ticker}</span>
</div>
</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>{(Number(h.weight) * 100).toFixed(2)}%</td>
</tr>
))}
{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 (
<tr key={h.ticker}>
<td>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
{h.ticker !== 'CASH' && <StockLogo ticker={h.ticker} size={18} />}
<span style={{ fontWeight: 700, color: '#000000' }}>{h.ticker}</span>
</div>
</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>
</table>
</div>

View File

@@ -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",