use rust_decimal::{
Decimal,
prelude::{FromPrimitive, ToPrimitive},
};
use serde::{Deserialize, Serialize};
use crate::{
data::domain::{Instrument, Period, Price},
error::{ChapatyResult, EnvError, SystemError},
};
#[derive(
Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize, Default,
)]
pub enum ValueAreaRule {
#[default]
HighestVolume,
HighestVolumePreferLower,
Symmetric,
}
#[derive(
Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize, Default,
)]
pub enum PocRule {
#[default]
LowestPrice,
HighestPrice,
ClosestToCenter,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
pub struct ProfileAggregation {
pub time_frame: Option<Period>,
pub ticks_per_bin: Option<u32>,
pub value_area_bps: Option<u16>,
pub poc_rule: Option<PocRule>,
pub value_area_rule: Option<ValueAreaRule>,
}
impl Default for ProfileAggregation {
fn default() -> Self {
Self {
time_frame: Some(Period::Minute(1)),
ticks_per_bin: Some(1),
value_area_bps: Some(7000),
poc_rule: Some(PocRule::default()),
value_area_rule: Some(ValueAreaRule::default()),
}
}
}
impl ProfileAggregation {
pub fn actual_price_bin_string<I: Instrument>(&self, instrument: &I) -> ChapatyResult<String> {
self.calculate_bin_decimal(instrument)
.map(|d| d.normalize().to_string())
}
pub fn actual_price_bin<I: Instrument>(&self, instrument: &I) -> ChapatyResult<f64> {
self.calculate_bin_decimal(instrument)?
.to_f64()
.ok_or_else(|| {
SystemError::InvariantViolation(
"Calculated price bin Decimal is too large or invalid to convert to f64"
.to_string(),
)
.into()
})
}
pub fn value_area_pct(&self) -> f64 {
let bps = self.value_area_bps.unwrap_or(7000);
f64::from(bps) / 10_000.0
}
}
impl ProfileAggregation {
fn calculate_bin_decimal<I: Instrument>(&self, instrument: &I) -> ChapatyResult<Decimal> {
let multiplier = self.ticks_per_bin.unwrap_or(1);
let tick_size = instrument.tick_size();
let tick_dec = Decimal::from_f64(tick_size).ok_or_else(|| {
SystemError::InvariantViolation(format!(
"Instrument tick size is invalid (NaN or Infinity): {}",
tick_size
))
})?;
Ok(tick_dec * Decimal::from(multiplier))
}
}
#[derive(Copy, Clone, Serialize, Deserialize)]
pub struct MarketProfileStats {
pub poc: Price,
pub value_area_high: Price,
pub value_area_low: Price,
}
pub trait ProfileBinStats {
fn get_value(&self) -> f64;
fn get_price(&self) -> Price;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
pub struct RiskMetricsConfig {
initial_portfolio_value: u32,
annual_risk_free_rate_bps: u16,
}
impl Default for RiskMetricsConfig {
fn default() -> Self {
Self {
initial_portfolio_value: 10_000,
annual_risk_free_rate_bps: 200,
}
}
}
impl RiskMetricsConfig {
pub fn new(initial_portfolio_value: u32) -> ChapatyResult<Self> {
if initial_portfolio_value == 0 {
return Err(EnvError::InvalidRiskMetricsConfig(
"Initial portfolio value must be positive (> 0)".to_string(),
)
.into());
}
Ok(Self {
initial_portfolio_value,
..Default::default()
})
}
pub fn with_annual_risk_free_rate_bps(self, bps: u16) -> Self {
Self {
annual_risk_free_rate_bps: bps,
..self
}
}
pub fn initial_portfolio_value(&self) -> u32 {
self.initial_portfolio_value
}
pub fn risk_free_rate_f64(&self) -> f64 {
f64::from(self.annual_risk_free_rate_bps) / 10_000.0
}
pub fn risk_free_rate_bps(&self) -> u16 {
self.annual_risk_free_rate_bps
}
}