Initial commit of integrated agent system
This commit is contained in:
923
backend/services/gateway.py
Normal file
923
backend/services/gateway.py
Normal file
@@ -0,0 +1,923 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
WebSocket Gateway for frontend communication
|
||||
"""
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable, Dict, List, Optional, Set
|
||||
|
||||
import websockets
|
||||
from websockets.asyncio.server import ServerConnection
|
||||
|
||||
from backend.data.provider_utils import normalize_symbol
|
||||
from backend.domains import news as news_domain
|
||||
from backend.llm.models import get_agent_model_info
|
||||
from backend.core.pipeline import TradingPipeline
|
||||
from backend.core.state_sync import StateSync
|
||||
from backend.services.market import MarketService
|
||||
from backend.services.storage import StorageService
|
||||
from backend.data.provider_router import get_provider_router
|
||||
from backend.tools.technical_signals import StockTechnicalAnalyzer
|
||||
from backend.core.scheduler import Scheduler
|
||||
from backend.services import gateway_admin_handlers
|
||||
from backend.services import gateway_cycle_support
|
||||
from backend.services import gateway_openclaw_handlers
|
||||
from backend.services import gateway_runtime_support
|
||||
from backend.services import gateway_stock_handlers
|
||||
from shared.client import NewsServiceClient
|
||||
from shared.client import TradingServiceClient
|
||||
from shared.client.openclaw_websocket_client import OpenClawWebSocketClient, DEFAULT_GATEWAY_URL as OPENCLAW_WS_URL
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
EDITABLE_AGENT_WORKSPACE_FILES = {
|
||||
"SOUL.md",
|
||||
"PROFILE.md",
|
||||
"AGENTS.md",
|
||||
"MEMORY.md",
|
||||
"POLICY.md",
|
||||
}
|
||||
|
||||
|
||||
class Gateway:
|
||||
"""WebSocket Gateway for frontend communication"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
market_service: MarketService,
|
||||
storage_service: StorageService,
|
||||
pipeline: TradingPipeline,
|
||||
state_sync: Optional[StateSync] = None,
|
||||
scheduler_callback: Optional[Callable] = None,
|
||||
scheduler: Optional[Scheduler] = None,
|
||||
config: Dict[str, Any] = None,
|
||||
):
|
||||
self.market_service = market_service
|
||||
self.storage = storage_service
|
||||
self.pipeline = pipeline
|
||||
self.scheduler_callback = scheduler_callback
|
||||
self.scheduler = scheduler
|
||||
self.config = config or {}
|
||||
|
||||
self.mode = self.config.get("mode", "live")
|
||||
self.is_backtest = self.mode == "backtest" or self.config.get(
|
||||
"backtest_mode",
|
||||
False,
|
||||
)
|
||||
|
||||
self.state_sync = state_sync or StateSync(storage=storage_service)
|
||||
# self.state_sync.set_mode(self.is_backtest)
|
||||
self.state_sync.set_broadcast_fn(self.broadcast)
|
||||
self.pipeline.state_sync = self.state_sync
|
||||
|
||||
self.connected_clients: Set[ServerConnection] = set()
|
||||
self.lock = asyncio.Lock()
|
||||
self._cycle_lock = asyncio.Lock()
|
||||
self._backtest_task: Optional[asyncio.Task] = None
|
||||
self._manual_cycle_task: Optional[asyncio.Task] = None
|
||||
self._backtest_start_date: Optional[str] = None
|
||||
self._backtest_end_date: Optional[str] = None
|
||||
self._market_status_task: Optional[asyncio.Task] = None
|
||||
self._watchlist_ingest_task: Optional[asyncio.Task] = None
|
||||
|
||||
# Session tracking for live returns
|
||||
self._session_start_portfolio_value: Optional[float] = None
|
||||
self._provider_router = get_provider_router()
|
||||
self._loop: Optional[asyncio.AbstractEventLoop] = None
|
||||
self._project_root = Path(__file__).resolve().parents[2]
|
||||
self._technical_analyzer = StockTechnicalAnalyzer()
|
||||
self._openclaw_ws: OpenClawWebSocketClient | None = None
|
||||
|
||||
async def start(self, host: str = "0.0.0.0", port: int = 8766):
|
||||
"""Start gateway server with proper initialization order.
|
||||
|
||||
Phase 1: Start WebSocket server first so frontend can connect immediately
|
||||
Phase 2: Start market data service (pushes data to connected clients)
|
||||
Phase 3: Start scheduler last (triggers trading cycles)
|
||||
"""
|
||||
logger.info(f"Starting gateway on {host}:{port}")
|
||||
self._loop = asyncio.get_running_loop()
|
||||
self._provider_router.add_listener(self._on_provider_usage_changed)
|
||||
|
||||
self.state_sync.load_state()
|
||||
self.market_service.set_price_recorder(self.storage.record_price_point)
|
||||
self.state_sync.update_state("status", "initializing")
|
||||
self.state_sync.update_state("server_mode", self.mode)
|
||||
self.state_sync.update_state("is_backtest", self.is_backtest)
|
||||
self.state_sync.update_state("tickers", self.config.get("tickers", []))
|
||||
self.state_sync.update_state(
|
||||
"runtime_config",
|
||||
{
|
||||
"tickers": self.config.get("tickers", []),
|
||||
"schedule_mode": self.config.get("schedule_mode", "daily"),
|
||||
"interval_minutes": self.config.get("interval_minutes", 60),
|
||||
"trigger_time": self.config.get("trigger_time", "09:30"),
|
||||
"initial_cash": self.config.get(
|
||||
"initial_cash",
|
||||
self.storage.initial_cash,
|
||||
),
|
||||
"margin_requirement": self.config.get("margin_requirement"),
|
||||
"max_comm_cycles": self.config.get("max_comm_cycles"),
|
||||
"enable_memory": self.config.get("enable_memory", False),
|
||||
},
|
||||
)
|
||||
self.state_sync.update_state(
|
||||
"data_sources",
|
||||
self._provider_router.get_usage_snapshot(),
|
||||
)
|
||||
|
||||
# Load and display existing portfolio state if available
|
||||
dashboard_snapshot = self.storage.build_dashboard_snapshot_from_state(self.state_sync.state)
|
||||
summary = dashboard_snapshot.get("summary")
|
||||
if summary:
|
||||
logger.info(
|
||||
"Loaded existing portfolio: $%s",
|
||||
f"{summary.get('totalAssetValue', 0):,.2f}",
|
||||
)
|
||||
|
||||
# ======================================================================
|
||||
# PHASE 1: Start WebSocket server first
|
||||
# This allows frontend to connect immediately and receive status updates
|
||||
# ======================================================================
|
||||
logger.info("[Phase 1/4] Starting WebSocket server...")
|
||||
self.state_sync.update_state("status", "websocket_ready")
|
||||
|
||||
# Create server but don't block yet - we'll serve inside the context manager
|
||||
server = await websockets.serve(
|
||||
self.handle_client,
|
||||
host,
|
||||
port,
|
||||
ping_interval=30,
|
||||
ping_timeout=60,
|
||||
)
|
||||
logger.info(f"WebSocket server ready: ws://{host}:{port}")
|
||||
|
||||
# Give a brief moment for any existing clients to reconnect
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
# Connect to OpenClaw Gateway (18789) via WebSocket
|
||||
logger.info("Connecting to OpenClaw Gateway...")
|
||||
try:
|
||||
self._openclaw_ws = OpenClawWebSocketClient(
|
||||
url=OPENCLAW_WS_URL,
|
||||
client_name="gateway-client",
|
||||
client_version="1.0.0",
|
||||
)
|
||||
await self._openclaw_ws.connect()
|
||||
logger.info("OpenClaw Gateway WebSocket connected")
|
||||
except Exception as e:
|
||||
logger.warning("Failed to connect to OpenClaw Gateway: %s", e)
|
||||
self._openclaw_ws = None
|
||||
|
||||
# ======================================================================
|
||||
# PHASE 2: Start market data service
|
||||
# Now frontend is connected, start pushing price updates
|
||||
# ======================================================================
|
||||
logger.info("[Phase 2/4] Starting market data service...")
|
||||
self.state_sync.update_state("status", "market_service_starting")
|
||||
await self.market_service.start(broadcast_func=self.broadcast)
|
||||
self.state_sync.update_state("status", "market_service_ready")
|
||||
logger.info("Market data service ready - price updates active")
|
||||
|
||||
# ======================================================================
|
||||
# PHASE 3: Start market status monitoring
|
||||
# Monitors market open/close and broadcasts status
|
||||
# ======================================================================
|
||||
logger.info("[Phase 3/4] Starting market status monitoring...")
|
||||
if not self.is_backtest:
|
||||
self._market_status_task = asyncio.create_task(
|
||||
self._market_status_monitor(),
|
||||
)
|
||||
|
||||
# ======================================================================
|
||||
# PHASE 4: Start scheduler last
|
||||
# Only start trading after everything else is ready
|
||||
# ======================================================================
|
||||
logger.info("[Phase 4/4] Starting scheduler...")
|
||||
self.state_sync.update_state("status", "scheduler_starting")
|
||||
|
||||
if self.scheduler:
|
||||
# Wire up heartbeat callback if heartbeat is configured
|
||||
heartbeat_interval = self.config.get("heartbeat_interval", 0)
|
||||
if heartbeat_interval and heartbeat_interval > 0:
|
||||
self.scheduler.set_heartbeat_callback(self.on_heartbeat_trigger)
|
||||
logger.info(
|
||||
f"[Heartbeat] Registered heartbeat callback (interval={heartbeat_interval}s)",
|
||||
)
|
||||
await self.scheduler.start(self.on_strategy_trigger)
|
||||
elif self.scheduler_callback:
|
||||
await self.scheduler_callback(callback=self.on_strategy_trigger)
|
||||
|
||||
self.state_sync.update_state("status", "running")
|
||||
logger.info(
|
||||
f"Gateway fully operational: ws://{host}:{port}, mode={self.mode}",
|
||||
)
|
||||
|
||||
# Keep server running
|
||||
await asyncio.Future()
|
||||
|
||||
def _on_provider_usage_changed(self, snapshot: Dict[str, Any]):
|
||||
"""Handle provider routing updates from the shared router."""
|
||||
self.state_sync.update_state("data_sources", snapshot)
|
||||
if self._loop and self._loop.is_running():
|
||||
asyncio.run_coroutine_threadsafe(
|
||||
self.broadcast(
|
||||
{
|
||||
"type": "data_sources_update",
|
||||
"data_sources": snapshot,
|
||||
},
|
||||
),
|
||||
self._loop,
|
||||
)
|
||||
|
||||
@property
|
||||
def state(self) -> Dict[str, Any]:
|
||||
return self.state_sync.state
|
||||
|
||||
@staticmethod
|
||||
def _news_rows_need_enrichment(rows: List[Dict[str, Any]]) -> bool:
|
||||
return news_domain.news_rows_need_enrichment(rows)
|
||||
|
||||
def _news_service_url(self) -> str | None:
|
||||
"""Return configured news-service base URL, if any."""
|
||||
candidate = self.config.get("news_service_url") or os.getenv(
|
||||
"NEWS_SERVICE_URL",
|
||||
"",
|
||||
)
|
||||
value = str(candidate or "").strip()
|
||||
return value or None
|
||||
|
||||
def _trading_service_url(self) -> str | None:
|
||||
"""Return configured trading-service base URL, if any."""
|
||||
candidate = self.config.get("trading_service_url") or os.getenv(
|
||||
"TRADING_SERVICE_URL",
|
||||
"",
|
||||
)
|
||||
value = str(candidate or "").strip()
|
||||
return value or None
|
||||
|
||||
async def _call_news_service(
|
||||
self,
|
||||
action: str,
|
||||
callback: Callable[[NewsServiceClient], Any],
|
||||
) -> Any | None:
|
||||
"""Call news-service when configured, otherwise return None."""
|
||||
service_url = self._news_service_url()
|
||||
if not service_url:
|
||||
return None
|
||||
|
||||
try:
|
||||
async with NewsServiceClient(service_url) as client:
|
||||
return await callback(client)
|
||||
except Exception as exc:
|
||||
logger.warning("news-service %s failed: %s", action, exc)
|
||||
return None
|
||||
|
||||
async def _call_trading_service(
|
||||
self,
|
||||
action: str,
|
||||
callback: Callable[[TradingServiceClient], Any],
|
||||
) -> Any | None:
|
||||
"""Call trading-service when configured, otherwise return None."""
|
||||
service_url = self._trading_service_url()
|
||||
if not service_url:
|
||||
return None
|
||||
|
||||
try:
|
||||
async with TradingServiceClient(service_url) as client:
|
||||
return await callback(client)
|
||||
except Exception as exc:
|
||||
logger.warning("trading-service %s failed: %s", action, exc)
|
||||
return None
|
||||
|
||||
async def handle_client(self, websocket: ServerConnection):
|
||||
"""Handle WebSocket client connection"""
|
||||
async with self.lock:
|
||||
self.connected_clients.add(websocket)
|
||||
|
||||
await self._send_initial_state(websocket)
|
||||
await self._handle_client_messages(websocket)
|
||||
|
||||
async with self.lock:
|
||||
self.connected_clients.discard(websocket)
|
||||
|
||||
async def _send_initial_state(self, websocket: ServerConnection):
|
||||
try:
|
||||
logger.info("[Gateway] Sending initial state to client...")
|
||||
state_payload = self.state_sync.get_initial_state_payload(
|
||||
include_dashboard=True,
|
||||
)
|
||||
state_payload["data_sources"] = (
|
||||
self._provider_router.get_usage_snapshot()
|
||||
)
|
||||
# Include market status in initial state
|
||||
state_payload[
|
||||
"market_status"
|
||||
] = self.market_service.get_market_status()
|
||||
|
||||
# Include live returns if session is active
|
||||
if self.storage.is_live_session_active:
|
||||
live_returns = self.storage.get_live_returns()
|
||||
if "portfolio" in state_payload:
|
||||
state_payload["portfolio"].update(live_returns)
|
||||
|
||||
await websocket.send(
|
||||
json.dumps(
|
||||
{"type": "initial_state", "state": state_payload},
|
||||
ensure_ascii=False,
|
||||
default=str,
|
||||
),
|
||||
)
|
||||
logger.info("[Gateway] Initial state sent successfully")
|
||||
except Exception as e:
|
||||
logger.exception(f"[Gateway] Failed to send initial state: {e}")
|
||||
# Send error response so client knows something went wrong
|
||||
try:
|
||||
await websocket.send(
|
||||
json.dumps(
|
||||
{"type": "error", "message": "Failed to load initial state"},
|
||||
ensure_ascii=False,
|
||||
),
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to send error response to client: {e}")
|
||||
|
||||
async def _handle_client_messages(
|
||||
self,
|
||||
websocket: ServerConnection,
|
||||
):
|
||||
try:
|
||||
async for message in websocket:
|
||||
data = json.loads(message)
|
||||
msg_type = data.get("type", "unknown")
|
||||
|
||||
if msg_type == "ping":
|
||||
await websocket.send(
|
||||
json.dumps(
|
||||
{
|
||||
"type": "pong",
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
},
|
||||
ensure_ascii=False,
|
||||
),
|
||||
)
|
||||
elif msg_type == "get_state":
|
||||
await self._send_initial_state(websocket)
|
||||
elif msg_type == "start_backtest":
|
||||
await self._handle_start_backtest(data)
|
||||
elif msg_type == "trigger_strategy":
|
||||
await self._handle_manual_trigger(websocket, data)
|
||||
elif msg_type == "update_runtime_config":
|
||||
await self._handle_update_runtime_config(websocket, data)
|
||||
elif msg_type == "reload_runtime_assets":
|
||||
await self._handle_reload_runtime_assets()
|
||||
elif msg_type == "update_watchlist":
|
||||
await self._handle_update_watchlist(websocket, data)
|
||||
elif msg_type == "get_agent_skills":
|
||||
await self._handle_get_agent_skills(websocket, data)
|
||||
elif msg_type == "get_agent_profile":
|
||||
await self._handle_get_agent_profile(websocket, data)
|
||||
elif msg_type == "get_skill_detail":
|
||||
await self._handle_get_skill_detail(websocket, data)
|
||||
elif msg_type == "create_agent_local_skill":
|
||||
await self._handle_create_agent_local_skill(websocket, data)
|
||||
elif msg_type == "update_agent_local_skill":
|
||||
await self._handle_update_agent_local_skill(websocket, data)
|
||||
elif msg_type == "delete_agent_local_skill":
|
||||
await self._handle_delete_agent_local_skill(websocket, data)
|
||||
elif msg_type == "remove_agent_skill":
|
||||
await self._handle_remove_agent_skill(websocket, data)
|
||||
elif msg_type == "update_agent_skill":
|
||||
await self._handle_update_agent_skill(websocket, data)
|
||||
elif msg_type == "get_agent_workspace_file":
|
||||
await self._handle_get_agent_workspace_file(websocket, data)
|
||||
elif msg_type == "update_agent_workspace_file":
|
||||
await self._handle_update_agent_workspace_file(websocket, data)
|
||||
elif msg_type == "get_stock_history":
|
||||
await self._handle_get_stock_history(websocket, data)
|
||||
elif msg_type == "get_stock_explain_events":
|
||||
await self._handle_get_stock_explain_events(websocket, data)
|
||||
elif msg_type == "get_stock_news":
|
||||
await self._handle_get_stock_news(websocket, data)
|
||||
elif msg_type == "get_stock_news_for_date":
|
||||
await self._handle_get_stock_news_for_date(websocket, data)
|
||||
elif msg_type == "get_stock_news_timeline":
|
||||
await self._handle_get_stock_news_timeline(websocket, data)
|
||||
elif msg_type == "get_stock_news_categories":
|
||||
await self._handle_get_stock_news_categories(websocket, data)
|
||||
elif msg_type == "get_stock_range_explain":
|
||||
await self._handle_get_stock_range_explain(websocket, data)
|
||||
elif msg_type == "get_stock_insider_trades":
|
||||
await self._handle_get_stock_insider_trades(websocket, data)
|
||||
elif msg_type == "get_stock_story":
|
||||
await self._handle_get_stock_story(websocket, data)
|
||||
elif msg_type == "get_stock_similar_days":
|
||||
await self._handle_get_stock_similar_days(websocket, data)
|
||||
elif msg_type == "get_stock_technical_indicators":
|
||||
await self._handle_get_stock_technical_indicators(websocket, data)
|
||||
elif msg_type == "run_stock_enrich":
|
||||
await self._handle_run_stock_enrich(websocket, data)
|
||||
elif msg_type == "get_openclaw_status":
|
||||
await self._handle_get_openclaw_status(websocket, data)
|
||||
elif msg_type == "get_openclaw_sessions":
|
||||
await self._handle_get_openclaw_sessions(websocket, data)
|
||||
elif msg_type == "get_openclaw_session_detail":
|
||||
await self._handle_get_openclaw_session_detail(websocket, data)
|
||||
elif msg_type == "get_openclaw_session_history":
|
||||
await self._handle_get_openclaw_session_history(websocket, data)
|
||||
elif msg_type == "get_openclaw_cron":
|
||||
await self._handle_get_openclaw_cron(websocket, data)
|
||||
elif msg_type == "get_openclaw_approvals":
|
||||
await self._handle_get_openclaw_approvals(websocket, data)
|
||||
elif msg_type == "get_openclaw_agents":
|
||||
await self._handle_get_openclaw_agents(websocket, data)
|
||||
elif msg_type == "get_openclaw_agents_presence":
|
||||
await self._handle_get_openclaw_agents_presence(websocket, data)
|
||||
elif msg_type == "get_openclaw_skills":
|
||||
await self._handle_get_openclaw_skills(websocket, data)
|
||||
elif msg_type == "get_openclaw_models":
|
||||
await self._handle_get_openclaw_models(websocket, data)
|
||||
elif msg_type == "get_openclaw_hooks":
|
||||
await gateway_openclaw_handlers.handle_get_openclaw_hooks(self, websocket, data)
|
||||
elif msg_type == "get_openclaw_plugins":
|
||||
await gateway_openclaw_handlers.handle_get_openclaw_plugins(self, websocket, data)
|
||||
elif msg_type == "get_openclaw_secrets_audit":
|
||||
await gateway_openclaw_handlers.handle_get_openclaw_secrets_audit(self, websocket, data)
|
||||
elif msg_type == "get_openclaw_security_audit":
|
||||
await gateway_openclaw_handlers.handle_get_openclaw_security_audit(self, websocket, data)
|
||||
elif msg_type == "get_openclaw_daemon_status":
|
||||
await gateway_openclaw_handlers.handle_get_openclaw_daemon_status(self, websocket, data)
|
||||
elif msg_type == "get_openclaw_pairing":
|
||||
await gateway_openclaw_handlers.handle_get_openclaw_pairing(self, websocket, data)
|
||||
elif msg_type == "get_openclaw_qr":
|
||||
await gateway_openclaw_handlers.handle_get_openclaw_qr(self, websocket, data)
|
||||
elif msg_type == "get_openclaw_update_status":
|
||||
await gateway_openclaw_handlers.handle_get_openclaw_update_status(self, websocket, data)
|
||||
elif msg_type == "get_openclaw_models_aliases":
|
||||
await gateway_openclaw_handlers.handle_get_openclaw_models_aliases(self, websocket, data)
|
||||
elif msg_type == "get_openclaw_models_fallbacks":
|
||||
await gateway_openclaw_handlers.handle_get_openclaw_models_fallbacks(self, websocket, data)
|
||||
elif msg_type == "get_openclaw_models_image_fallbacks":
|
||||
await gateway_openclaw_handlers.handle_get_openclaw_models_image_fallbacks(self, websocket, data)
|
||||
elif msg_type == "get_openclaw_skill_update":
|
||||
await gateway_openclaw_handlers.handle_get_openclaw_skill_update(self, websocket, data)
|
||||
elif msg_type == "get_openclaw_workspace_files":
|
||||
await gateway_openclaw_handlers.handle_get_openclaw_workspace_files(self, websocket, data)
|
||||
elif msg_type == "get_openclaw_workspace_file":
|
||||
await gateway_openclaw_handlers.handle_get_openclaw_workspace_file(self, websocket, data)
|
||||
elif msg_type == "openclaw_resolve_session":
|
||||
await gateway_openclaw_handlers.handle_openclaw_resolve_session(self, websocket, data)
|
||||
elif msg_type == "openclaw_create_session":
|
||||
await gateway_openclaw_handlers.handle_openclaw_create_session(self, websocket, data)
|
||||
elif msg_type == "openclaw_send_message":
|
||||
await gateway_openclaw_handlers.handle_openclaw_send_message(self, websocket, data)
|
||||
elif msg_type == "openclaw_subscribe_session":
|
||||
await gateway_openclaw_handlers.handle_openclaw_subscribe_session(self, websocket, data)
|
||||
elif msg_type == "openclaw_unsubscribe_session":
|
||||
await gateway_openclaw_handlers.handle_openclaw_unsubscribe_session(self, websocket, data)
|
||||
elif msg_type == "openclaw_reset_session":
|
||||
await gateway_openclaw_handlers.handle_openclaw_reset_session(self, websocket, data)
|
||||
elif msg_type == "openclaw_delete_session":
|
||||
await gateway_openclaw_handlers.handle_openclaw_delete_session(self, websocket, data)
|
||||
|
||||
except websockets.ConnectionClosed:
|
||||
pass
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
finally:
|
||||
subscriber_map = getattr(self, "_openclaw_session_subscribers", None)
|
||||
if isinstance(subscriber_map, dict):
|
||||
subscriber_map.pop(websocket, None)
|
||||
|
||||
async def _handle_get_stock_history(
|
||||
self,
|
||||
websocket: ServerConnection,
|
||||
data: Dict[str, Any],
|
||||
):
|
||||
await gateway_stock_handlers.handle_get_stock_history(self, websocket, data)
|
||||
|
||||
async def _handle_get_stock_explain_events(
|
||||
self,
|
||||
websocket: ServerConnection,
|
||||
data: Dict[str, Any],
|
||||
):
|
||||
await gateway_stock_handlers.handle_get_stock_explain_events(self, websocket, data)
|
||||
|
||||
async def _handle_get_stock_news(
|
||||
self,
|
||||
websocket: ServerConnection,
|
||||
data: Dict[str, Any],
|
||||
):
|
||||
await gateway_stock_handlers.handle_get_stock_news(self, websocket, data)
|
||||
|
||||
async def _handle_get_stock_news_for_date(
|
||||
self,
|
||||
websocket: ServerConnection,
|
||||
data: Dict[str, Any],
|
||||
):
|
||||
await gateway_stock_handlers.handle_get_stock_news_for_date(self, websocket, data)
|
||||
|
||||
async def _handle_get_stock_news_timeline(
|
||||
self,
|
||||
websocket: ServerConnection,
|
||||
data: Dict[str, Any],
|
||||
):
|
||||
await gateway_stock_handlers.handle_get_stock_news_timeline(self, websocket, data)
|
||||
|
||||
async def _handle_get_stock_news_categories(
|
||||
self,
|
||||
websocket: ServerConnection,
|
||||
data: Dict[str, Any],
|
||||
):
|
||||
await gateway_stock_handlers.handle_get_stock_news_categories(self, websocket, data)
|
||||
|
||||
async def _handle_get_stock_range_explain(
|
||||
self,
|
||||
websocket: ServerConnection,
|
||||
data: Dict[str, Any],
|
||||
):
|
||||
await gateway_stock_handlers.handle_get_stock_range_explain(self, websocket, data)
|
||||
|
||||
async def _handle_get_stock_insider_trades(
|
||||
self,
|
||||
websocket: ServerConnection,
|
||||
data: Dict[str, Any],
|
||||
):
|
||||
await gateway_stock_handlers.handle_get_stock_insider_trades(self, websocket, data)
|
||||
|
||||
async def _handle_get_stock_story(
|
||||
self,
|
||||
websocket: ServerConnection,
|
||||
data: Dict[str, Any],
|
||||
):
|
||||
await gateway_stock_handlers.handle_get_stock_story(self, websocket, data)
|
||||
|
||||
async def _handle_get_stock_similar_days(
|
||||
self,
|
||||
websocket: ServerConnection,
|
||||
data: Dict[str, Any],
|
||||
):
|
||||
await gateway_stock_handlers.handle_get_stock_similar_days(self, websocket, data)
|
||||
|
||||
async def _handle_get_stock_technical_indicators(
|
||||
self,
|
||||
websocket: ServerConnection,
|
||||
data: Dict[str, Any],
|
||||
):
|
||||
await gateway_stock_handlers.handle_get_stock_technical_indicators(self, websocket, data)
|
||||
|
||||
async def _handle_run_stock_enrich(
|
||||
self,
|
||||
websocket: ServerConnection,
|
||||
data: Dict[str, Any],
|
||||
):
|
||||
await gateway_stock_handlers.handle_run_stock_enrich(self, websocket, data)
|
||||
|
||||
async def _handle_start_backtest(self, data: Dict[str, Any]):
|
||||
if not self.is_backtest:
|
||||
return
|
||||
dates = data.get("dates", [])
|
||||
if dates and self._backtest_task is None:
|
||||
task = asyncio.create_task(
|
||||
self._run_backtest_dates(dates),
|
||||
)
|
||||
task.add_done_callback(self._handle_backtest_exception)
|
||||
self._backtest_task = task
|
||||
|
||||
async def _handle_manual_trigger(
|
||||
self,
|
||||
websocket: ServerConnection,
|
||||
data: Dict[str, Any],
|
||||
) -> None:
|
||||
"""Run one live trading cycle on demand."""
|
||||
if self.is_backtest:
|
||||
await websocket.send(
|
||||
json.dumps(
|
||||
{
|
||||
"type": "error",
|
||||
"message": "Manual trigger is only available in live mode.",
|
||||
},
|
||||
ensure_ascii=False,
|
||||
),
|
||||
)
|
||||
return
|
||||
|
||||
if (
|
||||
self._cycle_lock.locked()
|
||||
or (
|
||||
self._manual_cycle_task is not None
|
||||
and not self._manual_cycle_task.done()
|
||||
)
|
||||
):
|
||||
await websocket.send(
|
||||
json.dumps(
|
||||
{
|
||||
"type": "error",
|
||||
"message": "A trading cycle is already running.",
|
||||
},
|
||||
ensure_ascii=False,
|
||||
),
|
||||
)
|
||||
await self.state_sync.on_system_message("已有任务在运行,已忽略手动触发")
|
||||
return
|
||||
|
||||
requested_date = data.get("date")
|
||||
await self.state_sync.on_system_message("收到手动触发请求,准备开始新一轮分析与决策")
|
||||
task = asyncio.create_task(
|
||||
self.on_strategy_trigger(
|
||||
date=requested_date or datetime.now().strftime("%Y-%m-%d"),
|
||||
),
|
||||
)
|
||||
task.add_done_callback(self._handle_manual_cycle_exception)
|
||||
self._manual_cycle_task = task
|
||||
|
||||
async def _handle_reload_runtime_assets(self):
|
||||
await gateway_admin_handlers.handle_reload_runtime_assets(self)
|
||||
|
||||
async def _handle_update_runtime_config(
|
||||
self,
|
||||
websocket: ServerConnection,
|
||||
data: Dict[str, Any],
|
||||
) -> None:
|
||||
await gateway_admin_handlers.handle_update_runtime_config(self, websocket, data)
|
||||
|
||||
async def _handle_update_watchlist(
|
||||
self,
|
||||
websocket: ServerConnection,
|
||||
data: Dict[str, Any],
|
||||
) -> None:
|
||||
await gateway_admin_handlers.handle_update_watchlist(self, websocket, data)
|
||||
|
||||
async def _handle_get_agent_skills(
|
||||
self,
|
||||
websocket: ServerConnection,
|
||||
data: Dict[str, Any],
|
||||
) -> None:
|
||||
await gateway_admin_handlers.handle_get_agent_skills(self, websocket, data)
|
||||
|
||||
async def _handle_get_agent_profile(
|
||||
self,
|
||||
websocket: ServerConnection,
|
||||
data: Dict[str, Any],
|
||||
) -> None:
|
||||
await gateway_admin_handlers.handle_get_agent_profile(self, websocket, data)
|
||||
|
||||
async def _handle_get_skill_detail(
|
||||
self,
|
||||
websocket: ServerConnection,
|
||||
data: Dict[str, Any],
|
||||
) -> None:
|
||||
await gateway_admin_handlers.handle_get_skill_detail(self, websocket, data)
|
||||
|
||||
async def _handle_create_agent_local_skill(
|
||||
self,
|
||||
websocket: ServerConnection,
|
||||
data: Dict[str, Any],
|
||||
) -> None:
|
||||
await gateway_admin_handlers.handle_create_agent_local_skill(self, websocket, data)
|
||||
|
||||
async def _handle_update_agent_local_skill(
|
||||
self,
|
||||
websocket: ServerConnection,
|
||||
data: Dict[str, Any],
|
||||
) -> None:
|
||||
await gateway_admin_handlers.handle_update_agent_local_skill(self, websocket, data)
|
||||
|
||||
async def _handle_delete_agent_local_skill(
|
||||
self,
|
||||
websocket: ServerConnection,
|
||||
data: Dict[str, Any],
|
||||
) -> None:
|
||||
await gateway_admin_handlers.handle_delete_agent_local_skill(self, websocket, data)
|
||||
|
||||
async def _handle_remove_agent_skill(
|
||||
self,
|
||||
websocket: ServerConnection,
|
||||
data: Dict[str, Any],
|
||||
) -> None:
|
||||
await gateway_admin_handlers.handle_remove_agent_skill(self, websocket, data)
|
||||
|
||||
async def _handle_update_agent_skill(
|
||||
self,
|
||||
websocket: ServerConnection,
|
||||
data: Dict[str, Any],
|
||||
) -> None:
|
||||
await gateway_admin_handlers.handle_update_agent_skill(self, websocket, data)
|
||||
|
||||
async def _handle_get_agent_workspace_file(
|
||||
self,
|
||||
websocket: ServerConnection,
|
||||
data: Dict[str, Any],
|
||||
) -> None:
|
||||
await gateway_admin_handlers.handle_get_agent_workspace_file(self, websocket, data)
|
||||
|
||||
async def _handle_update_agent_workspace_file(
|
||||
self,
|
||||
websocket: ServerConnection,
|
||||
data: Dict[str, Any],
|
||||
) -> None:
|
||||
await gateway_admin_handlers.handle_update_agent_workspace_file(self, websocket, data)
|
||||
|
||||
async def _handle_get_openclaw_status(
|
||||
self,
|
||||
websocket: ServerConnection,
|
||||
data: Dict[str, Any],
|
||||
) -> None:
|
||||
await gateway_openclaw_handlers.handle_get_openclaw_status(self, websocket, data)
|
||||
|
||||
async def _handle_get_openclaw_sessions(
|
||||
self,
|
||||
websocket: ServerConnection,
|
||||
data: Dict[str, Any],
|
||||
) -> None:
|
||||
await gateway_openclaw_handlers.handle_get_openclaw_sessions(self, websocket, data)
|
||||
|
||||
async def _handle_get_openclaw_session_detail(
|
||||
self,
|
||||
websocket: ServerConnection,
|
||||
data: Dict[str, Any],
|
||||
) -> None:
|
||||
await gateway_openclaw_handlers.handle_get_openclaw_session_detail(self, websocket, data)
|
||||
|
||||
async def _handle_get_openclaw_session_history(
|
||||
self,
|
||||
websocket: ServerConnection,
|
||||
data: Dict[str, Any],
|
||||
) -> None:
|
||||
await gateway_openclaw_handlers.handle_get_openclaw_session_history(self, websocket, data)
|
||||
|
||||
async def _handle_get_openclaw_cron(
|
||||
self,
|
||||
websocket: ServerConnection,
|
||||
data: Dict[str, Any],
|
||||
) -> None:
|
||||
await gateway_openclaw_handlers.handle_get_openclaw_cron(self, websocket, data)
|
||||
|
||||
async def _handle_get_openclaw_approvals(
|
||||
self,
|
||||
websocket: ServerConnection,
|
||||
data: Dict[str, Any],
|
||||
) -> None:
|
||||
await gateway_openclaw_handlers.handle_get_openclaw_approvals(self, websocket, data)
|
||||
|
||||
async def _handle_get_openclaw_agents(
|
||||
self,
|
||||
websocket: ServerConnection,
|
||||
data: Dict[str, Any],
|
||||
) -> None:
|
||||
await gateway_openclaw_handlers.handle_get_openclaw_agents(self, websocket, data)
|
||||
|
||||
async def _handle_get_openclaw_agents_presence(
|
||||
self,
|
||||
websocket: ServerConnection,
|
||||
data: Dict[str, Any],
|
||||
) -> None:
|
||||
await gateway_openclaw_handlers.handle_get_openclaw_agents_presence(self, websocket, data)
|
||||
|
||||
async def _handle_get_openclaw_skills(
|
||||
self,
|
||||
websocket: ServerConnection,
|
||||
data: Dict[str, Any],
|
||||
) -> None:
|
||||
await gateway_openclaw_handlers.handle_get_openclaw_skills(self, websocket, data)
|
||||
|
||||
async def _handle_get_openclaw_models(
|
||||
self,
|
||||
websocket: ServerConnection,
|
||||
data: Dict[str, Any],
|
||||
) -> None:
|
||||
await gateway_openclaw_handlers.handle_get_openclaw_models(self, websocket, data)
|
||||
|
||||
async def _handle_get_openclaw_workspace_files(
|
||||
self,
|
||||
websocket: ServerConnection,
|
||||
data: Dict[str, Any],
|
||||
) -> None:
|
||||
await gateway_openclaw_handlers.handle_get_openclaw_workspace_files(self, websocket, data)
|
||||
|
||||
@staticmethod
|
||||
def _normalize_watchlist(raw_tickers: Any) -> List[str]:
|
||||
return gateway_runtime_support.normalize_watchlist(raw_tickers)
|
||||
|
||||
@staticmethod
|
||||
def _normalize_agent_workspace_filename(raw_name: Any) -> Optional[str]:
|
||||
return gateway_runtime_support.normalize_agent_workspace_filename(
|
||||
raw_name,
|
||||
allowlist=EDITABLE_AGENT_WORKSPACE_FILES,
|
||||
)
|
||||
|
||||
def _apply_runtime_config(
|
||||
self,
|
||||
runtime_config: Dict[str, Any],
|
||||
) -> Dict[str, Any]:
|
||||
return gateway_runtime_support.apply_runtime_config(self, runtime_config)
|
||||
|
||||
def _sync_runtime_state(self) -> None:
|
||||
gateway_runtime_support.sync_runtime_state(self)
|
||||
|
||||
def _schedule_watchlist_market_store_refresh(
|
||||
self,
|
||||
tickers: List[str],
|
||||
) -> None:
|
||||
gateway_cycle_support.schedule_watchlist_market_store_refresh(self, tickers)
|
||||
|
||||
async def _refresh_market_store_for_watchlist(
|
||||
self,
|
||||
tickers: List[str],
|
||||
) -> None:
|
||||
await gateway_cycle_support.refresh_market_store_for_watchlist(self, tickers)
|
||||
|
||||
async def broadcast(self, message: Dict[str, Any]):
|
||||
"""Broadcast message to all connected clients"""
|
||||
if not self.connected_clients:
|
||||
return
|
||||
|
||||
message_json = json.dumps(message, ensure_ascii=False, default=str)
|
||||
|
||||
async with self.lock:
|
||||
tasks = [
|
||||
self._send_to_client(client, message_json)
|
||||
for client in self.connected_clients.copy()
|
||||
]
|
||||
|
||||
if tasks:
|
||||
await asyncio.gather(*tasks, return_exceptions=True)
|
||||
|
||||
async def _send_to_client(
|
||||
self,
|
||||
client: ServerConnection,
|
||||
message: str,
|
||||
):
|
||||
try:
|
||||
await client.send(message)
|
||||
except websockets.ConnectionClosed:
|
||||
async with self.lock:
|
||||
self.connected_clients.discard(client)
|
||||
|
||||
async def _market_status_monitor(self):
|
||||
await gateway_cycle_support.market_status_monitor(self)
|
||||
|
||||
async def _update_and_broadcast_live_returns(self):
|
||||
await gateway_cycle_support.update_and_broadcast_live_returns(self)
|
||||
|
||||
async def on_strategy_trigger(self, date: str):
|
||||
await gateway_cycle_support.on_strategy_trigger(self, date)
|
||||
|
||||
async def on_heartbeat_trigger(self, date: str):
|
||||
await gateway_cycle_support.on_heartbeat_trigger(self, date)
|
||||
|
||||
async def _run_backtest_cycle(self, date: str, tickers: List[str]):
|
||||
await gateway_cycle_support.run_backtest_cycle(self, date, tickers)
|
||||
|
||||
async def _run_live_cycle(self, date: str, tickers: List[str]):
|
||||
await gateway_cycle_support.run_live_cycle(self, date, tickers)
|
||||
|
||||
async def _finalize_cycle(self, date: str):
|
||||
await gateway_cycle_support.finalize_cycle(self, date)
|
||||
|
||||
async def _get_market_caps(
|
||||
self,
|
||||
tickers: List[str],
|
||||
date: str,
|
||||
) -> Dict[str, float]:
|
||||
return await gateway_cycle_support.get_market_caps(self, tickers, date)
|
||||
|
||||
async def _broadcast_portfolio_updates(
|
||||
self,
|
||||
result: Dict[str, Any],
|
||||
prices: Dict[str, float],
|
||||
):
|
||||
await gateway_cycle_support.broadcast_portfolio_updates(self, result, prices)
|
||||
|
||||
def _save_cycle_results(
|
||||
self,
|
||||
result: Dict[str, Any],
|
||||
date: str,
|
||||
prices: Dict[str, float],
|
||||
settlement_result: Optional[Dict[str, Any]] = None,
|
||||
):
|
||||
gateway_cycle_support.save_cycle_results(
|
||||
self,
|
||||
result,
|
||||
date,
|
||||
prices,
|
||||
settlement_result,
|
||||
)
|
||||
|
||||
async def _run_backtest_dates(self, dates: List[str]):
|
||||
await gateway_cycle_support.run_backtest_dates(self, dates)
|
||||
|
||||
def _handle_backtest_exception(self, task: asyncio.Task):
|
||||
gateway_cycle_support.handle_backtest_exception(self, task)
|
||||
|
||||
def _handle_manual_cycle_exception(self, task: asyncio.Task):
|
||||
gateway_cycle_support.handle_manual_cycle_exception(self, task)
|
||||
|
||||
def set_backtest_dates(self, dates: List[str]):
|
||||
gateway_cycle_support.set_backtest_dates(self, dates)
|
||||
|
||||
def stop(self):
|
||||
gateway_cycle_support.stop_gateway(self)
|
||||
Reference in New Issue
Block a user