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

|

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

|

|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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,6 +511,7 @@ class Gateway:
|
|||||||
f"Starting backtest - {len(dates)} trading days",
|
f"Starting backtest - {len(dates)} trading days",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
for i, date in enumerate(dates):
|
for i, date in enumerate(dates):
|
||||||
self._dashboard.update(days_completed=i)
|
self._dashboard.update(days_completed=i)
|
||||||
await self.on_strategy_trigger(date=date)
|
await self.on_strategy_trigger(date=date)
|
||||||
@@ -522,9 +530,28 @@ class Gateway:
|
|||||||
)
|
)
|
||||||
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)
|
||||||
if dates:
|
if dates:
|
||||||
|
|||||||
@@ -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,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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,7 +367,12 @@ 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)."""
|
"""
|
||||||
|
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()
|
api_key = get_api_key()
|
||||||
headers = {"X-API-KEY": api_key}
|
headers = {"X-API-KEY": api_key}
|
||||||
|
|
||||||
@@ -378,19 +384,35 @@ def search_line_items(
|
|||||||
"period": period,
|
"period": period,
|
||||||
"limit": limit,
|
"limit": limit,
|
||||||
}
|
}
|
||||||
response = _make_api_request(url, headers, method="POST", json_data=body)
|
response = _make_api_request(
|
||||||
if response.status_code != 200:
|
url,
|
||||||
raise ValueError(
|
headers,
|
||||||
f"Error fetching data: {ticker} - {response.status_code} - {response.text}",
|
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()
|
data = response.json()
|
||||||
response_model = LineItemResponse(**data)
|
response_model = LineItemResponse(**data)
|
||||||
search_results = response_model.search_results
|
search_results = response_model.search_results
|
||||||
|
|
||||||
if not search_results:
|
if not search_results:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
return search_results[:limit]
|
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(
|
||||||
ticker: str,
|
ticker: str,
|
||||||
|
|||||||
@@ -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]],
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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',
|
||||||
}}>
|
}}>
|
||||||
|
|||||||
@@ -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,7 +358,12 @@ 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]) => {
|
||||||
|
const weightValue = Number(weight);
|
||||||
|
const isNegative = weightValue < 0;
|
||||||
|
const displayWeight = (weightValue * 100).toFixed(1);
|
||||||
|
|
||||||
|
return (
|
||||||
<div key={ticker} style={{
|
<div key={ticker} style={{
|
||||||
padding: '6px 10px',
|
padding: '6px 10px',
|
||||||
background: '#fafafa',
|
background: '#fafafa',
|
||||||
@@ -316,9 +376,12 @@ export default function StatisticsView({ trades, holdings, stats, baseline_vw, e
|
|||||||
fontFamily: '"Courier New", monospace'
|
fontFamily: '"Courier New", monospace'
|
||||||
}}>
|
}}>
|
||||||
<span style={{ color: '#000000' }}>{ticker}</span>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -401,7 +464,12 @@ export default function StatisticsView({ trades, holdings, stats, baseline_vw, e
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<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}>
|
<tr key={h.ticker}>
|
||||||
<td>
|
<td>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
<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' ? '-' : h.quantity}</td>
|
||||||
<td>{h.ticker === 'CASH' ? '-' : `$${Number(h.currentPrice).toFixed(2)}`}</td>
|
<td>{h.ticker === 'CASH' ? '-' : `$${Number(h.currentPrice).toFixed(2)}`}</td>
|
||||||
<td style={{ fontWeight: 700 }}>${formatNumber(h.marketValue)}</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>
|
</tr>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user