mod imbalanced_fill;
use std::ops::Neg;
#[allow(unused_imports)]
use apple_quant_core::log::trace;
use apple_quant_core::{log::info, AddUnchecked, SubReleaseUnchecked, SubUnchecked};
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use smallvec::SmallVec;
use tokio::{fs::File, io::AsyncWriteExt};
use tracing::instrument;
use crate::{
instrument::{AsDecimal, InstrumentSpec},
volume::{DirectionalIntent, VolumeType, ZeroableExt, ZeroableVolume},
order::PartialOrderFill, points::Subpoints, price::AbsolutePrice,
timestamp::TradeTimestamp,
};
use super::{DirectionalIntentVolume, DirectionlessVolume, MatchReduceResult, Zeroable};
pub use imbalanced_fill::*;
#[derive(Debug)]
pub struct DirectionalExposure<IS: InstrumentSpec> {
/// Directional intent volume that is effecting the current exposed position. Fully matched orders fully effect this and are negated from [`armed_directional_intent_volume`], while partially matched order partially effect both effective and armed. Only client acknowledged matched volume is part of this, all other volume contributes to armed. It is guaranteed that the combination of effective and armed directional intent volume equals the combination of all non-canceled/non-rejected submitted orders. Orders will be fully matched before the client is made aware of the events.
effective: Zeroable<DirectionalIntentVolume<IS>>,
/// Directional intent volume that has the potential to transition to altering the current exposed position. Resting limit, client stop, and all unbooked and unmatched orders contribute to this. Immediately executable orders do effect this until they are fully matched. Partially matched orders partially effect this. Orders will be fully matched before the client is made aware of the events.
armed: Zeroable<DirectionalIntentVolume<IS>>,
imbalanced_fills: SmallVec<[ImbalancedFill<IS>; 8]>,
captured_delta: Subpoints,
captured_volume: Zeroable<DirectionlessVolume<IS>>,
captured_list: Vec<(
Subpoints,
DirectionlessVolume<IS>,
TradeTimestamp,
)>,
}
impl<IS: InstrumentSpec> DirectionalExposure<IS> {
pub fn effective(
&self,
) -> &Zeroable<DirectionalIntentVolume<IS>> {
&self.effective
}
pub fn armed(
&self,
) -> &Zeroable<DirectionalIntentVolume<IS>> {
&self.armed
}
pub fn combined(
&self,
) -> Zeroable<DirectionalIntentVolume<IS>> {
self.effective.add_unchecked(&self.armed)
}
pub(crate) fn change_armed_directional_exposure(
&mut self,
delta_directional_exposure: &Zeroable<DirectionalIntentVolume<IS>>,
) {
self.armed = self.armed.add_unchecked(delta_directional_exposure);
#[cfg(feature = "log-trace-directional-exposure")]
trace!(
"Armed directional exposure modified to `{:?}`.",
self.armed
);
}
#[instrument(skip_all)]
pub(crate) async fn apply_partial_order_fill(
&mut self,
partial_order_fill: &PartialOrderFill<IS>,
) {
if let Some(
directional_intent_volume,
) = self.effective.as_optional_nonzero_ref() &&
partial_order_fill.directional_intent_volume.directional_intent !=
directional_intent_volume.directional_intent
{
self.match_reduce(
partial_order_fill.directional_intent_volume.directionless_volume,
partial_order_fill.price,
&partial_order_fill.timestamp,
).await;
return;
}
let imbalanced_fill = ImbalancedFill::from_partial_order_fill(
partial_order_fill,
);
self.imbalanced_fills.push(imbalanced_fill);
self.effective = self.effective.add_unchecked(
partial_order_fill.directional_intent_volume.as_zeroable(),
);
#[cfg(feature = "log-trace-directional-exposure")]
trace!(
"Effective directional exposure modified to `{:?}`.",
self.effective
);
}
#[instrument(skip_all)]
async fn match_reduce(
&mut self,
unmatched_volume: DirectionlessVolume<IS>,
unmatched_price: AbsolutePrice<IS>,
fill_timestamp: &TradeTimestamp,
) {
let total_matching_volume = unmatched_volume;
let mut unmatched_volume = Some(unmatched_volume);
self.imbalanced_fills
.drain_filter(|
imbalanced_fill,
| {
let Some(
some_unmatched_volume,
) = &unmatched_volume else {
return false;
};
let remaining_matching_volume = *some_unmatched_volume;
let MatchReduceResult {
new_matched,
new_matcher,
matched_quantity,
} = imbalanced_fill.volume.match_reduce(&some_unmatched_volume);
let Some(
new_matched,
) = new_matched else {
let Some(
new_matcher,
) = new_matcher else {
self.captured_volume = self.captured_volume.add_unchecked(
remaining_matching_volume.as_zeroable(),
);
let mut delta_price = *unmatched_price - *imbalanced_fill.price;
if (
self.effective.as_zeroable().directional_intent ==
DirectionalIntent::Negative
) {
delta_price = delta_price.neg();
}
remaining_matching_volume.multiply_price_type(&mut delta_price);
self.captured_delta = (*self.captured_delta.as_subpoints_type() +
delta_price.into().into_subpoints_type()).into();
self.captured_list.push((
delta_price.into().into_subpoints_type().into(),
remaining_matching_volume,
*fill_timestamp,
));
unmatched_volume = None;
return true;
};
self.captured_volume = self.captured_volume.add_unchecked(
imbalanced_fill.volume().as_zeroable(),
);
let mut delta_price = *unmatched_price - *imbalanced_fill.price;
if (
self.effective.as_zeroable().directional_intent ==
DirectionalIntent::Negative
) {
delta_price = delta_price.neg();
}
imbalanced_fill.volume().as_volume_type().multiply_price_type(
&mut delta_price,
);
self.captured_delta = (*self.captured_delta.as_subpoints_type() +
delta_price.into().into_subpoints_type()).into();
self.captured_list.push((
delta_price.into().into_subpoints_type().into(),
*imbalanced_fill.volume(),
*fill_timestamp,
));
unmatched_volume = Some(new_matcher);
return true;
};
// `new_matched` will always be smaller then the `imbalanced_fill.volume`.
let matched_volume_quantity = imbalanced_fill.volume.sub_release_unchecked(
new_matched,
).unwrap();
self.captured_volume = self.captured_volume.add_unchecked(
matched_volume_quantity.as_zeroable(),
);
let mut delta_price = *unmatched_price - *imbalanced_fill.price;
if (
self.effective.as_zeroable().directional_intent ==
DirectionalIntent::Negative
) {
delta_price = delta_price.neg();
}
matched_volume_quantity.as_volume_type().multiply_price_type(
&mut delta_price,
);
self.captured_delta = (*self.captured_delta.as_subpoints_type() +
delta_price.into().into_subpoints_type()).into();
self.captured_list.push((
delta_price.into().into_subpoints_type().into(),
matched_volume_quantity,
*fill_timestamp,
));
imbalanced_fill.volume = new_matched;
unmatched_volume = new_matcher;
false
}).for_each(drop);
info!(
"Updated captured delta: {}T, including fees: {}T.",
self.captured_delta.as_subpoints_type(),
AsDecimal::<IS::VolumeSpec>::as_decimal(self.captured_delta.as_subpoints_type())
- (self.captured_volume.as_zeroable().as_decimal() *
Decimal::from_f32_retain(2.0).unwrap()),
);
self.save_results().await;
let Some(
unmatched_volume,
) = unmatched_volume else {
let effective = self.effective.as_zeroable();
let total_matching_directional_intent_volume = total_matching_volume.with_directional_intent(
effective.directional_intent,
);
self.effective = effective
.sub_unchecked(total_matching_directional_intent_volume).as_zeroable();
#[cfg(feature = "log-trace-directional-exposure")]
trace!(
"Effective directional exposure modified to `{:?}`.",
self.effective
);
return;
};
let imbalanced_fill = ImbalancedFill {
price: unmatched_price,
volume: unmatched_volume,
};
debug_assert!(self.imbalanced_fills.is_empty());
self.imbalanced_fills.push(imbalanced_fill);
let effective = self.effective.as_zeroable();
let directional_intent = effective.directional_intent.as_flipped();
let effective_directional_intent_volume = unmatched_volume.with_directional_intent(
directional_intent,
);
self.effective = effective_directional_intent_volume.as_zeroable();
#[cfg(feature = "log-trace-directional-exposure")]
trace!(
"Effective directional exposure modified to `{:?}`.",
self.effective
);
}
pub(crate) async fn save_results(
&self,
) {
let delta_total = *self.captured_delta.as_subpoints_type() as i32;
let delta_total_realized = AsDecimal::<IS::VolumeSpec>::as_decimal(
self.captured_delta.as_subpoints_type(),
) -
(self.captured_volume.as_zeroable().as_decimal() *
Decimal::from_f32_retain(2.0).unwrap());
let delta_total_realized = delta_total_realized.as_i128() as i32;
let delta_subpoints = self.captured_list.iter().map(|(
subpoints,
_,
_,
)| *subpoints.as_subpoints_type() as i32).collect();
let delta_timestamps = self.captured_list.iter().map(|(
_,
_,
trade_timestamp,
)| *trade_timestamp.as_timestamp_type() as i128).collect();
let output_toml = OutputToml {
delta_total,
delta_total_realized,
delta_subpoints,
delta_timestamps,
};
let string = toml::to_string_pretty(&output_toml).unwrap();
let mut file = File::create("output.toml").await.unwrap();
file.write_all(string.as_bytes()).await.unwrap();
}
}
#[derive(Debug, Serialize, Deserialize)]
struct OutputToml {
delta_total: i32,
delta_total_realized: i32,
delta_subpoints: Vec<i32>,
delta_timestamps: Vec<i128>,
}
impl<IS: InstrumentSpec> Default for DirectionalExposure<IS> {
fn default() -> Self {
Self {
armed: Zeroable::ZERO,
effective: Zeroable::ZERO,
imbalanced_fills: SmallVec::default(),
captured_delta: Subpoints::ZERO,
captured_volume: Zeroable::ZERO,
captured_list: Vec::with_capacity(1024),
}
}
}