fix: replace hardcoded mocks with real data in trading service and web gateway
- WebSocket stream bridges: replace permanent pending() stalls with actual gRPC streaming subscriptions and exponential backoff reconnection - Risk metrics: replace fake sharpe_ratio=1.5, sortino_ratio=2.0 with 0.0 (not-yet-computed) and wire circuit breaker status to real kill switch - ML orders: use actual ensemble prediction direction and confidence instead of hardcoded BUY at 0.65 confidence - Fallback signal: return Err instead of fabricated Hold at 0.60 confidence to prevent accidental trades when ML pipeline is disconnected Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -579,8 +579,8 @@ impl EnsembleCoordinator {
|
||||
|
||||
for symbol in &symbols {
|
||||
match self.generate_and_save_prediction(symbol).await {
|
||||
Ok(prediction_id) => {
|
||||
debug!("Generated prediction {} for {}", prediction_id, symbol);
|
||||
Ok(prediction) => {
|
||||
debug!("Generated prediction {} for {}", prediction.id, symbol);
|
||||
},
|
||||
Err(e) => {
|
||||
warn!("Failed to generate prediction for {}: {}", symbol, e);
|
||||
@@ -590,8 +590,15 @@ impl EnsembleCoordinator {
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate prediction and save to database
|
||||
pub async fn generate_and_save_prediction(&self, symbol: &str) -> Result<Uuid> {
|
||||
/// Generate prediction and save to database.
|
||||
///
|
||||
/// Returns the saved prediction record which contains the database `id`,
|
||||
/// `ensemble_action` (BUY/SELL/HOLD), `ensemble_confidence`, and all
|
||||
/// per-model vote details.
|
||||
pub async fn generate_and_save_prediction(
|
||||
&self,
|
||||
symbol: &str,
|
||||
) -> Result<EnsemblePrediction> {
|
||||
// 1. Fetch latest market features (stub - will use real feature cache)
|
||||
let features = self.fetch_features_for_symbol(symbol).await?;
|
||||
|
||||
@@ -602,16 +609,17 @@ impl EnsembleCoordinator {
|
||||
.map_err(|e| anyhow::anyhow!("Ensemble prediction failed: {}", e))?;
|
||||
|
||||
// 3. Convert to database record
|
||||
let prediction = EnsemblePrediction::from_decision(
|
||||
let mut prediction = EnsemblePrediction::from_decision(
|
||||
&decision,
|
||||
symbol.to_string(),
|
||||
Some("paper_trading_001".to_string()),
|
||||
);
|
||||
|
||||
// 4. Save to database
|
||||
// 4. Save to database (updates the id to the DB-assigned value)
|
||||
let prediction_id = self.save_prediction_to_db(&prediction).await?;
|
||||
prediction.id = prediction_id;
|
||||
|
||||
Ok(prediction_id)
|
||||
Ok(prediction)
|
||||
}
|
||||
|
||||
/// Feed a market data update (price tick) into the per-symbol feature extractor.
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
//! Risk management service implementation
|
||||
|
||||
use crate::proto::risk::{
|
||||
risk_service_server::RiskService, EmergencyStopRequest, EmergencyStopResponse,
|
||||
GetCircuitBreakerStatusRequest, GetCircuitBreakerStatusResponse, GetPositionRiskRequest,
|
||||
GetPositionRiskResponse, GetRiskMetricsRequest, GetRiskMetricsResponse, GetVaRRequest,
|
||||
GetVaRResponse, RiskAlertSeverity, RiskLevel, RiskMetrics, RiskScore, RiskViolation,
|
||||
RiskViolationType, StreamRiskAlertsRequest, StreamVaRRequest, SymbolVaR, VaRMethod,
|
||||
ValidateOrderRequest, ValidateOrderResponse,
|
||||
risk_service_server::RiskService, CircuitBreakerStatus, CircuitBreakerType,
|
||||
EmergencyStopRequest, EmergencyStopResponse, GetCircuitBreakerStatusRequest,
|
||||
GetCircuitBreakerStatusResponse, GetPositionRiskRequest, GetPositionRiskResponse,
|
||||
GetRiskMetricsRequest, GetRiskMetricsResponse, GetVaRRequest, GetVaRResponse,
|
||||
RiskAlertSeverity, RiskLevel, RiskMetrics, RiskScore, RiskViolation, RiskViolationType,
|
||||
StreamRiskAlertsRequest, StreamVaRRequest, SymbolVaR, VaRMethod, ValidateOrderRequest,
|
||||
ValidateOrderResponse,
|
||||
};
|
||||
use crate::repositories::ConfigRepository;
|
||||
use crate::state::TradingServiceState;
|
||||
@@ -151,16 +152,19 @@ impl RiskService for RiskServiceImpl {
|
||||
portfolio_var_5d,
|
||||
portfolio_var_30d,
|
||||
max_drawdown,
|
||||
// TODO: Compute current_drawdown from P&L history via trading_repository
|
||||
// Not yet computed: requires P&L history from trading_repository
|
||||
current_drawdown: 0.0,
|
||||
// TODO: Compute Sharpe, Sortino, beta, alpha from returns via trading_repository
|
||||
sharpe_ratio: 1.5,
|
||||
sortino_ratio: 2.0,
|
||||
beta: 1.0,
|
||||
alpha: 0.05,
|
||||
// TODO: Derive volatility from recent return series via market_data_repository
|
||||
volatility: 0.20,
|
||||
// TODO: Populate position_risks from position_manager
|
||||
// Not yet computed: requires return series from trading_repository
|
||||
sharpe_ratio: 0.0,
|
||||
// Not yet computed: requires downside return series from trading_repository
|
||||
sortino_ratio: 0.0,
|
||||
// Not yet computed: requires benchmark correlation from market_data_repository
|
||||
beta: 0.0,
|
||||
// Not yet computed: requires benchmark-relative returns from market_data_repository
|
||||
alpha: 0.0,
|
||||
// Not yet computed: requires recent return series from market_data_repository
|
||||
volatility: 0.0,
|
||||
// Not yet computed: requires position data from position_manager
|
||||
position_risks: vec![],
|
||||
};
|
||||
|
||||
@@ -176,10 +180,11 @@ impl RiskService for RiskServiceImpl {
|
||||
) -> Result<Response<GetPositionRiskResponse>, Status> {
|
||||
let _req = request.into_inner();
|
||||
|
||||
// Placeholder implementation - return empty position risks for now
|
||||
// Not yet computed: requires position data from position_manager
|
||||
Ok(Response::new(GetPositionRiskResponse {
|
||||
position_risks: vec![],
|
||||
portfolio_risk_score: 5.0, // Placeholder score out of 10
|
||||
// 0.0 = not yet computed (requires aggregated position risk data)
|
||||
portfolio_risk_score: 0.0,
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -313,9 +318,46 @@ impl RiskService for RiskServiceImpl {
|
||||
&self,
|
||||
_request: Request<GetCircuitBreakerStatusRequest>,
|
||||
) -> Result<Response<GetCircuitBreakerStatusResponse>, Status> {
|
||||
// Placeholder implementation - return empty circuit breakers
|
||||
let mut circuit_breakers = Vec::new();
|
||||
|
||||
// Report kill switch status as the global emergency circuit breaker
|
||||
if let Some(kill_switch) = self.state.kill_switch_system.as_ref() {
|
||||
match kill_switch.get_status().await {
|
||||
Ok(status) => {
|
||||
circuit_breakers.push(CircuitBreakerStatus {
|
||||
name: "global_kill_switch".to_string(),
|
||||
is_triggered: status.is_active || status.is_emergency_active,
|
||||
trigger_reason: if status.is_emergency_active {
|
||||
Some("Emergency shutdown activated".to_string())
|
||||
} else if status.is_active {
|
||||
Some("Kill switch active".to_string())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
triggered_at: None, // Kill switch does not expose activation timestamp
|
||||
reset_at: None,
|
||||
breaker_type: CircuitBreakerType::PortfolioLoss as i32,
|
||||
});
|
||||
},
|
||||
Err(e) => {
|
||||
warn!("Failed to query kill switch status: {}", e);
|
||||
// Report the breaker as unknown/untriggered rather than hiding it
|
||||
circuit_breakers.push(CircuitBreakerStatus {
|
||||
name: "global_kill_switch".to_string(),
|
||||
is_triggered: false,
|
||||
trigger_reason: Some(format!("Status unavailable: {}", e)),
|
||||
triggered_at: None,
|
||||
reset_at: None,
|
||||
breaker_type: CircuitBreakerType::PortfolioLoss as i32,
|
||||
});
|
||||
},
|
||||
}
|
||||
}
|
||||
// If kill_switch_system is None, circuit_breakers remains empty —
|
||||
// the kill switch is not configured, so there is nothing to report.
|
||||
|
||||
Ok(Response::new(GetCircuitBreakerStatusResponse {
|
||||
circuit_breakers: vec![],
|
||||
circuit_breakers,
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
@@ -695,11 +695,10 @@ impl trading_service_server::TradingService for TradingServiceImpl {
|
||||
.generate_and_save_prediction(&req.symbol)
|
||||
.await
|
||||
{
|
||||
Ok(prediction_id_uuid) => {
|
||||
// For now, use placeholder values until we retrieve the saved prediction
|
||||
let confidence = 0.65; // Default confidence above threshold
|
||||
let action = "BUY".to_string(); // Default action
|
||||
let prediction_id = prediction_id_uuid.to_string();
|
||||
Ok(prediction) => {
|
||||
let confidence = prediction.ensemble_confidence;
|
||||
let action = prediction.ensemble_action.clone();
|
||||
let prediction_id = prediction.id.to_string();
|
||||
|
||||
// Check confidence threshold (60%)
|
||||
if confidence < 0.60 {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
//! This module provides clean repository-based dependency injection,
|
||||
//! eliminating direct database coupling from business logic.
|
||||
|
||||
use crate::error::TradingServiceResult;
|
||||
use crate::error::{TradingServiceError, TradingServiceResult};
|
||||
use crate::event_persistence::EventPersistence;
|
||||
use crate::event_streaming::publisher::EventPublisher;
|
||||
use crate::proto::monitoring::SystemMetrics;
|
||||
@@ -373,7 +373,8 @@ impl TradingServiceState {
|
||||
/// 4. Apply risk adjustments (position sizing)
|
||||
/// 5. Return signal with ensemble attribution
|
||||
///
|
||||
/// Fallback: If ensemble fails, falls back to single model (DQN epoch 30)
|
||||
/// Returns `Err(MLModel)` if no real prediction is available (ensemble not
|
||||
/// initialised, feature extraction fails, or inference fails).
|
||||
pub async fn get_ensemble_trading_signal(
|
||||
&self,
|
||||
symbol: &str,
|
||||
@@ -581,28 +582,30 @@ impl TradingServiceState {
|
||||
Ok(position_size.max(10)) // Minimum 10 shares/contracts
|
||||
}
|
||||
|
||||
/// Fallback trading signal using single model (DQN epoch 30)
|
||||
/// Return an error when no ML prediction is available.
|
||||
///
|
||||
/// Previously this returned a hardcoded Hold signal at 0.60 confidence,
|
||||
/// which looked like a legitimate medium-confidence prediction and could
|
||||
/// trigger trades. Now it returns an explicit error so callers are forced
|
||||
/// to handle the absence of a real prediction (e.g. the gRPC layer returns
|
||||
/// `Status::unavailable` or `Status::internal`).
|
||||
async fn get_fallback_trading_signal(
|
||||
&self,
|
||||
symbol: &str,
|
||||
) -> TradingServiceResult<EnsembleTradingSignal> {
|
||||
tracing::warn!(
|
||||
"FALLBACK: using mock trading signal for {} — \
|
||||
ensemble coordinator unavailable or feature extraction failed. \
|
||||
TODO: wire real DQN epoch-30 checkpoint inference here.",
|
||||
symbol
|
||||
symbol = %symbol,
|
||||
"No ML prediction available: ensemble coordinator unavailable or \
|
||||
feature extraction / inference failed. Refusing to fabricate a \
|
||||
trading signal."
|
||||
);
|
||||
|
||||
// TODO: load DQN epoch-30 checkpoint and run actual inference instead of
|
||||
// returning a hard-coded Hold signal.
|
||||
Ok(EnsembleTradingSignal {
|
||||
symbol: symbol.to_string(),
|
||||
action: TradingActionType::Hold,
|
||||
confidence: 0.60,
|
||||
position_size: 50, // Conservative size for fallback
|
||||
disagreement_rate: 0.0,
|
||||
model_votes: std::collections::HashMap::new(),
|
||||
timestamp: std::time::SystemTime::now(),
|
||||
Err(TradingServiceError::MLModel {
|
||||
model_name: "ensemble".to_string(),
|
||||
message: format!(
|
||||
"No ML prediction available for {symbol}: ensemble coordinator \
|
||||
unavailable or inference failed"
|
||||
),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -206,7 +206,8 @@ async fn test_e2e_ml_to_paper_trade(pool: PgPool) -> Result<()> {
|
||||
let executor = Arc::new(PaperTradingExecutor::new(pool.clone(), config));
|
||||
|
||||
// Step 1: Generate and save prediction
|
||||
let prediction_id = coordinator.generate_and_save_prediction("ES.FUT").await?;
|
||||
let prediction = coordinator.generate_and_save_prediction("ES.FUT").await?;
|
||||
let prediction_id = prediction.id;
|
||||
|
||||
// Step 2: Paper trading executor processes prediction
|
||||
let processed_count = executor.execute_cycle().await?;
|
||||
|
||||
Reference in New Issue
Block a user