Files
foxhunt/testing/integration/standalone/position_manager_fixes.rs
jgrusewski 9c3d741a08 refactor: restructure repo — crates/, bin/, testing/ layout
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>
2026-02-25 11:56:00 +01:00

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
}