use chrono::{DateTime, Utc};
use derive_more::Constructor;
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
#[derive(
Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize, Constructor,
)]
pub struct Position {
pub quantity: Decimal,
pub entry_price: Option<Decimal>,
pub unrealized_pnl: Option<Decimal>,
pub margin_used: Option<Decimal>,
pub liquidation_price: Option<Decimal>,
pub leverage: Option<Decimal>,
pub carry: Option<Decimal>,
pub time_exchange: DateTime<Utc>,
}
impl Position {
pub fn is_flat(&self) -> bool {
self.quantity.is_zero()
}
pub fn is_long(&self) -> bool {
self.quantity > Decimal::ZERO
}
pub fn is_short(&self) -> bool {
self.quantity < Decimal::ZERO
}
pub fn abs_quantity(&self) -> Decimal {
self.quantity.abs()
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
pub enum PositionReport {
#[default]
Unreported,
Flat,
Open(Position),
}
impl PositionReport {
pub fn from_position(position: Position) -> Self {
if position.is_flat() {
Self::Flat
} else {
Self::Open(position)
}
}
pub fn open(&self) -> Option<&Position> {
match self {
Self::Open(position) => Some(position),
Self::Unreported | Self::Flat => None,
}
}
pub fn quantity(&self) -> Option<Decimal> {
match self {
Self::Unreported => None,
Self::Flat => Some(Decimal::ZERO),
Self::Open(position) => Some(position.quantity),
}
}
pub fn is_unreported(&self) -> bool {
matches!(self, Self::Unreported)
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)] mod tests {
use super::*;
use rust_decimal_macros::dec;
#[test]
fn instrument_snapshot_position_serde() {
use crate::InstrumentAccountSnapshot;
use rustrade_instrument::{
asset::name::AssetNameExchange, exchange::ExchangeId,
instrument::name::InstrumentNameExchange,
};
type Snapshot =
InstrumentAccountSnapshot<ExchangeId, AssetNameExchange, InstrumentNameExchange>;
let open = Position::new(
dec!(-2),
Some(dec!(10)),
None,
None,
None,
None,
None,
Utc::now(),
);
for (position, expected) in [
(PositionReport::Unreported, None),
(PositionReport::Flat, Some(serde_json::json!("Flat"))),
(
PositionReport::Open(open.clone()),
Some(serde_json::json!({ "Open": serde_json::to_value(&open).unwrap() })),
),
] {
let snapshot = Snapshot::new("x".into(), vec![], false, position, None);
let json = serde_json::to_value(&snapshot).unwrap();
assert_eq!(json.get("position").cloned(), expected);
assert_eq!(serde_json::from_value::<Snapshot>(json).unwrap(), snapshot);
}
}
#[test]
fn position_report_from_position_reports_zero_as_flat() {
let now = Utc::now();
let open = Position::new(dec!(-2), None, None, None, None, None, None, now);
assert_eq!(
PositionReport::from_position(open.clone()),
PositionReport::Open(open.clone())
);
for zero in [dec!(0), -dec!(0)] {
let flat = Position::new(zero, None, None, None, None, None, None, now);
assert_eq!(PositionReport::from_position(flat), PositionReport::Flat);
}
assert_eq!(PositionReport::Open(open).quantity(), Some(dec!(-2)));
assert_eq!(PositionReport::Flat.quantity(), Some(Decimal::ZERO));
assert_eq!(PositionReport::Unreported.quantity(), None);
}
#[test]
fn test_position_side_detection() {
let now = Utc::now();
let long = Position::new(dec!(1.5), None, None, None, None, None, None, now);
assert!(long.is_long());
assert!(!long.is_short());
assert!(!long.is_flat());
let short = Position::new(dec!(-1.5), None, None, None, None, None, None, now);
assert!(!short.is_long());
assert!(short.is_short());
assert!(!short.is_flat());
for zero in [dec!(0), -dec!(0)] {
let flat = Position::new(zero, None, None, None, None, None, None, now);
assert!(!flat.is_long(), "{zero:?}");
assert!(!flat.is_short(), "{zero:?}");
assert!(flat.is_flat(), "{zero:?}");
}
}
#[test]
fn test_abs_quantity() {
let now = Utc::now();
let short = Position::new(dec!(-2.5), None, None, None, None, None, None, now);
assert_eq!(short.abs_quantity(), dec!(2.5));
}
}