use std::fmt::Display;
use nautilus_model::{enums::OrderSide, position::Position};
use crate::{Returns, statistic::PortfolioStatistic};
#[repr(C)]
#[derive(Debug, Clone)]
#[cfg_attr(
feature = "python",
pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.analysis", from_py_object)
)]
#[cfg_attr(
feature = "python",
pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.analysis")
)]
pub struct LongRatio {
pub precision: usize,
}
impl LongRatio {
#[must_use]
pub fn new(precision: Option<usize>) -> Self {
Self {
precision: precision.unwrap_or(2),
}
}
}
impl Display for LongRatio {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Long Ratio")
}
}
impl PortfolioStatistic for LongRatio {
type Item = f64;
fn name(&self) -> String {
self.to_string()
}
fn calculate_from_positions(&self, positions: &[Position]) -> Option<Self::Item> {
if positions.is_empty() {
return None;
}
let long_count = positions
.iter()
.filter(|p| p.entry == OrderSide::Buy)
.count();
let value = long_count as f64 / positions.len() as f64;
let scale = 10f64.powi(self.precision as i32);
Some((value * scale).round() / scale)
}
fn calculate_from_returns(&self, _returns: &Returns) -> Option<Self::Item> {
None
}
fn calculate_from_realized_pnls(&self, _realized_pnls: &[f64]) -> Option<Self::Item> {
None
}
}
#[cfg(test)]
mod tests {
use ahash::{AHashMap, AHashSet};
use nautilus_core::{UnixNanos, approx_eq};
use nautilus_model::{
enums::{InstrumentClass, PositionSide},
identifiers::{
AccountId, ClientOrderId, PositionId,
stubs::{instrument_id_aud_usd_sim, strategy_id_ema_cross, trader_id},
},
stubs::TestDefault,
types::{Currency, Quantity},
};
use rstest::rstest;
use super::*;
fn create_closed_position(entry: OrderSide) -> Position {
Position {
events: Vec::new(),
trader_id: trader_id(),
strategy_id: strategy_id_ema_cross(),
instrument_id: instrument_id_aud_usd_sim(),
id: PositionId::new("test-position"),
account_id: AccountId::new("test-account"),
opening_order_id: ClientOrderId::test_default(),
closing_order_id: None,
entry,
side: PositionSide::Flat, signed_qty: 0.0,
quantity: Quantity::default(),
peak_qty: Quantity::default(),
price_precision: 2,
size_precision: 2,
multiplier: Quantity::default(),
is_inverse: false,
base_currency: None,
quote_currency: Currency::USD(),
settlement_currency: Currency::USD(),
ts_init: UnixNanos::default(),
ts_opened: UnixNanos::default(),
ts_last: UnixNanos::default(),
ts_closed: Some(UnixNanos::from(1)), duration_ns: 2,
avg_px_open: 0.0,
avg_px_close: Some(0.0),
realized_return: 0.0,
realized_pnl: None,
trade_ids: AHashSet::new(),
buy_qty: Quantity::default(),
sell_qty: Quantity::default(),
commissions: AHashMap::new(),
adjustments: Vec::new(),
instrument_class: InstrumentClass::Spot,
is_currency_pair: true,
}
}
#[rstest]
fn test_empty_positions() {
let long_ratio = LongRatio::new(None);
let result = long_ratio.calculate_from_positions(&[]);
assert!(result.is_none());
}
#[rstest]
fn test_all_long_positions() {
let long_ratio = LongRatio::new(None);
let positions = vec![
create_closed_position(OrderSide::Buy),
create_closed_position(OrderSide::Buy),
create_closed_position(OrderSide::Buy),
];
let result = long_ratio.calculate_from_positions(&positions);
assert!(result.is_some());
assert!(approx_eq!(f64, result.unwrap(), 1.00, epsilon = 1e-9));
}
#[rstest]
fn test_all_short_positions() {
let long_ratio = LongRatio::new(None);
let positions = vec![
create_closed_position(OrderSide::Sell),
create_closed_position(OrderSide::Sell),
create_closed_position(OrderSide::Sell),
];
let result = long_ratio.calculate_from_positions(&positions);
assert!(result.is_some());
assert!(approx_eq!(f64, result.unwrap(), 0.00, epsilon = 1e-9));
}
#[rstest]
fn test_mixed_positions() {
let long_ratio = LongRatio::new(None);
let positions = vec![
create_closed_position(OrderSide::Buy),
create_closed_position(OrderSide::Sell),
create_closed_position(OrderSide::Buy),
create_closed_position(OrderSide::Sell),
];
let result = long_ratio.calculate_from_positions(&positions);
assert!(result.is_some());
assert!(approx_eq!(f64, result.unwrap(), 0.50, epsilon = 1e-9));
}
#[rstest]
fn test_custom_precision() {
let long_ratio = LongRatio::new(Some(3));
let positions = vec![
create_closed_position(OrderSide::Buy),
create_closed_position(OrderSide::Buy),
create_closed_position(OrderSide::Sell),
];
let result = long_ratio.calculate_from_positions(&positions);
assert!(result.is_some());
assert!(approx_eq!(f64, result.unwrap(), 0.667, epsilon = 1e-9));
}
#[rstest]
fn test_single_position_long() {
let long_ratio = LongRatio::new(None);
let positions = vec![create_closed_position(OrderSide::Buy)];
let result = long_ratio.calculate_from_positions(&positions);
assert!(result.is_some());
assert!(approx_eq!(f64, result.unwrap(), 1.00, epsilon = 1e-9));
}
#[rstest]
fn test_single_position_short() {
let long_ratio = LongRatio::new(None);
let positions = vec![create_closed_position(OrderSide::Sell)];
let result = long_ratio.calculate_from_positions(&positions);
assert!(result.is_some());
assert!(approx_eq!(f64, result.unwrap(), 0.00, epsilon = 1e-9));
}
#[rstest]
fn test_zero_precision() {
let long_ratio = LongRatio::new(Some(0));
let positions = vec![
create_closed_position(OrderSide::Buy),
create_closed_position(OrderSide::Buy),
create_closed_position(OrderSide::Sell),
];
let result = long_ratio.calculate_from_positions(&positions);
assert!(result.is_some());
assert!(approx_eq!(f64, result.unwrap(), 1.00, epsilon = 1e-9));
}
#[rstest]
fn test_name() {
let long_ratio = LongRatio::new(None);
assert_eq!(long_ratio.name(), "Long Ratio");
}
}