#![allow(dead_code)]
use std::{
fmt::Display,
hash::{Hash, Hasher},
ops::{Deref, DerefMut},
};
use ahash::AHashMap;
use nautilus_core::correctness::{FAILED, check_positive_decimal};
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use crate::{
accounts::{
Account,
base::BaseAccount,
margin_model::{MarginModel, MarginModelAny},
},
enums::{AccountType, InstrumentClass, LiquiditySide, OrderSide},
events::{AccountState, OrderFilled},
identifiers::{AccountId, InstrumentId},
instruments::{Instrument, InstrumentAny},
position::Position,
types::{AccountBalance, Currency, MarginBalance, Money, Price, Quantity, money::MoneyRaw},
};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(
feature = "python",
pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model", from_py_object)
)]
#[cfg_attr(
feature = "python",
pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.model")
)]
pub struct MarginAccount {
pub base: BaseAccount,
pub leverages: AHashMap<InstrumentId, Decimal>,
pub margins: AHashMap<InstrumentId, MarginBalance>,
pub default_leverage: Decimal,
#[serde(skip, default = "MarginModelAny::default")]
margin_model: MarginModelAny,
}
impl MarginAccount {
pub fn new(event: AccountState, calculate_account_state: bool) -> Self {
let margins = event
.margins
.iter()
.map(|margin| (margin.instrument_id, *margin))
.collect();
Self {
base: BaseAccount::new(event, calculate_account_state),
leverages: AHashMap::new(),
margins,
default_leverage: Decimal::ONE,
margin_model: MarginModelAny::default(),
}
}
pub fn set_margin_model(&mut self, model: MarginModelAny) {
self.margin_model = model;
}
#[must_use]
pub const fn margin_model(&self) -> &MarginModelAny {
&self.margin_model
}
pub fn set_default_leverage(&mut self, leverage: Decimal) {
check_positive_decimal(leverage, "leverage").expect(FAILED);
self.default_leverage = leverage;
}
pub fn set_leverage(&mut self, instrument_id: InstrumentId, leverage: Decimal) {
check_positive_decimal(leverage, "leverage").expect(FAILED);
self.leverages.insert(instrument_id, leverage);
}
#[must_use]
pub fn get_leverage(&self, instrument_id: &InstrumentId) -> Decimal {
*self
.leverages
.get(instrument_id)
.unwrap_or(&self.default_leverage)
}
#[must_use]
pub fn is_unleveraged(&self, instrument_id: InstrumentId) -> bool {
self.get_leverage(&instrument_id) == Decimal::ONE
}
#[must_use]
pub fn is_cash_account(&self) -> bool {
self.account_type == AccountType::Cash
}
#[must_use]
pub fn is_margin_account(&self) -> bool {
self.account_type == AccountType::Margin
}
#[must_use]
pub fn initial_margins(&self) -> AHashMap<InstrumentId, Money> {
let mut initial_margins: AHashMap<InstrumentId, Money> = AHashMap::new();
self.margins.values().for_each(|margin_balance| {
initial_margins.insert(margin_balance.instrument_id, margin_balance.initial);
});
initial_margins
}
#[must_use]
pub fn maintenance_margins(&self) -> AHashMap<InstrumentId, Money> {
let mut maintenance_margins: AHashMap<InstrumentId, Money> = AHashMap::new();
self.margins.values().for_each(|margin_balance| {
maintenance_margins.insert(margin_balance.instrument_id, margin_balance.maintenance);
});
maintenance_margins
}
pub fn update_initial_margin(&mut self, instrument_id: InstrumentId, margin_init: Money) {
let margin_balance = self.margins.get(&instrument_id);
if let Some(balance) = margin_balance {
let mut new_margin_balance = *balance;
new_margin_balance.initial = margin_init;
self.margins.insert(instrument_id, new_margin_balance);
} else {
self.margins.insert(
instrument_id,
MarginBalance::new(
margin_init,
Money::new(0.0, margin_init.currency),
instrument_id,
),
);
}
self.recalculate_balance(margin_init.currency);
}
#[must_use]
pub fn initial_margin(&self, instrument_id: InstrumentId) -> Money {
let margin_balance = self.margins.get(&instrument_id);
assert!(
margin_balance.is_some(),
"Cannot get margin_init when no margin_balance"
);
margin_balance.unwrap().initial
}
pub fn update_maintenance_margin(
&mut self,
instrument_id: InstrumentId,
margin_maintenance: Money,
) {
let margin_balance = self.margins.get(&instrument_id);
if let Some(balance) = margin_balance {
let mut new_margin_balance = *balance;
new_margin_balance.maintenance = margin_maintenance;
self.margins.insert(instrument_id, new_margin_balance);
} else {
self.margins.insert(
instrument_id,
MarginBalance::new(
Money::new(0.0, margin_maintenance.currency),
margin_maintenance,
instrument_id,
),
);
}
self.recalculate_balance(margin_maintenance.currency);
}
#[must_use]
pub fn maintenance_margin(&self, instrument_id: InstrumentId) -> Money {
let margin_balance = self.margins.get(&instrument_id);
assert!(
margin_balance.is_some(),
"Cannot get maintenance_margin when no margin_balance"
);
margin_balance.unwrap().maintenance
}
#[must_use]
pub fn margin(&self, instrument_id: &InstrumentId) -> Option<MarginBalance> {
self.margins.get(instrument_id).copied()
}
pub fn update_margin(&mut self, margin_balance: MarginBalance) {
self.margins
.insert(margin_balance.instrument_id, margin_balance);
self.recalculate_balance(margin_balance.currency);
}
pub fn clear_margin(&mut self, instrument_id: InstrumentId) {
if let Some(margin_balance) = self.margins.remove(&instrument_id) {
self.recalculate_balance(margin_balance.currency);
}
}
pub fn calculate_initial_margin<T: Instrument>(
&mut self,
instrument: &T,
quantity: Quantity,
price: Price,
use_quote_for_inverse: Option<bool>,
) -> anyhow::Result<Money> {
let leverage = self.get_leverage(&instrument.id());
self.margin_model.calculate_initial_margin(
instrument,
quantity,
price,
leverage,
use_quote_for_inverse,
)
}
pub fn calculate_maintenance_margin<T: Instrument>(
&mut self,
instrument: &T,
quantity: Quantity,
price: Price,
use_quote_for_inverse: Option<bool>,
) -> anyhow::Result<Money> {
let leverage = self.get_leverage(&instrument.id());
self.margin_model.calculate_maintenance_margin(
instrument,
quantity,
price,
leverage,
use_quote_for_inverse,
)
}
pub fn recalculate_balance(&mut self, currency: Currency) {
let current_balance = match self.balances.get(¤cy) {
Some(balance) => *balance,
None => {
let zero = Money::from_raw(0, currency);
AccountBalance::new(zero, zero, zero)
}
};
let mut total_margin: MoneyRaw = 0;
for margin in self.margins.values() {
if margin.currency == currency {
total_margin = total_margin
.checked_add(margin.initial.raw)
.and_then(|sum| sum.checked_add(margin.maintenance.raw))
.unwrap_or_else(|| {
panic!(
"Margin calculation overflow for currency {}: total would exceed maximum",
currency.code
)
});
}
}
let total_free = if total_margin > current_balance.total.raw {
total_margin = current_balance.total.raw.max(0);
current_balance.total.raw - total_margin
} else {
current_balance.total.raw - total_margin
};
let new_balance = AccountBalance::new(
current_balance.total,
Money::from_raw(total_margin, currency),
Money::from_raw(total_free, currency),
);
self.balances.insert(currency, new_balance);
}
}
impl Deref for MarginAccount {
type Target = BaseAccount;
fn deref(&self) -> &Self::Target {
&self.base
}
}
impl DerefMut for MarginAccount {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.base
}
}
impl Account for MarginAccount {
fn id(&self) -> AccountId {
self.id
}
fn account_type(&self) -> AccountType {
self.account_type
}
fn base_currency(&self) -> Option<Currency> {
self.base_currency
}
fn is_cash_account(&self) -> bool {
self.account_type == AccountType::Cash
}
fn is_margin_account(&self) -> bool {
self.account_type == AccountType::Margin
}
fn calculated_account_state(&self) -> bool {
false }
fn balance_total(&self, currency: Option<Currency>) -> Option<Money> {
self.base_balance_total(currency)
}
fn balances_total(&self) -> AHashMap<Currency, Money> {
self.base_balances_total()
}
fn balance_free(&self, currency: Option<Currency>) -> Option<Money> {
self.base_balance_free(currency)
}
fn balances_free(&self) -> AHashMap<Currency, Money> {
self.base_balances_free()
}
fn balance_locked(&self, currency: Option<Currency>) -> Option<Money> {
self.base_balance_locked(currency)
}
fn balances_locked(&self) -> AHashMap<Currency, Money> {
self.base_balances_locked()
}
fn balance(&self, currency: Option<Currency>) -> Option<&AccountBalance> {
self.base_balance(currency)
}
fn last_event(&self) -> Option<AccountState> {
self.base_last_event()
}
fn events(&self) -> Vec<AccountState> {
self.events.clone()
}
fn event_count(&self) -> usize {
self.events.len()
}
fn currencies(&self) -> Vec<Currency> {
self.balances.keys().copied().collect()
}
fn starting_balances(&self) -> AHashMap<Currency, Money> {
self.balances_starting.clone()
}
fn balances(&self) -> AHashMap<Currency, AccountBalance> {
self.balances.clone()
}
fn apply(&mut self, event: AccountState) -> anyhow::Result<()> {
let margins = event
.margins
.iter()
.map(|margin| (margin.instrument_id, *margin))
.collect();
self.base_apply(event);
self.margins = margins;
Ok(())
}
fn purge_account_events(&mut self, ts_now: nautilus_core::UnixNanos, lookback_secs: u64) {
self.base.base_purge_account_events(ts_now, lookback_secs);
}
fn calculate_balance_locked(
&mut self,
instrument: &InstrumentAny,
side: OrderSide,
quantity: Quantity,
price: Price,
use_quote_for_inverse: Option<bool>,
) -> anyhow::Result<Money> {
self.base_calculate_balance_locked(instrument, side, quantity, price, use_quote_for_inverse)
}
fn calculate_pnls(
&self,
instrument: &InstrumentAny,
fill: &OrderFilled,
position: Option<Position>,
) -> anyhow::Result<Vec<Money>> {
let mut pnls: Vec<Money> = Vec::new();
let instrument_class = instrument.instrument_class();
if matches!(
instrument_class,
InstrumentClass::Option
| InstrumentClass::OptionSpread
| InstrumentClass::BinaryOption
| InstrumentClass::Warrant
) {
let notional = instrument.calculate_notional_value(fill.last_qty, fill.last_px, None);
let pnl = if fill.order_side == OrderSide::Buy {
Money::from_raw(-notional.raw, notional.currency)
} else {
notional
};
pnls.push(pnl);
return Ok(pnls);
}
if let Some(ref pos) = position
&& pos.quantity.is_positive()
&& pos.entry != fill.order_side
{
let pnl_quantity = Quantity::from_raw(
fill.last_qty.raw.min(pos.quantity.raw),
fill.last_qty.precision,
);
let pnl = pos.calculate_pnl(pos.avg_px_open, fill.last_px.as_f64(), pnl_quantity);
pnls.push(pnl);
}
Ok(pnls)
}
fn calculate_commission(
&self,
instrument: &InstrumentAny,
last_qty: Quantity,
last_px: Price,
liquidity_side: LiquiditySide,
use_quote_for_inverse: Option<bool>,
) -> anyhow::Result<Money> {
self.base_calculate_commission(
instrument,
last_qty,
last_px,
liquidity_side,
use_quote_for_inverse,
)
}
}
impl PartialEq for MarginAccount {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
}
}
impl Eq for MarginAccount {}
impl Display for MarginAccount {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"MarginAccount(id={}, type={}, base={})",
self.id,
self.account_type,
self.base_currency.map_or_else(
|| "None".to_string(),
|base_currency| format!("{}", base_currency.code)
),
)
}
}
impl Hash for MarginAccount {
fn hash<H: Hasher>(&self, state: &mut H) {
self.id.hash(state);
}
}
#[cfg(test)]
mod tests {
use ahash::AHashMap;
use nautilus_core::UnixNanos;
use rstest::rstest;
use rust_decimal::Decimal;
use crate::{
accounts::{Account, MarginAccount, stubs::*},
enums::{AccountType, LiquiditySide, OrderSide, OrderType},
events::{AccountState, OrderFilled, account::stubs::*},
identifiers::{
AccountId, ClientOrderId, InstrumentId, PositionId, StrategyId, TradeId, TraderId,
VenueOrderId,
stubs::{uuid4, *},
},
instruments::{
CryptoPerpetual, CurrencyPair, Instrument, InstrumentAny,
stubs::{binary_option, option_contract_appl, *},
},
orders::{OrderTestBuilder, stubs::TestOrderEventStubs},
position::Position,
types::{Currency, MarginBalance, Money, Price, Quantity},
};
#[rstest]
fn test_display(margin_account: MarginAccount) {
assert_eq!(
margin_account.to_string(),
"MarginAccount(id=SIM-001, type=MARGIN, base=USD)"
);
}
#[rstest]
fn test_base_account_properties(
margin_account: MarginAccount,
margin_account_state: AccountState,
) {
assert_eq!(margin_account.base_currency, Some(Currency::from("USD")));
assert_eq!(
margin_account.last_event(),
Some(margin_account_state.clone())
);
assert_eq!(margin_account.events(), vec![margin_account_state.clone()]);
assert_eq!(margin_account.event_count(), 1);
assert_eq!(
margin_account.balance_total(None),
Some(Money::from("1525000 USD"))
);
assert_eq!(
margin_account.balance_free(None),
Some(Money::from("1500000 USD"))
);
assert_eq!(
margin_account.balance_locked(None),
Some(Money::from("25000 USD"))
);
let mut balances_total_expected = AHashMap::new();
balances_total_expected.insert(Currency::from("USD"), Money::from("1525000 USD"));
assert_eq!(margin_account.balances_total(), balances_total_expected);
let mut balances_free_expected = AHashMap::new();
balances_free_expected.insert(Currency::from("USD"), Money::from("1500000 USD"));
assert_eq!(margin_account.balances_free(), balances_free_expected);
let mut balances_locked_expected = AHashMap::new();
balances_locked_expected.insert(Currency::from("USD"), Money::from("25000 USD"));
assert_eq!(margin_account.balances_locked(), balances_locked_expected);
let margin_balance = margin_account_state.margins[0];
let mut initial_margins_expected = AHashMap::new();
initial_margins_expected.insert(margin_balance.instrument_id, margin_balance.initial);
assert_eq!(margin_account.initial_margins(), initial_margins_expected);
let mut maintenance_margins_expected = AHashMap::new();
maintenance_margins_expected
.insert(margin_balance.instrument_id, margin_balance.maintenance);
assert_eq!(
margin_account.maintenance_margins(),
maintenance_margins_expected
);
}
#[rstest]
fn test_set_default_leverage(mut margin_account: MarginAccount) {
assert_eq!(margin_account.default_leverage, Decimal::ONE);
margin_account.set_default_leverage(Decimal::from(10));
assert_eq!(margin_account.default_leverage, Decimal::from(10));
}
#[rstest]
fn test_get_leverage_default_leverage(
margin_account: MarginAccount,
instrument_id_aud_usd_sim: InstrumentId,
) {
assert_eq!(
margin_account.get_leverage(&instrument_id_aud_usd_sim),
Decimal::ONE
);
}
#[rstest]
fn test_set_leverage(
mut margin_account: MarginAccount,
instrument_id_aud_usd_sim: InstrumentId,
) {
assert_eq!(margin_account.leverages.len(), 0);
margin_account.set_leverage(instrument_id_aud_usd_sim, Decimal::from(10));
assert_eq!(margin_account.leverages.len(), 1);
assert_eq!(
margin_account.get_leverage(&instrument_id_aud_usd_sim),
Decimal::from(10)
);
}
#[rstest]
fn test_is_unleveraged_with_leverage_returns_false(
mut margin_account: MarginAccount,
instrument_id_aud_usd_sim: InstrumentId,
) {
margin_account.set_leverage(instrument_id_aud_usd_sim, Decimal::from(10));
assert!(!margin_account.is_unleveraged(instrument_id_aud_usd_sim));
}
#[rstest]
fn test_is_unleveraged_with_no_leverage_returns_true(
mut margin_account: MarginAccount,
instrument_id_aud_usd_sim: InstrumentId,
) {
margin_account.set_leverage(instrument_id_aud_usd_sim, Decimal::ONE);
assert!(margin_account.is_unleveraged(instrument_id_aud_usd_sim));
}
#[rstest]
fn test_is_unleveraged_with_default_leverage_of_1_returns_true(
margin_account: MarginAccount,
instrument_id_aud_usd_sim: InstrumentId,
) {
assert!(margin_account.is_unleveraged(instrument_id_aud_usd_sim));
}
#[rstest]
fn test_update_margin_init(
mut margin_account: MarginAccount,
instrument_id_aud_usd_sim: InstrumentId,
) {
assert_eq!(margin_account.margins.len(), 1);
let margin = Money::from("10000 USD");
margin_account.update_initial_margin(instrument_id_aud_usd_sim, margin);
assert_eq!(
margin_account.initial_margin(instrument_id_aud_usd_sim),
margin
);
assert_eq!(margin_account.margins.len(), 2);
assert_eq!(
margin_account
.margins
.get(&instrument_id_aud_usd_sim)
.expect("AUD/USD margin should exist")
.initial,
margin
);
}
#[rstest]
fn test_update_margin_maintenance(
mut margin_account: MarginAccount,
instrument_id_aud_usd_sim: InstrumentId,
) {
let margin = Money::from("10000 USD");
margin_account.update_maintenance_margin(instrument_id_aud_usd_sim, margin);
assert_eq!(
margin_account.maintenance_margin(instrument_id_aud_usd_sim),
margin
);
assert_eq!(margin_account.margins.len(), 2);
assert_eq!(
margin_account
.margins
.get(&instrument_id_aud_usd_sim)
.expect("AUD/USD margin should exist")
.maintenance,
margin
);
}
#[rstest]
fn test_apply_replaces_margin_balances_from_event(
mut margin_account: MarginAccount,
margin_account_state: AccountState,
) {
let old_instrument_id = margin_account_state.margins[0].instrument_id;
let new_instrument_id = InstrumentId::from("USDJPY.SIM");
let event = AccountState::new(
margin_account_state.account_id,
AccountType::Margin,
margin_account_state.balances.clone(),
vec![MarginBalance::new(
Money::from("12500 USD"),
Money::from("25000 USD"),
new_instrument_id,
)],
true,
uuid4(),
1.into(),
1.into(),
margin_account_state.base_currency,
);
margin_account.apply(event).unwrap();
assert_eq!(
margin_account.initial_margin(new_instrument_id),
Money::from("12500 USD")
);
assert_eq!(
margin_account.maintenance_margin(new_instrument_id),
Money::from("25000 USD")
);
assert!(margin_account.margin(&old_instrument_id).is_none());
}
#[rstest]
fn test_calculate_margin_init_with_leverage(
mut margin_account: MarginAccount,
audusd_sim: CurrencyPair,
) {
margin_account.set_leverage(audusd_sim.id, Decimal::from(50));
let result = margin_account
.calculate_initial_margin(
&audusd_sim,
Quantity::from(100_000),
Price::from("0.8000"),
None,
)
.unwrap();
assert_eq!(result, Money::from("48.00 USD"));
}
#[rstest]
fn test_calculate_margin_init_with_default_leverage(
mut margin_account: MarginAccount,
audusd_sim: CurrencyPair,
) {
margin_account.set_default_leverage(Decimal::from(10));
let result = margin_account
.calculate_initial_margin(
&audusd_sim,
Quantity::from(100_000),
Price::from("0.8"),
None,
)
.unwrap();
assert_eq!(result, Money::from("240.00 USD"));
}
#[rstest]
fn test_calculate_margin_init_with_no_leverage_for_inverse(
mut margin_account: MarginAccount,
xbtusd_bitmex: CryptoPerpetual,
) {
let result_use_quote_inverse_true = margin_account
.calculate_initial_margin(
&xbtusd_bitmex,
Quantity::from(100_000),
Price::from("11493.60"),
Some(false),
)
.unwrap();
assert_eq!(result_use_quote_inverse_true, Money::from("0.08700494 BTC"));
let result_use_quote_inverse_false = margin_account
.calculate_initial_margin(
&xbtusd_bitmex,
Quantity::from(100_000),
Price::from("11493.60"),
Some(true),
)
.unwrap();
assert_eq!(result_use_quote_inverse_false, Money::from("1000 USD"));
}
#[rstest]
fn test_calculate_margin_maintenance_with_no_leverage(
mut margin_account: MarginAccount,
xbtusd_bitmex: CryptoPerpetual,
) {
let result = margin_account
.calculate_maintenance_margin(
&xbtusd_bitmex,
Quantity::from(100_000),
Price::from("11493.60"),
None,
)
.unwrap();
assert_eq!(result, Money::from("0.03045173 BTC"));
}
#[rstest]
fn test_calculate_margin_maintenance_with_leverage_fx_instrument(
mut margin_account: MarginAccount,
audusd_sim: CurrencyPair,
) {
margin_account.set_default_leverage(Decimal::from(50));
let result = margin_account
.calculate_maintenance_margin(
&audusd_sim,
Quantity::from(1_000_000),
Price::from("1"),
None,
)
.unwrap();
assert_eq!(result, Money::from("600.00 USD"));
}
#[rstest]
fn test_calculate_margin_maintenance_with_leverage_inverse_instrument(
mut margin_account: MarginAccount,
xbtusd_bitmex: CryptoPerpetual,
) {
margin_account.set_default_leverage(Decimal::from(10));
let result = margin_account
.calculate_maintenance_margin(
&xbtusd_bitmex,
Quantity::from(100_000),
Price::from("100000.00"),
None,
)
.unwrap();
assert_eq!(result, Money::from("0.00035000 BTC"));
}
#[rstest]
fn test_calculate_pnls_github_issue_2657() {
let account_state = margin_account_state();
let account = MarginAccount::new(account_state, false);
let btcusdt = currency_pair_btcusdt();
let btcusdt_any = InstrumentAny::CurrencyPair(btcusdt);
let fill1 = OrderFilled::new(
TraderId::from("TRADER-001"),
StrategyId::from("S-001"),
btcusdt_any.id(),
ClientOrderId::from("O-1"),
VenueOrderId::from("V-1"),
AccountId::from("SIM-001"),
TradeId::from("T-1"),
OrderSide::Buy,
OrderType::Market,
Quantity::from("0.001"),
Price::from("50000.00"),
btcusdt_any.quote_currency(),
LiquiditySide::Taker,
uuid4(),
UnixNanos::from(1_000_000_000),
UnixNanos::default(),
false,
Some(PositionId::from("P-GITHUB-2657")),
None,
);
let position = Position::new(&btcusdt_any, fill1);
let fill2 = OrderFilled::new(
TraderId::from("TRADER-001"),
StrategyId::from("S-001"),
btcusdt_any.id(),
ClientOrderId::from("O-2"),
VenueOrderId::from("V-2"),
AccountId::from("SIM-001"),
TradeId::from("T-2"),
OrderSide::Sell,
OrderType::Market,
Quantity::from("0.002"), Price::from("50075.00"),
btcusdt_any.quote_currency(),
LiquiditySide::Taker,
uuid4(),
UnixNanos::from(2_000_000_000),
UnixNanos::default(),
false,
Some(PositionId::from("P-GITHUB-2657")),
None,
);
let pnls = account
.calculate_pnls(&btcusdt_any, &fill2, Some(position))
.unwrap();
assert_eq!(pnls.len(), 1);
let expected_pnl = Money::from("0.075 USDT");
assert_eq!(pnls[0], expected_pnl);
}
#[rstest]
#[should_panic(expected = "not positive")]
fn test_set_leverage_zero_panics(mut margin_account: MarginAccount, audusd_sim: CurrencyPair) {
margin_account.set_leverage(audusd_sim.id, Decimal::ZERO);
}
#[rstest]
#[should_panic(expected = "not positive")]
fn test_set_default_leverage_zero_panics(mut margin_account: MarginAccount) {
margin_account.set_default_leverage(Decimal::ZERO);
}
#[rstest]
#[should_panic(expected = "not positive")]
fn test_set_leverage_negative_panics(
mut margin_account: MarginAccount,
audusd_sim: CurrencyPair,
) {
margin_account.set_leverage(audusd_sim.id, Decimal::from(-1));
}
#[rstest]
fn test_calculate_pnls_with_same_side_fill_returns_empty() {
use nautilus_core::UnixNanos;
use crate::{
enums::{LiquiditySide, OrderSide, OrderType},
events::OrderFilled,
identifiers::{
AccountId, ClientOrderId, PositionId, StrategyId, TradeId, TraderId, VenueOrderId,
stubs::uuid4,
},
instruments::InstrumentAny,
position::Position,
types::{Price, Quantity},
};
let account_state = margin_account_state();
let account = MarginAccount::new(account_state, false);
let btcusdt = currency_pair_btcusdt();
let btcusdt_any = InstrumentAny::CurrencyPair(btcusdt.clone());
let fill1 = OrderFilled::new(
TraderId::from("TRADER-001"),
StrategyId::from("S-001"),
btcusdt.id,
ClientOrderId::from("O-1"),
VenueOrderId::from("V-1"),
AccountId::from("SIM-001"),
TradeId::from("T-1"),
OrderSide::Buy,
OrderType::Market,
Quantity::from("1.0"),
Price::from("50000.00"),
btcusdt.quote_currency,
LiquiditySide::Taker,
uuid4(),
UnixNanos::from(1_000_000_000),
UnixNanos::default(),
false,
Some(PositionId::from("P-123456")),
None,
);
let position = Position::new(&btcusdt_any, fill1);
let fill2 = OrderFilled::new(
TraderId::from("TRADER-001"),
StrategyId::from("S-001"),
btcusdt.id,
ClientOrderId::from("O-2"),
VenueOrderId::from("V-2"),
AccountId::from("SIM-001"),
TradeId::from("T-2"),
OrderSide::Buy, OrderType::Market,
Quantity::from("0.5"),
Price::from("51000.00"),
btcusdt.quote_currency,
LiquiditySide::Taker,
uuid4(),
UnixNanos::from(2_000_000_000),
UnixNanos::default(),
false,
Some(PositionId::from("P-123456")),
None,
);
let pnls = account
.calculate_pnls(&btcusdt_any, &fill2, Some(position))
.unwrap();
assert_eq!(pnls.len(), 0);
}
#[rstest]
fn test_margin_accessor(
mut margin_account: MarginAccount,
instrument_id_aud_usd_sim: InstrumentId,
) {
let margin_balance = MarginBalance::new(
Money::from("1000 USD"),
Money::from("500 USD"),
instrument_id_aud_usd_sim,
);
margin_account.update_margin(margin_balance);
let retrieved = margin_account.margin(&instrument_id_aud_usd_sim);
assert!(retrieved.is_some());
let retrieved = retrieved.unwrap();
assert_eq!(retrieved.initial, Money::from("1000 USD"));
assert_eq!(retrieved.maintenance, Money::from("500 USD"));
assert_eq!(retrieved.instrument_id, instrument_id_aud_usd_sim);
}
#[rstest]
fn test_clear_margin(
mut margin_account: MarginAccount,
instrument_id_aud_usd_sim: InstrumentId,
) {
let margin_balance = MarginBalance::new(
Money::from("1000 USD"),
Money::from("500 USD"),
instrument_id_aud_usd_sim,
);
margin_account.update_margin(margin_balance);
assert!(margin_account.margin(&instrument_id_aud_usd_sim).is_some());
margin_account.clear_margin(instrument_id_aud_usd_sim);
assert!(margin_account.margin(&instrument_id_aud_usd_sim).is_none());
}
#[rstest]
fn test_calculate_pnls_for_option_buy_realizes_premium(margin_account: MarginAccount) {
let option = option_contract_appl();
let option_any = InstrumentAny::OptionContract(option.clone());
let order = OrderTestBuilder::new(OrderType::Market)
.instrument_id(option.id)
.side(OrderSide::Buy)
.quantity(Quantity::from("10"))
.build();
let fill = TestOrderEventStubs::filled(
&order,
&option_any,
None,
Some(PositionId::new("P-OPT-001")),
Some(Price::from("5.50")),
None,
None,
None,
None,
Some(AccountId::from("SIM-001")),
);
let fill_owned: crate::events::OrderFilled = fill.into();
let pnls = margin_account
.calculate_pnls(&option_any, &fill_owned, None)
.unwrap();
assert_eq!(pnls.len(), 1);
assert_eq!(pnls[0], Money::from("-55 USD"));
}
#[rstest]
fn test_calculate_pnls_for_option_sell_realizes_premium(margin_account: MarginAccount) {
let option = option_contract_appl();
let option_any = InstrumentAny::OptionContract(option.clone());
let order = OrderTestBuilder::new(OrderType::Market)
.instrument_id(option.id)
.side(OrderSide::Sell)
.quantity(Quantity::from("10"))
.build();
let fill = TestOrderEventStubs::filled(
&order,
&option_any,
None,
Some(PositionId::new("P-OPT-002")),
Some(Price::from("5.50")),
None,
None,
None,
None,
Some(AccountId::from("SIM-001")),
);
let fill_owned: crate::events::OrderFilled = fill.into();
let pnls = margin_account
.calculate_pnls(&option_any, &fill_owned, None)
.unwrap();
assert_eq!(pnls.len(), 1);
assert_eq!(pnls[0], Money::from("55 USD"));
}
#[rstest]
fn test_calculate_pnls_for_binary_option(margin_account: MarginAccount) {
let binary = binary_option();
let binary_any = InstrumentAny::BinaryOption(binary);
let order = OrderTestBuilder::new(OrderType::Market)
.instrument_id(binary_any.id())
.side(OrderSide::Buy)
.quantity(Quantity::from("100"))
.build();
let fill = TestOrderEventStubs::filled(
&order,
&binary_any,
None,
Some(PositionId::new("P-BIN-001")),
Some(Price::from("0.65")),
None,
None,
None,
None,
Some(AccountId::from("SIM-001")),
);
let fill_owned: crate::events::OrderFilled = fill.into();
let pnls = margin_account
.calculate_pnls(&binary_any, &fill_owned, None)
.unwrap();
assert_eq!(pnls.len(), 1);
assert!(pnls[0].as_f64() < 0.0);
}
}