Skip to main content

apple_quant_algorithmic/volume/
directional_exposure.rs

1mod imbalanced_fill;
2
3use std::ops::Neg;
4
5#[allow(unused_imports)]
6use apple_quant_core::log::trace;
7
8use apple_quant_core::{log::info, AddUnchecked, SubReleaseUnchecked, SubUnchecked};
9
10use rust_decimal::Decimal;
11
12use serde::{Deserialize, Serialize};
13
14use smallvec::SmallVec;
15
16use tokio::{fs::File, io::AsyncWriteExt};
17
18use tracing::instrument;
19
20use crate::{
21	instrument::{AsDecimal, InstrumentSpec},
22	volume::{DirectionalIntent, VolumeType, ZeroableExt, ZeroableVolume},
23	order::PartialOrderFill, points::Subpoints, price::AbsolutePrice,
24	timestamp::TradeTimestamp,
25};
26
27use super::{DirectionalIntentVolume, DirectionlessVolume, MatchReduceResult, Zeroable};
28
29pub use imbalanced_fill::*;
30
31#[derive(Debug)]
32pub struct DirectionalExposure<IS: InstrumentSpec> {
33	/// 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.
34	effective: Zeroable<DirectionalIntentVolume<IS>>,
35
36	/// 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.
37	armed: Zeroable<DirectionalIntentVolume<IS>>,
38	imbalanced_fills: SmallVec<[ImbalancedFill<IS>; 8]>,
39	captured_delta: Subpoints,
40	captured_volume: Zeroable<DirectionlessVolume<IS>>,
41
42	captured_list: Vec<(
43		Subpoints,
44		DirectionlessVolume<IS>,
45		TradeTimestamp,
46	)>,
47}
48
49impl<IS: InstrumentSpec> DirectionalExposure<IS> {
50	pub fn effective(
51		&self,
52	) -> &Zeroable<DirectionalIntentVolume<IS>> {
53		&self.effective
54	}
55
56	pub fn armed(
57		&self,
58	) -> &Zeroable<DirectionalIntentVolume<IS>> {
59		&self.armed
60	}
61
62	pub fn combined(
63		&self,
64	) -> Zeroable<DirectionalIntentVolume<IS>> {
65		self.effective.add_unchecked(&self.armed)
66	}
67
68	pub(crate) fn change_armed_directional_exposure(
69		&mut self,
70		delta_directional_exposure: &Zeroable<DirectionalIntentVolume<IS>>,
71	) {
72		self.armed = self.armed.add_unchecked(delta_directional_exposure);
73
74		#[cfg(feature = "log-trace-directional-exposure")]
75		trace!(
76			"Armed directional exposure modified to `{:?}`.",
77			self.armed
78		);
79	}
80
81	#[instrument(skip_all)]
82	pub(crate) async fn apply_partial_order_fill(
83		&mut self,
84		partial_order_fill: &PartialOrderFill<IS>,
85	) {
86		if let Some(
87			directional_intent_volume,
88		) = self.effective.as_optional_nonzero_ref() &&
89			partial_order_fill.directional_intent_volume.directional_intent !=
90			directional_intent_volume.directional_intent
91		{
92			self.match_reduce(
93				partial_order_fill.directional_intent_volume.directionless_volume,
94				partial_order_fill.price,
95				&partial_order_fill.timestamp,
96			).await;
97
98			return;
99		}
100
101		let imbalanced_fill = ImbalancedFill::from_partial_order_fill(
102			partial_order_fill,
103		);
104
105		self.imbalanced_fills.push(imbalanced_fill);
106
107		self.effective = self.effective.add_unchecked(
108			partial_order_fill.directional_intent_volume.as_zeroable(),
109		);
110
111		#[cfg(feature = "log-trace-directional-exposure")]
112		trace!(
113			"Effective directional exposure modified to `{:?}`.",
114			self.effective
115		);
116	}
117
118	#[instrument(skip_all)]
119	async fn match_reduce(
120		&mut self,
121		unmatched_volume: DirectionlessVolume<IS>,
122		unmatched_price: AbsolutePrice<IS>,
123		fill_timestamp: &TradeTimestamp,
124	) {
125		let total_matching_volume = unmatched_volume;
126		let mut unmatched_volume = Some(unmatched_volume);
127
128		self.imbalanced_fills
129			.drain_filter(|
130				imbalanced_fill,
131			| {
132				let Some(
133					some_unmatched_volume,
134				) = &unmatched_volume else {
135					return false;
136				};
137
138				let remaining_matching_volume = *some_unmatched_volume;
139
140				let MatchReduceResult {
141					new_matched,
142					new_matcher,
143					matched_quantity,
144				} = imbalanced_fill.volume.match_reduce(&some_unmatched_volume);
145
146				let Some(
147					new_matched,
148				) = new_matched else {
149					let Some(
150						new_matcher,
151					) = new_matcher else {
152						self.captured_volume = self.captured_volume.add_unchecked(
153							remaining_matching_volume.as_zeroable(),
154						);
155
156						let mut delta_price = *unmatched_price - *imbalanced_fill.price;
157
158						if (
159							self.effective.as_zeroable().directional_intent ==
160							DirectionalIntent::Negative
161						) {
162							delta_price = delta_price.neg();
163						}
164
165						remaining_matching_volume.multiply_price_type(&mut delta_price);
166
167						self.captured_delta = (*self.captured_delta.as_subpoints_type() +
168						delta_price.into().into_subpoints_type()).into();
169
170						self.captured_list.push((
171							delta_price.into().into_subpoints_type().into(),
172							remaining_matching_volume,
173							*fill_timestamp,
174						));
175
176						unmatched_volume = None;
177
178						return true;
179					};
180
181					self.captured_volume = self.captured_volume.add_unchecked(
182						imbalanced_fill.volume().as_zeroable(),
183					);
184
185					let mut delta_price = *unmatched_price - *imbalanced_fill.price;
186
187					if (
188						self.effective.as_zeroable().directional_intent ==
189						DirectionalIntent::Negative
190					) {
191						delta_price = delta_price.neg();
192					}
193
194					imbalanced_fill.volume().as_volume_type().multiply_price_type(
195						&mut delta_price,
196					);
197
198					self.captured_delta = (*self.captured_delta.as_subpoints_type() +
199					delta_price.into().into_subpoints_type()).into();
200
201					self.captured_list.push((
202						delta_price.into().into_subpoints_type().into(),
203						*imbalanced_fill.volume(),
204						*fill_timestamp,
205					));
206
207					unmatched_volume = Some(new_matcher);
208
209					return true;
210				};
211
212				// `new_matched` will always be smaller then the `imbalanced_fill.volume`.
213				let matched_volume_quantity = imbalanced_fill.volume.sub_release_unchecked(
214					new_matched,
215				).unwrap();
216
217				self.captured_volume = self.captured_volume.add_unchecked(
218					matched_volume_quantity.as_zeroable(),
219				);
220
221				let mut delta_price = *unmatched_price - *imbalanced_fill.price;
222
223				if (
224					self.effective.as_zeroable().directional_intent ==
225					DirectionalIntent::Negative
226				) {
227					delta_price = delta_price.neg();
228				}
229
230				matched_volume_quantity.as_volume_type().multiply_price_type(
231					&mut delta_price,
232				);
233
234				self.captured_delta = (*self.captured_delta.as_subpoints_type() +
235				delta_price.into().into_subpoints_type()).into();
236
237				self.captured_list.push((
238					delta_price.into().into_subpoints_type().into(),
239					matched_volume_quantity,
240					*fill_timestamp,
241				));
242
243				imbalanced_fill.volume = new_matched;
244
245				unmatched_volume = new_matcher;
246
247				false
248			}).for_each(drop);
249
250		info!(
251			"Updated captured delta: {}T, including fees: {}T.",
252			self.captured_delta.as_subpoints_type(),
253			AsDecimal::<IS::VolumeSpec>::as_decimal(self.captured_delta.as_subpoints_type())
254			- (self.captured_volume.as_zeroable().as_decimal() *
255			Decimal::from_f32_retain(2.0).unwrap()),
256		);
257
258		self.save_results().await;
259
260		let Some(
261			unmatched_volume,
262		) = unmatched_volume else {
263			let effective = self.effective.as_zeroable();
264
265			let total_matching_directional_intent_volume = total_matching_volume.with_directional_intent(
266				effective.directional_intent,
267			);
268
269			self.effective = effective
270				.sub_unchecked(total_matching_directional_intent_volume).as_zeroable();
271
272			#[cfg(feature = "log-trace-directional-exposure")]
273			trace!(
274				"Effective directional exposure modified to `{:?}`.",
275				self.effective
276			);
277
278			return;
279		};
280
281		let imbalanced_fill = ImbalancedFill {
282			price: unmatched_price,
283			volume: unmatched_volume,
284		};
285
286		debug_assert!(self.imbalanced_fills.is_empty());
287
288		self.imbalanced_fills.push(imbalanced_fill);
289
290		let effective = self.effective.as_zeroable();
291		let directional_intent = effective.directional_intent.as_flipped();
292
293		let effective_directional_intent_volume = unmatched_volume.with_directional_intent(
294			directional_intent,
295		);
296
297		self.effective = effective_directional_intent_volume.as_zeroable();
298
299		#[cfg(feature = "log-trace-directional-exposure")]
300		trace!(
301			"Effective directional exposure modified to `{:?}`.",
302			self.effective
303		);
304	}
305
306	pub(crate) async fn save_results(
307		&self,
308	) {
309		let delta_total = *self.captured_delta.as_subpoints_type() as i32;
310
311		let delta_total_realized = AsDecimal::<IS::VolumeSpec>::as_decimal(
312			self.captured_delta.as_subpoints_type(),
313		) -
314			(self.captured_volume.as_zeroable().as_decimal() *
315			Decimal::from_f32_retain(2.0).unwrap());
316
317		let delta_total_realized = delta_total_realized.as_i128() as i32;
318
319		let delta_subpoints = self.captured_list.iter().map(|(
320			subpoints,
321			_,
322			_,
323		)| *subpoints.as_subpoints_type() as i32).collect();
324
325		let delta_timestamps = self.captured_list.iter().map(|(
326			_,
327			_,
328			trade_timestamp,
329		)| *trade_timestamp.as_timestamp_type() as i128).collect();
330
331		let output_toml = OutputToml {
332			delta_total,
333			delta_total_realized,
334			delta_subpoints,
335			delta_timestamps,
336		};
337
338		let string = toml::to_string_pretty(&output_toml).unwrap();
339		let mut file = File::create("output.toml").await.unwrap();
340
341		file.write_all(string.as_bytes()).await.unwrap();
342	}
343}
344
345#[derive(Debug, Serialize, Deserialize)]
346struct OutputToml {
347	delta_total: i32,
348	delta_total_realized: i32,
349	delta_subpoints: Vec<i32>,
350	delta_timestamps: Vec<i128>,
351}
352
353impl<IS: InstrumentSpec> Default for DirectionalExposure<IS> {
354	fn default() -> Self {
355		Self {
356			armed: Zeroable::ZERO,
357			effective: Zeroable::ZERO,
358			imbalanced_fills: SmallVec::default(),
359			captured_delta: Subpoints::ZERO,
360			captured_volume: Zeroable::ZERO,
361			captured_list: Vec::with_capacity(1024),
362		}
363	}
364}