Move 17 library crates into crates/, CLI binary into bin/fxt, consolidate 10 test crates into testing/, split config crate from deployment config files. Root directory reduced from 38+ to ~17 directories. All Cargo.toml paths and build.rs proto refs updated. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
251 lines
9.6 KiB
Rust
251 lines
9.6 KiB
Rust
//! Position Manager fixes for Price/Decimal conflicts
|
|
//!
|
|
//! Key fixes needed:
|
|
//! 1. Replace .value() calls on Decimal with direct usage
|
|
//! 2. Fix Position field access (avg_cost -> avg_price)
|
|
//! 3. Handle Volume::new() Result return type
|
|
//! 4. Add proper type conversions
|
|
|
|
use std::collections::HashMap;
|
|
use std::sync::Arc;
|
|
use tokio::sync::RwLock;
|
|
use tracing::{debug, info, warn};
|
|
|
|
use crate::trading_operations::ExecutionResult;
|
|
use crate::types::prelude::*;
|
|
use rust_decimal::Decimal;
|
|
|
|
/// Position Manager for tracking and managing positions
|
|
#[derive(Debug)]
|
|
pub struct PositionManager {
|
|
/// Current positions by symbol
|
|
positions: Arc<RwLock<HashMap<String, Position>>>,
|
|
}
|
|
|
|
impl PositionManager {
|
|
/// Create a new position manager
|
|
pub fn new() -> Self {
|
|
Self {
|
|
positions: Arc::new(RwLock::new(HashMap::new())),
|
|
}
|
|
}
|
|
|
|
/// Update position based on execution
|
|
pub async fn update_position(&self, execution: &ExecutionResult) -> Result<(), String> {
|
|
let mut positions = self.positions.write().await;
|
|
|
|
let position = positions
|
|
.entry(execution.symbol.clone())
|
|
.or_insert_with(|| {
|
|
let now = chrono::Utc::now();
|
|
Position {
|
|
id: uuid::Uuid::new_v4(),
|
|
symbol: execution.symbol.clone(),
|
|
quantity: Decimal::ZERO,
|
|
avg_price: Decimal::ZERO,
|
|
avg_cost: Decimal::ZERO,
|
|
basis: Decimal::ZERO,
|
|
average_price: Decimal::ZERO,
|
|
market_value: Decimal::ZERO,
|
|
unrealized_pnl: Decimal::ZERO,
|
|
realized_pnl: Decimal::ZERO,
|
|
created_at: now,
|
|
updated_at: now,
|
|
last_updated: now,
|
|
current_price: None,
|
|
notional_value: Decimal::ZERO,
|
|
margin_requirement: Decimal::ZERO,
|
|
}
|
|
});
|
|
|
|
// CRITICAL FIX: Don't call .value() on Decimal - use directly
|
|
let is_buy = execution.executed_quantity > Decimal::ZERO;
|
|
let old_quantity = position.quantity; // Already Decimal
|
|
let old_cost = position.avg_price; // FIX: Use avg_price not avg_cost
|
|
|
|
if is_buy {
|
|
if position.quantity >= Decimal::ZERO {
|
|
// Same direction - calculate new average cost
|
|
let old_qty_decimal = old_quantity;
|
|
let old_cost_decimal = old_cost; // Already Decimal
|
|
let exec_qty_decimal = execution.executed_quantity;
|
|
let exec_price_decimal = execution.execution_price;
|
|
|
|
let total_cost = old_qty_decimal * old_cost_decimal + exec_qty_decimal * exec_price_decimal;
|
|
let new_quantity_decimal = old_qty_decimal + exec_qty_decimal;
|
|
|
|
position.quantity = new_quantity_decimal;
|
|
position.avg_price = if new_quantity_decimal > Decimal::ZERO {
|
|
total_cost / new_quantity_decimal
|
|
} else {
|
|
Decimal::ZERO
|
|
};
|
|
// Keep other fields in sync
|
|
position.avg_cost = position.avg_price;
|
|
position.average_price = position.avg_price;
|
|
} else {
|
|
// Reducing short position
|
|
let exec_qty_decimal = execution.executed_quantity;
|
|
let exec_price_decimal = execution.execution_price;
|
|
let old_qty_decimal = old_quantity;
|
|
// FIX: old_cost is already Decimal, no conversion needed
|
|
let old_cost_decimal = old_cost;
|
|
|
|
let reduction = exec_qty_decimal.min(old_qty_decimal.abs());
|
|
let realized_pnl = reduction * (old_cost_decimal - exec_price_decimal);
|
|
position.realized_pnl = position.realized_pnl + realized_pnl;
|
|
|
|
let new_quantity = old_qty_decimal + reduction;
|
|
position.quantity = new_quantity;
|
|
|
|
if new_quantity > Decimal::ZERO {
|
|
position.avg_price = exec_price_decimal;
|
|
position.avg_cost = exec_price_decimal;
|
|
position.average_price = exec_price_decimal;
|
|
}
|
|
}
|
|
} else {
|
|
// Decreasing position (sell)
|
|
let exec_qty_decimal = execution.executed_quantity;
|
|
let exec_price_decimal = execution.execution_price;
|
|
// FIX: Don't call .value() on Decimal
|
|
let old_qty_decimal = old_quantity;
|
|
let old_cost_decimal = old_cost; // Already Decimal
|
|
|
|
if old_qty_decimal > Decimal::ZERO {
|
|
// Reducing long position
|
|
let reduction = exec_qty_decimal.min(old_qty_decimal);
|
|
let realized_pnl = reduction * (exec_price_decimal - old_cost_decimal);
|
|
position.realized_pnl = position.realized_pnl + realized_pnl;
|
|
|
|
let new_quantity_decimal = old_qty_decimal - reduction;
|
|
position.quantity = new_quantity_decimal;
|
|
|
|
if new_quantity_decimal < Decimal::ZERO {
|
|
position.avg_price = exec_price_decimal;
|
|
position.avg_cost = exec_price_decimal;
|
|
position.average_price = exec_price_decimal;
|
|
}
|
|
} else {
|
|
// Increasing short position
|
|
let total_cost = old_qty_decimal.abs() * old_cost_decimal + exec_qty_decimal * exec_price_decimal;
|
|
let new_quantity_decimal = old_qty_decimal - exec_qty_decimal;
|
|
|
|
position.quantity = new_quantity_decimal;
|
|
if new_quantity_decimal < Decimal::ZERO {
|
|
let new_avg_price = total_cost / new_quantity_decimal.abs();
|
|
position.avg_price = new_avg_price;
|
|
position.avg_cost = new_avg_price;
|
|
position.average_price = new_avg_price;
|
|
} else {
|
|
position.avg_price = Decimal::ZERO;
|
|
position.avg_cost = Decimal::ZERO;
|
|
position.average_price = Decimal::ZERO;
|
|
}
|
|
}
|
|
}
|
|
position.last_updated = chrono::Utc::now();
|
|
position.updated_at = chrono::Utc::now();
|
|
|
|
info!(
|
|
"Position updated for {}: {} @ {} (realized P&L: {})",
|
|
execution.symbol, position.quantity, position.avg_price, position.realized_pnl
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Update market values based on current market prices
|
|
pub async fn update_market_values(
|
|
&self,
|
|
market_prices: HashMap<String, Decimal>,
|
|
) -> Result<(), String> {
|
|
let mut positions = self.positions.write().await;
|
|
|
|
for (symbol, market_price) in market_prices {
|
|
if let Some(position) = positions.get_mut(&symbol) {
|
|
let qty_decimal = position.quantity;
|
|
// FIX: avg_cost is already Decimal
|
|
let avg_cost_decimal = position.avg_price; // Use avg_price
|
|
|
|
// Calculate market value
|
|
let market_value_decimal = qty_decimal * market_price;
|
|
position.market_value = market_value_decimal;
|
|
|
|
// Calculate unrealized P&L
|
|
if qty_decimal != Decimal::ZERO {
|
|
let unrealized_pnl = if qty_decimal > Decimal::ZERO {
|
|
// Long position
|
|
qty_decimal * (market_price - avg_cost_decimal)
|
|
} else {
|
|
// Short position
|
|
qty_decimal.abs() * (avg_cost_decimal - market_price)
|
|
};
|
|
position.unrealized_pnl = unrealized_pnl;
|
|
} else {
|
|
position.unrealized_pnl = Decimal::ZERO;
|
|
}
|
|
|
|
position.last_updated = chrono::Utc::now();
|
|
position.updated_at = chrono::Utc::now();
|
|
|
|
debug!(
|
|
"Updated market value for {}: {} (unrealized P&L: {})",
|
|
symbol, position.market_value, position.unrealized_pnl
|
|
);
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Get total portfolio value
|
|
pub async fn get_total_portfolio_value(&self) -> Decimal {
|
|
let positions = self.positions.read().await;
|
|
|
|
positions
|
|
.values()
|
|
.map(|pos| pos.market_value) // Already Decimal, no conversion needed
|
|
.sum::<Decimal>()
|
|
}
|
|
|
|
/// Get positions that exceed risk limits
|
|
pub async fn get_positions_exceeding_limits(
|
|
&self,
|
|
max_position_value: Decimal,
|
|
) -> Vec<Position> {
|
|
let positions = self.positions.read().await;
|
|
|
|
positions
|
|
.values()
|
|
.filter(|pos| pos.market_value.abs() > max_position_value) // Already Decimal
|
|
.cloned()
|
|
.collect()
|
|
}
|
|
|
|
/// Calculate position concentration risk
|
|
pub async fn calculate_concentration_risk(&self) -> HashMap<String, f64> {
|
|
let positions = self.positions.read().await;
|
|
let total_value = positions
|
|
.values()
|
|
.map(|pos| pos.market_value.abs()) // Already Decimal
|
|
.sum::<Decimal>();
|
|
|
|
if total_value == Decimal::ZERO {
|
|
return HashMap::new();
|
|
}
|
|
|
|
positions
|
|
.iter()
|
|
.map(|(symbol, position)| {
|
|
let concentration = (position.market_value.abs() / total_value)
|
|
.to_f64()
|
|
.unwrap_or(0.0)
|
|
* 100.0;
|
|
(symbol.clone(), concentration)
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
// ... rest of methods remain the same
|
|
} |