Refine runtime data flow and UI layering

This commit is contained in:
2026-03-24 15:00:35 +08:00
parent c5eaf2b5ad
commit 6413edf8c9
17 changed files with 373 additions and 114 deletions

View File

@@ -152,10 +152,11 @@ class Gateway:
)
# Load and display existing portfolio state if available
summary = self.storage.load_file("summary")
dashboard_snapshot = self.storage.build_dashboard_snapshot_from_state(self.state_sync.state)
summary = dashboard_snapshot.get("summary")
if summary:
holdings = self.storage.load_file("holdings") or []
trades = self.storage.load_file("trades") or []
holdings = dashboard_snapshot.get("holdings") or []
trades = dashboard_snapshot.get("trades") or []
current_date = self.state_sync.state.get("current_date")
self._dashboard.update(
date=current_date or "-",

View File

@@ -61,7 +61,7 @@ async def market_status_monitor(gateway: Any) -> None:
status = gateway.market_service.get_market_status()
if status["status"] == "open" and not gateway.storage.is_live_session_active:
gateway.storage.start_live_session()
summary = gateway.storage.load_file("summary") or {}
summary = gateway.storage.build_dashboard_snapshot_from_state(gateway.state_sync.state).get("summary") or {}
gateway._session_start_portfolio_value = summary.get(
"totalAssetValue",
gateway.storage.initial_cash,
@@ -240,14 +240,15 @@ async def run_live_cycle(gateway: Any, date: str, tickers: list[str]) -> None:
async def finalize_cycle(gateway: Any, date: str) -> None:
summary = gateway.storage.load_file("summary") or {}
dashboard_snapshot = gateway.storage.build_dashboard_snapshot_from_state(gateway.state_sync.state)
summary = dashboard_snapshot.get("summary") or {}
if gateway.storage.is_live_session_active:
summary.update(gateway.storage.get_live_returns())
await gateway.state_sync.on_cycle_end(date, portfolio_summary=summary)
holdings = gateway.storage.load_file("holdings") or []
trades = gateway.storage.load_file("trades") or []
leaderboard = gateway.storage.load_file("leaderboard") or []
holdings = dashboard_snapshot.get("holdings") or []
trades = dashboard_snapshot.get("trades") or []
leaderboard = dashboard_snapshot.get("leaderboard") or []
if leaderboard:
await gateway.state_sync.on_leaderboard_update(leaderboard)
gateway._dashboard.update(date=date, status="Running", portfolio=summary, holdings=holdings, trades=trades)
@@ -319,7 +320,7 @@ async def run_backtest_dates(gateway: Any, dates: list[str]) -> None:
await gateway.on_strategy_trigger(date=date)
await asyncio.sleep(0.1)
await gateway.state_sync.on_system_message(f"Backtest complete - {len(dates)} days")
summary = gateway.storage.load_file("summary") or {}
summary = gateway.storage.build_dashboard_snapshot_from_state(gateway.state_sync.state).get("summary") or {}
gateway._dashboard.update(status="Complete", portfolio=summary, days_completed=len(dates))
gateway._dashboard.stop()
gateway._dashboard.print_final_summary()

View File

@@ -164,9 +164,10 @@ def sync_runtime_state(gateway: Any) -> None:
gateway._dashboard.initial_cash = gateway.storage.initial_cash
gateway._dashboard.enable_memory = bool(gateway.config.get("enable_memory", False))
summary = gateway.storage.load_file("summary") or {}
holdings = gateway.storage.load_file("holdings") or []
trades = gateway.storage.load_file("trades") or []
dashboard_snapshot = gateway.storage.build_dashboard_snapshot_from_state(gateway.state_sync.state)
summary = dashboard_snapshot.get("summary") or {}
holdings = dashboard_snapshot.get("holdings") or []
trades = dashboard_snapshot.get("trades") or []
gateway._dashboard.update(
portfolio=summary,
holdings=holdings,

View File

@@ -361,6 +361,7 @@ async def handle_get_stock_range_explain(gateway: Any, websocket: Any, data: dic
end_date=end_date,
article_ids=article_ids if isinstance(article_ids, list) else None,
limit=100,
refresh_if_stale=False,
)
result = payload.get("result")

View File

@@ -11,7 +11,6 @@ from pathlib import Path
from typing import Any, Dict, List, Optional
from backend.data.market_store import MarketStore
from .research_db import ResearchDb
from .runtime_db import RuntimeDb
logger = logging.getLogger(__name__)
@@ -22,12 +21,18 @@ class StorageService:
Storage service for data persistence
Responsibilities:
1. Load/save dashboard JSON files
1. Export dashboard JSON files
(summary, holdings, stats, trades, leaderboard)
2. Load/save internal state (_internal_state.json)
3. Load/save server state (server_state.json) with feed history
4. Manage portfolio state persistence
5. Support loading from saved state to resume execution
Notes:
- team_dashboard/*.json is treated as an export/compatibility layer
rather than the authoritative runtime source of truth.
- authoritative runtime reads should prefer in-memory state, server_state,
runtime.db, and market_research.db.
"""
def __init__(
@@ -49,7 +54,7 @@ class StorageService:
self.initial_cash = initial_cash
self.config_name = config_name
# Dashboard file paths
# Dashboard export file paths
self.files = {
"summary": self.dashboard_dir / "summary.json",
"holdings": self.dashboard_dir / "holdings.json",
@@ -66,7 +71,6 @@ class StorageService:
self.state_dir.mkdir(parents=True, exist_ok=True)
self.server_state_file = self.state_dir / "server_state.json"
self.runtime_db = RuntimeDb(self.state_dir / "runtime.db")
self.research_db = ResearchDb(self.state_dir / "research.db")
self.market_store = MarketStore()
# Feed history (for agent messages)
@@ -84,16 +88,8 @@ class StorageService:
logger.info(f"Storage service initialized: {self.dashboard_dir}")
def load_file(self, file_type: str) -> Optional[Any]:
"""
Load dashboard JSON file
Args:
file_type: One of: summary, holdings, stats, trades, leaderboard
Returns:
Loaded data or None if file doesn't exist
"""
def load_export_file(self, file_type: str) -> Optional[Any]:
"""Load dashboard export JSON file."""
file_path = self.files.get(file_type)
if not file_path or not file_path.exists():
return None
@@ -105,14 +101,12 @@ class StorageService:
logger.error(f"Failed to load {file_type}.json: {e}")
return None
def save_file(self, file_type: str, data: Any):
"""
Save dashboard JSON file
def load_file(self, file_type: str) -> Optional[Any]:
"""Backward-compatible alias for export-layer JSON reads."""
return self.load_export_file(file_type)
Args:
file_type: One of: summary, holdings, stats, trades, leaderboard
data: Data to save
"""
def save_export_file(self, file_type: str, data: Any):
"""Save dashboard export JSON file."""
file_path = self.files.get(file_type)
if not file_path:
logger.error(f"Unknown file type: {file_type}")
@@ -129,6 +123,48 @@ class StorageService:
except Exception as e:
logger.error(f"Failed to save {file_type}.json: {e}")
def save_file(self, file_type: str, data: Any):
"""Backward-compatible alias for export-layer JSON writes."""
self.save_export_file(file_type, data)
def build_dashboard_snapshot_from_state(
self,
state: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
"""Build dashboard view data from runtime state instead of JSON exports."""
runtime_state = state or self.load_server_state()
portfolio = dict(runtime_state.get("portfolio") or {})
holdings = list(runtime_state.get("holdings") or [])
stats = runtime_state.get("stats") or self._get_default_stats()
trades = list(runtime_state.get("trades") or [])
leaderboard = list(runtime_state.get("leaderboard") or [])
summary = {
"totalAssetValue": portfolio.get("total_value", self.initial_cash),
"totalReturn": portfolio.get("pnl_percent", 0.0),
"cashPosition": portfolio.get("cash", self.initial_cash),
"tickerWeights": stats.get("tickerWeights", {}),
"totalTrades": len(trades),
"pnlPct": portfolio.get("pnl_percent", 0.0),
"balance": portfolio.get("total_value", self.initial_cash),
"equity": portfolio.get("equity", []),
"baseline": portfolio.get("baseline", []),
"baseline_vw": portfolio.get("baseline_vw", []),
"momentum": portfolio.get("momentum", []),
"equity_return": portfolio.get("equity_return", []),
"baseline_return": portfolio.get("baseline_return", []),
"baseline_vw_return": portfolio.get("baseline_vw_return", []),
"momentum_return": portfolio.get("momentum_return", []),
}
return {
"summary": summary,
"holdings": holdings,
"stats": stats,
"trades": trades,
"leaderboard": leaderboard,
}
def check_file_updates(self) -> Dict[str, bool]:
"""
Check which dashboard files have been updated since last check
@@ -297,7 +333,7 @@ class StorageService:
def initialize_empty_dashboard(self):
"""Initialize empty dashboard files with default values"""
# Summary
self.save_file(
self.save_export_file(
"summary",
{
"totalAssetValue": self.initial_cash,
@@ -315,10 +351,10 @@ class StorageService:
)
# Holdings
self.save_file("holdings", [])
self.save_export_file("holdings", [])
# Stats
self.save_file(
self.save_export_file(
"stats",
{
"totalAssetValue": self.initial_cash,
@@ -335,7 +371,7 @@ class StorageService:
)
# Trades
self.save_file("trades", [])
self.save_export_file("trades", [])
# Leaderboard with model info
self.generate_leaderboard()
@@ -375,7 +411,7 @@ class StorageService:
ranking_entries.append(entry)
leaderboard = team_entries + ranking_entries
self.save_file("leaderboard", leaderboard)
self.save_export_file("leaderboard", leaderboard)
logger.info("Leaderboard generated with model info")
def update_leaderboard_model_info(self):
@@ -398,7 +434,7 @@ class StorageService:
entry["modelName"] = model_name
entry["modelProvider"] = model_provider
self.save_file("leaderboard", existing)
self.save_export_file("leaderboard", existing)
logger.info("Leaderboard model info updated")
def get_current_timestamp_ms(self, date: str = None) -> int:
@@ -653,7 +689,7 @@ class StorageService:
"momentum": state.get("momentum_history", []),
}
self.save_file("summary", summary)
self.save_export_file("summary", summary)
def _generate_holdings(
self,
@@ -715,7 +751,7 @@ class StorageService:
# Sort by weight
holdings.sort(key=lambda x: abs(x["weight"]), reverse=True)
self.save_file("holdings", holdings)
self.save_export_file("holdings", holdings)
def _generate_stats(self, state: Dict[str, Any], net_value: float):
"""Generate stats.json"""
@@ -738,7 +774,7 @@ class StorageService:
},
}
self.save_file("stats", stats)
self.save_export_file("stats", stats)
def _generate_trades(self, state: Dict[str, Any]):
"""Generate trades.json"""
@@ -764,7 +800,7 @@ class StorageService:
},
)
self.save_file("trades", trades)
self.save_export_file("trades", trades)
# Server State Management Methods
@@ -1001,12 +1037,12 @@ class StorageService:
Args:
state: Server state dictionary to update
"""
# Load dashboard data
summary = self.load_file("summary") or {}
holdings = self.load_file("holdings") or []
stats = self.load_file("stats") or self._get_default_stats()
trades = self.load_file("trades") or []
leaderboard = self.load_file("leaderboard") or []
dashboard_snapshot = self.build_dashboard_snapshot_from_state(state)
summary = dashboard_snapshot.get("summary") or {}
holdings = dashboard_snapshot.get("holdings") or []
stats = dashboard_snapshot.get("stats") or self._get_default_stats()
trades = dashboard_snapshot.get("trades") or []
leaderboard = dashboard_snapshot.get("leaderboard") or []
internal_state = self.load_internal_state()
# Update state
@@ -1040,7 +1076,6 @@ class StorageService:
Start tracking live returns for current trading session.
Captures current values as session start baseline.
"""
summary = self.load_file("summary") or {}
state = self.load_internal_state()
# Capture current values as session start
@@ -1052,7 +1087,7 @@ class StorageService:
self._session_start_equity = (
equity_history[-1]["v"]
if equity_history
else summary.get("totalAssetValue", self.initial_cash)
else self.initial_cash
)
self._session_start_baseline = (
baseline_history[-1]["v"]