use fin_primitives::position::{Fill, Position, PositionLedger};
use fin_primitives::types::*;
use rust_decimal_macros::dec;
use std::collections::HashMap;
#[test]
fn test_position_open_close_pnl() {
let mut ledger = PositionLedger::new(dec!(100000));
let sym = Symbol::new("TSLA").unwrap();
ledger
.apply_fill(Fill {
symbol: sym.clone(),
side: Side::Bid,
quantity: Quantity::new(dec!(100)).unwrap(),
price: Price::new(dec!(200)).unwrap(),
timestamp: NanoTimestamp::new(0),
commission: dec!(0),
})
.unwrap();
assert_eq!(ledger.cash(), dec!(80000));
ledger
.apply_fill(Fill {
symbol: sym.clone(),
side: Side::Ask,
quantity: Quantity::new(dec!(100)).unwrap(),
price: Price::new(dec!(220)).unwrap(),
timestamp: NanoTimestamp::new(1),
commission: dec!(0),
})
.unwrap();
assert_eq!(ledger.cash(), dec!(102000));
assert_eq!(ledger.realized_pnl_total(), dec!(2000));
assert!(ledger.position(&sym).unwrap().is_flat());
}
#[test]
fn test_avg_cost_two_buys_different_prices() {
let sym = Symbol::new("X").unwrap();
let mut pos = Position::new(sym.clone());
pos.apply_fill(&Fill {
symbol: sym.clone(),
side: Side::Bid,
quantity: Quantity::new(dec!(10)).unwrap(),
price: Price::new(dec!(100)).unwrap(),
timestamp: NanoTimestamp::new(0),
commission: dec!(0),
})
.unwrap();
pos.apply_fill(&Fill {
symbol: sym.clone(),
side: Side::Bid,
quantity: Quantity::new(dec!(5)).unwrap(),
price: Price::new(dec!(130)).unwrap(),
timestamp: NanoTimestamp::new(1),
commission: dec!(0),
})
.unwrap();
assert_eq!(pos.avg_cost, dec!(110));
}
#[test]
fn test_avg_cost_three_buys_equal_size() {
let sym = Symbol::new("X").unwrap();
let mut pos = Position::new(sym.clone());
for p in [dec!(100), dec!(200), dec!(300)] {
pos.apply_fill(&Fill {
symbol: sym.clone(),
side: Side::Bid,
quantity: Quantity::new(dec!(1)).unwrap(),
price: Price::new(p).unwrap(),
timestamp: NanoTimestamp::new(0),
commission: dec!(0),
})
.unwrap();
}
assert_eq!(pos.avg_cost, dec!(200));
}
#[test]
fn test_short_position_unrealized_pnl_below_entry() {
let sym = Symbol::new("X").unwrap();
let mut pos = Position::new(sym.clone());
pos.apply_fill(&Fill {
symbol: sym.clone(),
side: Side::Ask,
quantity: Quantity::new(dec!(5)).unwrap(),
price: Price::new(dec!(100)).unwrap(),
timestamp: NanoTimestamp::new(0),
commission: dec!(0),
})
.unwrap();
let upnl = pos.unrealized_pnl(Price::new(dec!(80)).unwrap());
assert_eq!(upnl, dec!(100));
}
#[test]
fn test_short_position_unrealized_pnl_above_entry_is_negative() {
let sym = Symbol::new("X").unwrap();
let mut pos = Position::new(sym.clone());
pos.apply_fill(&Fill {
symbol: sym.clone(),
side: Side::Ask,
quantity: Quantity::new(dec!(5)).unwrap(),
price: Price::new(dec!(100)).unwrap(),
timestamp: NanoTimestamp::new(0),
commission: dec!(0),
})
.unwrap();
let upnl = pos.unrealized_pnl(Price::new(dec!(110)).unwrap());
assert_eq!(upnl, dec!(-50));
}
#[test]
fn test_flat_to_long_to_flat_to_short() {
let sym = Symbol::new("X").unwrap();
let mut pos = Position::new(sym.clone());
assert!(pos.is_flat());
pos.apply_fill(&Fill {
symbol: sym.clone(),
side: Side::Bid,
quantity: Quantity::new(dec!(10)).unwrap(),
price: Price::new(dec!(100)).unwrap(),
timestamp: NanoTimestamp::new(0),
commission: dec!(0),
})
.unwrap();
assert_eq!(pos.quantity, dec!(10));
assert!(!pos.is_flat());
let pnl = pos
.apply_fill(&Fill {
symbol: sym.clone(),
side: Side::Ask,
quantity: Quantity::new(dec!(10)).unwrap(),
price: Price::new(dec!(110)).unwrap(),
timestamp: NanoTimestamp::new(1),
commission: dec!(0),
})
.unwrap();
assert_eq!(pnl, dec!(100));
assert!(pos.is_flat());
assert_eq!(pos.avg_cost, dec!(0));
pos.apply_fill(&Fill {
symbol: sym.clone(),
side: Side::Ask,
quantity: Quantity::new(dec!(5)).unwrap(),
price: Price::new(dec!(120)).unwrap(),
timestamp: NanoTimestamp::new(2),
commission: dec!(0),
})
.unwrap();
assert_eq!(pos.quantity, dec!(-5));
assert_eq!(pos.avg_cost, dec!(120));
}
#[test]
fn test_long_to_short_in_one_fill() {
let sym = Symbol::new("X").unwrap();
let mut pos = Position::new(sym.clone());
pos.apply_fill(&Fill {
symbol: sym.clone(),
side: Side::Bid,
quantity: Quantity::new(dec!(5)).unwrap(),
price: Price::new(dec!(100)).unwrap(),
timestamp: NanoTimestamp::new(0),
commission: dec!(0),
})
.unwrap();
pos.apply_fill(&Fill {
symbol: sym.clone(),
side: Side::Ask,
quantity: Quantity::new(dec!(15)).unwrap(),
price: Price::new(dec!(110)).unwrap(),
timestamp: NanoTimestamp::new(1),
commission: dec!(0),
})
.unwrap();
assert_eq!(
pos.quantity,
dec!(-10),
"position should be short 10 after flip"
);
assert_eq!(
pos.avg_cost,
dec!(110),
"avg_cost of new short = fill price"
);
}
#[test]
fn test_position_equity_with_open_position() {
let mut ledger = PositionLedger::new(dec!(50000));
let sym = Symbol::new("NVDA").unwrap();
ledger
.apply_fill(Fill {
symbol: sym.clone(),
side: Side::Bid,
quantity: Quantity::new(dec!(10)).unwrap(),
price: Price::new(dec!(500)).unwrap(),
timestamp: NanoTimestamp::new(0),
commission: dec!(0),
})
.unwrap();
let mut prices = HashMap::new();
prices.insert("NVDA".to_string(), Price::new(dec!(550)).unwrap());
let equity = ledger.equity(&prices).unwrap();
assert_eq!(equity, dec!(45500));
}