Fix EvoTraders homepage url; fix baseline ,dashboard property, search line items and memory (#80)
This commit is contained in:
@@ -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>
|
||||
|
||||

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

|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -199,6 +199,7 @@ class TradingPipeline:
|
||||
market_caps=market_caps,
|
||||
agent_portfolio=execution_result.get("portfolio", {}),
|
||||
analyst_results=analyst_results,
|
||||
pm_decisions=decisions,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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,6 +511,7 @@ class Gateway:
|
||||
f"Starting backtest - {len(dates)} trading days",
|
||||
)
|
||||
|
||||
try:
|
||||
for i, date in enumerate(dates):
|
||||
self._dashboard.update(days_completed=i)
|
||||
await self.on_strategy_trigger(date=date)
|
||||
@@ -522,9 +530,28 @@ class Gateway:
|
||||
)
|
||||
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
|
||||
|
||||
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)
|
||||
if dates:
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -34,6 +34,7 @@ from backend.data.schema import (
|
||||
Price,
|
||||
PriceResponse,
|
||||
)
|
||||
from backend.utils.settlement import logger
|
||||
|
||||
# Global cache instance
|
||||
_cache = get_cache()
|
||||
@@ -366,7 +367,12 @@ def search_line_items(
|
||||
period: str = "ttm",
|
||||
limit: int = 10,
|
||||
) -> list[LineItem]:
|
||||
"""Fetch line items from Financial Datasets API (only supported source)."""
|
||||
"""
|
||||
Fetch line items from Financial Datasets API (only supported source).
|
||||
|
||||
Returns empty list on API errors to allow graceful degradation.
|
||||
"""
|
||||
try:
|
||||
api_key = get_api_key()
|
||||
headers = {"X-API-KEY": api_key}
|
||||
|
||||
@@ -378,19 +384,35 @@ def search_line_items(
|
||||
"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}",
|
||||
response = _make_api_request(
|
||||
url,
|
||||
headers,
|
||||
method="POST",
|
||||
json_data=body,
|
||||
)
|
||||
|
||||
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(
|
||||
ticker: str,
|
||||
|
||||
@@ -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]],
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
}}>
|
||||
|
||||
@@ -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,7 +358,12 @@ export default function StatisticsView({ trades, holdings, stats, baseline_vw, e
|
||||
gap: 8,
|
||||
maxHeight: 120
|
||||
}}>
|
||||
{Object.entries(stats.tickerWeights).map(([ticker, weight]) => (
|
||||
{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',
|
||||
@@ -316,9 +376,12 @@ export default function StatisticsView({ trades, holdings, stats, baseline_vw, e
|
||||
fontFamily: '"Courier New", monospace'
|
||||
}}>
|
||||
<span style={{ color: '#000000' }}>{ticker}</span>
|
||||
<span style={{ color: '#00C853' }}>{(weight * 100).toFixed(1)}%</span>
|
||||
<span style={{ color: isNegative ? '#FF1744' : '#00C853' }}>
|
||||
{displayWeight}%
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -401,7 +464,12 @@ export default function StatisticsView({ trades, holdings, stats, baseline_vw, e
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{currentHoldings.map(h => (
|
||||
{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 }}>
|
||||
@@ -412,9 +480,12 @@ export default function StatisticsView({ trades, holdings, stats, baseline_vw, e
|
||||
<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>
|
||||
<td style={{ color: isShort ? '#FF1744' : '#000000' }}>
|
||||
{(displayWeight * 100).toFixed(2)}%
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user