fix(trading_agent): wire real position data into allocation calculations
Replaces hardcoded zeros in AssetAllocation with real position data queried from agent_orders. Adds fetch_current_positions() helper that derives net quantity per symbol (buy - sell) and reuses it in both allocate_portfolio and rebalance_portfolio to eliminate SQL duplication. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -269,6 +269,37 @@ impl TradingAgentServiceImpl {
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch current net positions from the `agent_orders` table.
|
||||
///
|
||||
/// Returns a map of symbol -> net quantity (buys positive, sells negative)
|
||||
/// derived from non-cancelled orders. Returns an empty map on DB error
|
||||
/// so callers degrade gracefully to zero-position assumptions.
|
||||
async fn fetch_current_positions(&self) -> HashMap<String, f64> {
|
||||
let rows: Result<Vec<(String, f64)>, _> = sqlx::query_as(
|
||||
r#"
|
||||
SELECT symbol, COALESCE(SUM(
|
||||
CASE WHEN side = 'Buy' THEN CAST(quantity AS DOUBLE PRECISION)
|
||||
WHEN side = 'Sell' THEN -CAST(quantity AS DOUBLE PRECISION)
|
||||
ELSE 0.0
|
||||
END
|
||||
), 0.0) as net_quantity
|
||||
FROM agent_orders
|
||||
WHERE status != 'CANCELLED'
|
||||
GROUP BY symbol
|
||||
"#,
|
||||
)
|
||||
.fetch_all(&self.db_pool)
|
||||
.await;
|
||||
|
||||
match rows {
|
||||
Ok(data) => data.into_iter().collect(),
|
||||
Err(e) => {
|
||||
warn!("Failed to fetch current positions (defaulting to empty): {}", e);
|
||||
HashMap::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert internal Instrument to proto
|
||||
fn convert_instrument(&self, inst: &crate::universe::Instrument) -> Instrument {
|
||||
Instrument {
|
||||
@@ -771,14 +802,24 @@ impl trading_agent_service_server::TradingAgentService for TradingAgentServiceIm
|
||||
Status::internal(format!("Allocation failed: {}", e))
|
||||
})?;
|
||||
|
||||
// 4. Convert to proto — compute target_quantity from last price
|
||||
// 4. Convert to proto -- compute target_quantity from last price,
|
||||
// and populate current position data from agent_orders.
|
||||
//
|
||||
// BLOCKER: current_weight and current_quantity require live position data from
|
||||
// a portfolio/position-tracking service (or a positions table). This service does
|
||||
// not currently have access to live position state. When a PositionService gRPC
|
||||
// client is added, these should be fetched per-symbol.
|
||||
// rebalance_delta = target_quantity - current_quantity (set to target_quantity
|
||||
// until current positions are available).
|
||||
// Current positions are derived from the agent_orders table (net buy - sell
|
||||
// quantities per symbol). Current weight is the symbol's share of total
|
||||
// portfolio exposure valued at last close prices. Falls back to zeros when
|
||||
// position or price data is unavailable.
|
||||
let current_positions = self.fetch_current_positions().await;
|
||||
|
||||
// Compute total portfolio value from current positions * last prices
|
||||
let total_current_value: f64 = current_positions
|
||||
.iter()
|
||||
.map(|(sym, qty)| {
|
||||
let price = symbol_last_prices.get(sym).copied().unwrap_or(0.0);
|
||||
qty.abs() * price
|
||||
})
|
||||
.sum();
|
||||
|
||||
let proto_allocations: Vec<AssetAllocation> = allocations
|
||||
.iter()
|
||||
.map(|(symbol, capital)| {
|
||||
@@ -796,17 +837,28 @@ impl trading_agent_service_server::TradingAgentService for TradingAgentServiceIm
|
||||
.map(|price| capital_f64 / price)
|
||||
.unwrap_or(0.0); // 0.0 if no price data available
|
||||
|
||||
// Current position from agent_orders (net quantity)
|
||||
let current_quantity = current_positions
|
||||
.get(symbol)
|
||||
.copied()
|
||||
.unwrap_or(0.0);
|
||||
|
||||
// Current weight: position value / total portfolio value
|
||||
let current_weight = if total_current_value > 0.0 {
|
||||
let price = symbol_last_prices.get(symbol).copied().unwrap_or(0.0);
|
||||
(current_quantity.abs() * price) / total_current_value
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
AssetAllocation {
|
||||
symbol: symbol.clone(),
|
||||
target_weight: weight,
|
||||
target_capital: capital_f64,
|
||||
target_quantity,
|
||||
// TODO(positions): Fetch from live position service / positions table.
|
||||
// Requires PositionService gRPC client or position-tracking DB query.
|
||||
current_weight: 0.0,
|
||||
current_quantity: 0.0,
|
||||
// Once current_quantity is available: target_quantity - current_quantity
|
||||
rebalance_delta: target_quantity,
|
||||
current_weight,
|
||||
current_quantity,
|
||||
rebalance_delta: target_quantity - current_quantity,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
@@ -1057,25 +1109,8 @@ impl trading_agent_service_server::TradingAgentService for TradingAgentServiceIm
|
||||
.collect();
|
||||
|
||||
// 3. Load current positions from agent_orders to derive current weights
|
||||
let current_rows: Vec<(String, f64)> = sqlx::query_as(
|
||||
r#"
|
||||
SELECT symbol, COALESCE(SUM(
|
||||
CASE WHEN side = 'Buy' THEN CAST(quantity AS DOUBLE PRECISION)
|
||||
WHEN side = 'Sell' THEN -CAST(quantity AS DOUBLE PRECISION)
|
||||
ELSE 0.0
|
||||
END
|
||||
), 0.0) as net_quantity
|
||||
FROM agent_orders
|
||||
WHERE status != 'CANCELLED'
|
||||
GROUP BY symbol
|
||||
"#,
|
||||
)
|
||||
.fetch_all(&self.db_pool)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("Failed to query current positions (non-fatal): {}", e);
|
||||
Status::internal(format!("Failed to query positions: {e}"))
|
||||
})?;
|
||||
let current_positions = self.fetch_current_positions().await;
|
||||
let current_rows: Vec<(String, f64)> = current_positions.into_iter().collect();
|
||||
|
||||
let total_current: f64 = current_rows.iter().map(|(_, q)| q.abs()).sum();
|
||||
let current_weights: HashMap<String, f64> = if total_current > 0.0 {
|
||||
|
||||
Reference in New Issue
Block a user