确认PokieTicker新闻库数据源
This commit is contained in:
@@ -10,6 +10,8 @@ from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from .runtime_db import RuntimeDb
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -61,6 +63,7 @@ class StorageService:
|
||||
self.state_dir = self.dashboard_dir.parent / "state"
|
||||
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")
|
||||
|
||||
# Feed history (for agent messages)
|
||||
self.max_feed_history = 200
|
||||
@@ -114,6 +117,11 @@ class StorageService:
|
||||
try:
|
||||
with open(file_path, "w", encoding="utf-8") as f:
|
||||
json.dump(data, f, indent=2, ensure_ascii=False)
|
||||
if file_type == "leaderboard" and isinstance(data, list):
|
||||
self.runtime_db.replace_signals_for_leaderboard(data)
|
||||
elif file_type == "trades" and isinstance(data, list):
|
||||
for trade in data:
|
||||
self.runtime_db.upsert_trade(trade)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to save {file_type}.json: {e}")
|
||||
|
||||
@@ -211,6 +219,7 @@ class StorageService:
|
||||
try:
|
||||
with open(self.internal_state_file, "w", encoding="utf-8") as f:
|
||||
json.dump(state, f, indent=2, ensure_ascii=False)
|
||||
self._sync_price_history_to_db(state.get("price_history", {}))
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to save internal state: {e}")
|
||||
|
||||
@@ -231,6 +240,41 @@ class StorageService:
|
||||
"margin_requirement": 0.25, # Default 25% margin requirement
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _portfolio_is_pristine(portfolio_state: Dict[str, Any]) -> bool:
|
||||
"""Return whether the persisted portfolio can be safely rebased."""
|
||||
positions = portfolio_state.get("positions", {})
|
||||
has_positions = any(
|
||||
position.get("long", 0) or position.get("short", 0)
|
||||
for position in positions.values()
|
||||
)
|
||||
margin_used = float(portfolio_state.get("margin_used", 0.0) or 0.0)
|
||||
return not has_positions and margin_used == 0.0
|
||||
|
||||
def can_apply_initial_cash(self) -> bool:
|
||||
"""Only allow initial cash changes before the run has traded."""
|
||||
state = self.load_internal_state()
|
||||
if not self._portfolio_is_pristine(state.get("portfolio_state", {})):
|
||||
return False
|
||||
if state.get("all_trades"):
|
||||
return False
|
||||
return len(state.get("equity_history", [])) <= 1
|
||||
|
||||
def apply_initial_cash(self, initial_cash: float) -> bool:
|
||||
"""Rebase storage state to a new initial cash when the run is pristine."""
|
||||
if not self.can_apply_initial_cash():
|
||||
return False
|
||||
|
||||
self.initial_cash = float(initial_cash)
|
||||
if self.internal_state_file.exists():
|
||||
self.internal_state_file.unlink()
|
||||
|
||||
self.initialize_empty_dashboard()
|
||||
state = self.load_server_state()
|
||||
self.update_server_state_from_dashboard(state)
|
||||
self.save_server_state(state)
|
||||
return True
|
||||
|
||||
def save_portfolio_state(self, portfolio: Dict[str, Any]):
|
||||
"""
|
||||
Save portfolio state to internal state
|
||||
@@ -750,6 +794,7 @@ class StorageService:
|
||||
"last_day_history": [],
|
||||
"trading_days_total": 0,
|
||||
"trading_days_completed": 0,
|
||||
"price_history": {},
|
||||
}
|
||||
|
||||
if not self.server_state_file.exists():
|
||||
@@ -771,6 +816,11 @@ class StorageService:
|
||||
)
|
||||
logger.info(f"Trades: {len(saved_state.get('trades', []))} records")
|
||||
|
||||
for event in saved_state.get("feed_history", []):
|
||||
self.runtime_db.insert_event(event)
|
||||
for trade in saved_state.get("trades", []):
|
||||
self.runtime_db.upsert_trade(trade)
|
||||
|
||||
return saved_state
|
||||
|
||||
def save_server_state(self, state: Dict[str, Any]):
|
||||
@@ -852,6 +902,7 @@ class StorageService:
|
||||
state["feed_history"] = []
|
||||
|
||||
state["feed_history"].insert(0, feed_msg)
|
||||
self.runtime_db.insert_event(feed_msg)
|
||||
|
||||
# Trim to max size
|
||||
if len(state["feed_history"]) > self.max_feed_history:
|
||||
@@ -861,6 +912,69 @@ class StorageService:
|
||||
|
||||
return True
|
||||
|
||||
def record_price_point(
|
||||
self,
|
||||
*,
|
||||
ticker: str,
|
||||
timestamp: str,
|
||||
price: float,
|
||||
open_price: Optional[float] = None,
|
||||
ret: Optional[float] = None,
|
||||
source: Optional[str] = None,
|
||||
meta: Optional[Dict[str, Any]] = None,
|
||||
):
|
||||
"""Persist a runtime price point for later query-oriented reads."""
|
||||
if not ticker or not timestamp:
|
||||
return
|
||||
try:
|
||||
self.runtime_db.insert_price_point(
|
||||
ticker=ticker,
|
||||
timestamp=timestamp,
|
||||
price=price,
|
||||
open_price=open_price,
|
||||
ret=ret,
|
||||
source=source,
|
||||
meta=meta,
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.warning("Failed to record price point for %s: %s", ticker, exc)
|
||||
|
||||
def _sync_price_history_to_db(self, price_history: Dict[str, Any]):
|
||||
"""Backfill structured price points from serialized internal state."""
|
||||
if not isinstance(price_history, dict):
|
||||
return
|
||||
for ticker, points in price_history.items():
|
||||
if not ticker or not isinstance(points, list):
|
||||
continue
|
||||
for point in points:
|
||||
if isinstance(point, (list, tuple)) and len(point) >= 2:
|
||||
timestamp, price = point[0], point[1]
|
||||
try:
|
||||
self.record_price_point(
|
||||
ticker=str(ticker),
|
||||
timestamp=str(timestamp),
|
||||
price=float(price),
|
||||
)
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
elif isinstance(point, dict):
|
||||
timestamp = point.get("timestamp") or point.get("label") or point.get("date")
|
||||
price = point.get("price") or point.get("close") or point.get("value")
|
||||
if not timestamp or price is None:
|
||||
continue
|
||||
try:
|
||||
self.record_price_point(
|
||||
ticker=str(ticker),
|
||||
timestamp=str(timestamp),
|
||||
price=float(price),
|
||||
open_price=point.get("open"),
|
||||
ret=point.get("ret"),
|
||||
source=point.get("source"),
|
||||
meta=point,
|
||||
)
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
|
||||
def _get_default_stats(self) -> Dict[str, Any]:
|
||||
"""Get default stats structure"""
|
||||
return {
|
||||
@@ -889,6 +1003,7 @@ class StorageService:
|
||||
stats = self.load_file("stats") or self._get_default_stats()
|
||||
trades = self.load_file("trades") or []
|
||||
leaderboard = self.load_file("leaderboard") or []
|
||||
internal_state = self.load_internal_state()
|
||||
|
||||
# Update state
|
||||
state["portfolio"] = {
|
||||
@@ -910,6 +1025,9 @@ class StorageService:
|
||||
state["stats"] = stats
|
||||
state["trades"] = trades
|
||||
state["leaderboard"] = leaderboard
|
||||
state["price_history"] = internal_state.get("price_history", {})
|
||||
self.runtime_db.replace_signals_for_leaderboard(leaderboard)
|
||||
self._sync_price_history_to_db(state["price_history"])
|
||||
|
||||
# ========== Live Returns Tracking ==========
|
||||
|
||||
|
||||
Reference in New Issue
Block a user