feat(agent): complete EvoAgent integration for all 6 agent roles
Migrate all agent roles from Legacy to EvoAgent architecture: - fundamentals_analyst, technical_analyst, sentiment_analyst, valuation_analyst - risk_manager, portfolio_manager Key changes: - EvoAgent now supports Portfolio Manager compatibility methods (_make_decision, get_decisions, get_portfolio_state, load_portfolio_state, update_portfolio) - Add UnifiedAgentFactory for centralized agent creation - ToolGuard with batch approval API and WebSocket broadcast - Legacy agents marked deprecated (AnalystAgent, RiskAgent, PMAgent) - Remove backend/agents/compat.py migration shim - Add run_id alongside workspace_id for semantic clarity - Complete integration test coverage (13 tests) - All smoke tests passing for 6 agent roles Constraint: Must maintain backward compatibility with existing run configs Constraint: Memory support must work with EvoAgent (no fallback to Legacy) Rejected: Separate PM implementation for EvoAgent | unified approach cleaner Confidence: high Scope-risk: broad Directive: EVO_AGENT_IDS env var still respected but defaults to all roles Not-tested: Kubernetes sandbox mode for skill execution
This commit is contained in:
@@ -311,7 +311,7 @@ class TestRiskAgent:
|
||||
|
||||
|
||||
class TestStorageService:
|
||||
def test_storage_service_defaults_to_live_config(self):
|
||||
def test_storage_service_defaults_to_runtime_config(self):
|
||||
from backend.services.storage import StorageService
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
@@ -320,7 +320,7 @@ class TestStorageService:
|
||||
initial_cash=100000.0,
|
||||
)
|
||||
|
||||
assert storage.config_name == "live"
|
||||
assert storage.config_name == "runtime"
|
||||
|
||||
def test_calculate_portfolio_value_cash_only(self):
|
||||
from backend.services.storage import StorageService
|
||||
@@ -404,7 +404,7 @@ class TestStorageService:
|
||||
assert trades[0]["qty"] == 50
|
||||
assert trades[0]["price"] == 200.0
|
||||
|
||||
def test_generate_summary(self):
|
||||
def test_build_summary_export(self):
|
||||
from backend.services.storage import StorageService
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
@@ -424,13 +424,12 @@ class TestStorageService:
|
||||
}
|
||||
prices = {"AAPL": 500.0}
|
||||
|
||||
storage._generate_summary(state, 100000.0, prices)
|
||||
summary = storage._build_summary_export(state, 100000.0, prices)
|
||||
|
||||
summary = storage.load_file("summary")
|
||||
assert summary["totalAssetValue"] == 100000.0
|
||||
assert summary["totalReturn"] == 0.0
|
||||
|
||||
def test_generate_holdings(self):
|
||||
def test_build_holdings_export(self):
|
||||
from backend.services.storage import StorageService
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
@@ -448,9 +447,8 @@ class TestStorageService:
|
||||
}
|
||||
prices = {"AAPL": 500.0}
|
||||
|
||||
storage._generate_holdings(state, prices)
|
||||
holdings = storage._build_holdings_export(state, prices)
|
||||
|
||||
holdings = storage.load_file("holdings")
|
||||
assert len(holdings) == 2 # AAPL + CASH
|
||||
|
||||
aapl_holding = next(
|
||||
@@ -461,6 +459,150 @@ class TestStorageService:
|
||||
assert aapl_holding["quantity"] == 100
|
||||
assert aapl_holding["currentPrice"] == 500.0
|
||||
|
||||
def test_export_dashboard_compatibility_files_writes_expected_exports(self):
|
||||
from backend.services.storage import StorageService
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
storage = StorageService(
|
||||
dashboard_dir=Path(tmpdir) / "team_dashboard",
|
||||
initial_cash=100000.0,
|
||||
)
|
||||
state = {
|
||||
"portfolio_state": {
|
||||
"cash": 90000.0,
|
||||
"positions": {"AAPL": {"long": 50, "short": 0}},
|
||||
"margin_used": 0.0,
|
||||
},
|
||||
"equity_history": [{"t": 1000, "v": 100000}],
|
||||
"baseline_history": [{"t": 1000, "v": 100000}],
|
||||
"baseline_vw_history": [{"t": 1000, "v": 100000}],
|
||||
"momentum_history": [{"t": 1000, "v": 100000}],
|
||||
"all_trades": [
|
||||
{
|
||||
"id": "t1",
|
||||
"ts": 1000,
|
||||
"trading_date": "2024-01-15",
|
||||
"side": "LONG",
|
||||
"ticker": "AAPL",
|
||||
"qty": 50,
|
||||
"price": 200.0,
|
||||
}
|
||||
],
|
||||
}
|
||||
prices = {"AAPL": 200.0}
|
||||
|
||||
storage.export_dashboard_compatibility_files(
|
||||
state,
|
||||
net_value=100000.0,
|
||||
prices=prices,
|
||||
)
|
||||
|
||||
assert storage.load_export_file("summary")["totalAssetValue"] == 100000.0
|
||||
holdings = storage.load_export_file("holdings")
|
||||
assert any(item["ticker"] == "AAPL" for item in holdings)
|
||||
assert storage.load_export_file("stats")["totalTrades"] == 1
|
||||
assert storage.load_export_file("trades")[0]["ticker"] == "AAPL"
|
||||
|
||||
def test_build_dashboard_snapshot_prefers_persisted_runtime_state_when_memory_view_is_sparse(self):
|
||||
from backend.services.storage import StorageService
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
dashboard_dir = Path(tmpdir) / "team_dashboard"
|
||||
storage = StorageService(
|
||||
dashboard_dir=dashboard_dir,
|
||||
initial_cash=100000.0,
|
||||
)
|
||||
storage.save_server_state(
|
||||
{
|
||||
"portfolio": {
|
||||
"total_value": 123456.0,
|
||||
"cash": 45678.0,
|
||||
"pnl_percent": 23.45,
|
||||
},
|
||||
"holdings": [{"ticker": "AAPL", "quantity": 10}],
|
||||
"stats": {"totalTrades": 3},
|
||||
"trades": [{"ticker": "AAPL"}],
|
||||
"leaderboard": [{"agentId": "technical_analyst"}],
|
||||
}
|
||||
)
|
||||
|
||||
snapshot = storage.build_dashboard_snapshot_from_state({"portfolio": {}})
|
||||
|
||||
assert snapshot["summary"]["totalAssetValue"] == 123456.0
|
||||
assert snapshot["holdings"][0]["ticker"] == "AAPL"
|
||||
assert snapshot["trades"][0]["ticker"] == "AAPL"
|
||||
assert snapshot["leaderboard"][0]["agentId"] == "technical_analyst"
|
||||
|
||||
def test_runtime_leaderboard_prefers_server_state_and_persists_back(self):
|
||||
from backend.services.storage import StorageService
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
dashboard_dir = Path(tmpdir) / "team_dashboard"
|
||||
storage = StorageService(
|
||||
dashboard_dir=dashboard_dir,
|
||||
initial_cash=100000.0,
|
||||
)
|
||||
storage.save_export_file("leaderboard", [{"agentId": "export_only"}])
|
||||
storage.save_server_state({"leaderboard": [{"agentId": "runtime_state"}]})
|
||||
|
||||
leaderboard = storage.load_runtime_leaderboard()
|
||||
assert leaderboard[0]["agentId"] == "runtime_state"
|
||||
|
||||
updated = [{"agentId": "updated_runtime"}]
|
||||
storage.persist_runtime_leaderboard(updated)
|
||||
|
||||
saved_state = storage.read_persisted_server_state()
|
||||
saved_export = storage.load_export_file("leaderboard")
|
||||
assert saved_state["leaderboard"][0]["agentId"] == "updated_runtime"
|
||||
assert saved_export[0]["agentId"] == "updated_runtime"
|
||||
|
||||
def test_compatibility_exports_can_be_disabled_without_breaking_runtime_leaderboard(self):
|
||||
from backend.services.storage import StorageService
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
dashboard_dir = Path(tmpdir) / "team_dashboard"
|
||||
storage = StorageService(
|
||||
dashboard_dir=dashboard_dir,
|
||||
initial_cash=100000.0,
|
||||
enable_compat_exports=False,
|
||||
)
|
||||
|
||||
storage.generate_leaderboard()
|
||||
storage.export_dashboard_compatibility_files(
|
||||
{
|
||||
"portfolio_state": {
|
||||
"cash": 100000.0,
|
||||
"positions": {},
|
||||
"margin_used": 0.0,
|
||||
},
|
||||
"equity_history": [],
|
||||
"baseline_history": [],
|
||||
"baseline_vw_history": [],
|
||||
"momentum_history": [],
|
||||
"all_trades": [],
|
||||
},
|
||||
net_value=100000.0,
|
||||
prices={},
|
||||
)
|
||||
|
||||
assert not dashboard_dir.joinpath("summary.json").exists()
|
||||
assert storage.load_runtime_leaderboard()
|
||||
persisted = storage.read_persisted_server_state()
|
||||
assert persisted["leaderboard"]
|
||||
|
||||
def test_compatibility_exports_default_can_be_disabled_via_env(self, monkeypatch):
|
||||
from backend.services.storage import StorageService
|
||||
|
||||
monkeypatch.setenv("ENABLE_DASHBOARD_COMPAT_EXPORTS", "false")
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
storage = StorageService(
|
||||
dashboard_dir=Path(tmpdir) / "team_dashboard",
|
||||
initial_cash=100000.0,
|
||||
)
|
||||
|
||||
assert storage.enable_compat_exports is False
|
||||
|
||||
|
||||
class TestTradeExecutor:
|
||||
def test_execute_trade_long(self):
|
||||
|
||||
Reference in New Issue
Block a user