apple_quant_algorithmic/volume/
directional_exposure.rs1mod 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 effective: Zeroable<DirectionalIntentVolume<IS>>,
35
36 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 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}