Skip to main content

nautilus_data/defi/
engine.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! DeFi-specific data engine functionality.
17//!
18//! This module provides DeFi processing methods for the `DataEngine`.
19//! All code in this module requires the `defi` feature flag.
20
21use std::{rc::Rc, sync::Arc};
22
23use nautilus_common::{
24    defi,
25    messages::defi::{
26        DefiRequestCommand, DefiSubscribeCommand, DefiUnsubscribeCommand, PoolSnapshotResponse,
27        RequestPoolSnapshot,
28    },
29    msgbus::{self, TypedHandler},
30};
31use nautilus_core::UUID4;
32use nautilus_model::{
33    defi::{
34        Blockchain, DefiData, PoolProfiler,
35        data::{DexPoolData, block::BlockPosition},
36        pool_analysis::PoolSnapshot,
37    },
38    identifiers::{ClientId, InstrumentId},
39    instruments::{CurrencyPair, InstrumentAny},
40};
41
42use crate::engine::{
43    DataEngine,
44    pool::{
45        PoolCollectHandler, PoolFlashHandler, PoolLiquidityHandler, PoolSwapHandler, PoolUpdater,
46    },
47};
48
49/// Extracts the block position tuple from a `DexPoolData` event.
50fn get_event_block_position(event: &DexPoolData) -> (u64, u32, u32) {
51    match event {
52        DexPoolData::Swap(s) => (s.block, s.transaction_index, s.log_index),
53        DexPoolData::LiquidityUpdate(u) => (u.block, u.transaction_index, u.log_index),
54        DexPoolData::FeeCollect(c) => (c.block, c.transaction_index, c.log_index),
55        DexPoolData::FeeProtocolUpdate(u) => (u.block, u.transaction_index, u.log_index),
56        DexPoolData::FeeProtocolCollect(c) => (c.block, c.transaction_index, c.log_index),
57        DexPoolData::Flash(f) => (f.block, f.transaction_index, f.log_index),
58    }
59}
60
61/// Converts buffered `DefiData` events to `DexPoolData` and sorts by block position.
62fn convert_and_sort_buffered_events(buffered_events: Vec<DefiData>) -> Vec<DexPoolData> {
63    let mut events: Vec<DexPoolData> = buffered_events
64        .into_iter()
65        .filter_map(|event| match event {
66            DefiData::PoolSwap(swap) => Some(DexPoolData::Swap(swap)),
67            DefiData::PoolLiquidityUpdate(update) => Some(DexPoolData::LiquidityUpdate(update)),
68            DefiData::PoolFeeCollect(collect) => Some(DexPoolData::FeeCollect(collect)),
69            DefiData::PoolFeeProtocolUpdate(update) => Some(DexPoolData::FeeProtocolUpdate(update)),
70            DefiData::PoolFeeProtocolCollect(collect) => {
71                Some(DexPoolData::FeeProtocolCollect(collect))
72            }
73            DefiData::PoolFlash(flash) => Some(DexPoolData::Flash(flash)),
74            _ => None,
75        })
76        .collect();
77
78    events.sort_by(|a, b| {
79        let pos_a = get_event_block_position(a);
80        let pos_b = get_event_block_position(b);
81        pos_a.cmp(&pos_b)
82    });
83
84    events
85}
86
87impl DataEngine {
88    /// Returns all blockchains for which blocks subscriptions exist.
89    #[must_use]
90    pub fn subscribed_blocks(&self) -> Vec<Blockchain> {
91        self.collect_subscriptions(|client| &client.subscriptions_blocks)
92    }
93
94    /// Returns all instrument IDs for which pool subscriptions exist.
95    #[must_use]
96    pub fn subscribed_pools(&self) -> Vec<InstrumentId> {
97        self.collect_subscriptions(|client| &client.subscriptions_pools)
98    }
99
100    /// Returns all instrument IDs for which swap subscriptions exist.
101    #[must_use]
102    pub fn subscribed_pool_swaps(&self) -> Vec<InstrumentId> {
103        self.collect_subscriptions(|client| &client.subscriptions_pool_swaps)
104    }
105
106    /// Returns all instrument IDs for which liquidity update subscriptions exist.
107    #[must_use]
108    pub fn subscribed_pool_liquidity_updates(&self) -> Vec<InstrumentId> {
109        self.collect_subscriptions(|client| &client.subscriptions_pool_liquidity_updates)
110    }
111
112    /// Returns all instrument IDs for which fee collect subscriptions exist.
113    #[must_use]
114    pub fn subscribed_pool_fee_collects(&self) -> Vec<InstrumentId> {
115        self.collect_subscriptions(|client| &client.subscriptions_pool_fee_collects)
116    }
117
118    /// Returns all instrument IDs for which flash loan subscriptions exist.
119    #[must_use]
120    pub fn subscribed_pool_flash(&self) -> Vec<InstrumentId> {
121        self.collect_subscriptions(|client| &client.subscriptions_pool_flash)
122    }
123
124    /// Handles a subscribe command, updating internal state and forwarding to the client.
125    ///
126    /// # Errors
127    ///
128    /// Returns an error if the subscription is invalid (e.g., synthetic instrument for book data),
129    /// or if the underlying client operation fails.
130    pub fn execute_defi_subscribe(&mut self, cmd: DefiSubscribeCommand) -> anyhow::Result<()> {
131        if let Some(client_id) = cmd.client_id()
132            && self.is_external_client(*client_id)
133        {
134            if self.config().debug {
135                log::debug!("Skipping defi subscribe for external client {client_id}: {cmd:?}");
136            }
137            return Ok(());
138        }
139
140        let Some(client) = self.get_client(cmd.client_id(), cmd.venue()) else {
141            log::error!(
142                "Cannot handle command: no client found for client_id={:?}, venue={:?}",
143                cmd.client_id(),
144                cmd.venue(),
145            );
146            return Ok(());
147        };
148
149        #[rustfmt::skip]
150        let pool = match &cmd {
151            DefiSubscribeCommand::Pool(cmd) => Some((cmd.instrument_id, cmd.client_id)),
152            DefiSubscribeCommand::PoolSwaps(cmd) => Some((cmd.instrument_id, cmd.client_id)),
153            DefiSubscribeCommand::PoolLiquidityUpdates(cmd) => Some((cmd.instrument_id, cmd.client_id)),
154            DefiSubscribeCommand::PoolFeeCollects(cmd) => Some((cmd.instrument_id, cmd.client_id)),
155            DefiSubscribeCommand::PoolFlashEvents(cmd) => Some((cmd.instrument_id, cmd.client_id)),
156            DefiSubscribeCommand::Blocks(_) => None, // No pool setup needed for blocks
157        };
158
159        log::info!("Forwarding subscription to client {}", client.client_id);
160        client.execute_defi_subscribe_with_retained(cmd, true);
161
162        if let Some((instrument_id, client_id)) = pool {
163            self.setup_pool_updater(&instrument_id, client_id.as_ref());
164        }
165
166        Ok(())
167    }
168
169    /// Handles an unsubscribe command, updating internal state and forwarding to the client.
170    ///
171    /// # Errors
172    ///
173    /// Returns an error if the underlying client operation fails.
174    pub fn execute_defi_unsubscribe(&mut self, cmd: &DefiUnsubscribeCommand) -> anyhow::Result<()> {
175        if let Some(client_id) = cmd.client_id()
176            && self.is_external_client(*client_id)
177        {
178            if self.config().debug {
179                log::debug!("Skipping defi unsubscribe for external client {client_id}: {cmd:?}");
180            }
181            return Ok(());
182        }
183
184        let Some(client) = self.get_client(cmd.client_id(), cmd.venue()) else {
185            log::error!(
186                "Cannot handle command: no client found for client_id={:?}, venue={:?}",
187                cmd.client_id(),
188                cmd.venue(),
189            );
190            return Ok(());
191        };
192
193        client.execute_defi_unsubscribe(cmd);
194
195        #[rustfmt::skip]
196        let instrument_id = match cmd {
197            DefiUnsubscribeCommand::Pool(cmd) => cmd.instrument_id,
198            DefiUnsubscribeCommand::PoolSwaps(cmd) => cmd.instrument_id,
199            DefiUnsubscribeCommand::PoolLiquidityUpdates(cmd) => cmd.instrument_id,
200            DefiUnsubscribeCommand::PoolFeeCollects(cmd) => cmd.instrument_id,
201            DefiUnsubscribeCommand::PoolFlashEvents(cmd) => cmd.instrument_id,
202            DefiUnsubscribeCommand::Blocks(_) => return Ok(()),
203        };
204
205        // The updater serves every pool subscription shape on every client
206        if !self
207            .get_clients()
208            .iter()
209            .any(|client| client.has_defi_pool_demand(&instrument_id))
210        {
211            self.release_pool_updater(&instrument_id);
212        }
213
214        Ok(())
215    }
216
217    /// Sends a [`DefiRequestCommand`] to a suitable data client implementation.
218    ///
219    /// # Errors
220    ///
221    /// Returns an error if no client is found for the given client ID or venue,
222    /// or if the client fails to process the request.
223    pub fn execute_defi_request(&mut self, req: DefiRequestCommand) -> anyhow::Result<()> {
224        // Skip requests for external clients
225        if let Some(cid) = req.client_id()
226            && self.is_external_client(*cid)
227        {
228            if self.config().debug {
229                log::debug!("Skipping defi data request for external client {cid}: {req:?}");
230            }
231            return Ok(());
232        }
233
234        if let Some(client) = self.get_client(req.client_id(), req.venue()) {
235            let client_id = client.client_id();
236            client
237                .execute_defi_request(req)
238                .map_err(|e| e.context(format!("Data client {client_id} request failed")))
239        } else {
240            anyhow::bail!(
241                "Cannot handle request: no client found for {:?} {:?}",
242                req.client_id(),
243                req.venue()
244            );
245        }
246    }
247
248    /// Processes DeFi-specific data events.
249    pub fn process_defi_data(&mut self, data: DefiData) {
250        self.increment_data_count();
251
252        match data {
253            DefiData::Block(block) => {
254                let topic = defi::switchboard::get_defi_blocks_topic(block.chain());
255                msgbus::publish_defi_block(topic, &block);
256            }
257            DefiData::Pool(pool) => {
258                if let Err(e) = self.cache().borrow_mut().add_pool(pool.clone()) {
259                    log::error!("Failed to add Pool to cache: {e}");
260                }
261
262                match CurrencyPair::try_from(&pool) {
263                    Ok(instrument) => {
264                        self.handle_instrument(&InstrumentAny::CurrencyPair(instrument));
265                    }
266                    Err(e) => {
267                        log::error!(
268                            "Failed to create instrument for Pool {}: {e}",
269                            pool.instrument_id
270                        );
271                    }
272                }
273
274                let topic = defi::switchboard::get_defi_pool_topic(pool.instrument_id);
275                msgbus::publish_defi_pool(topic, &pool);
276            }
277            DefiData::PoolSnapshot(snapshot) => {
278                // Replayed snapshots carry no request identity, so any pending bootstrap accepts them
279                if !self
280                    .pool_snapshot_pending
281                    .contains_key(&snapshot.instrument_id)
282                {
283                    log::warn!(
284                        "Received unexpected pool snapshot for {} (not in pending set)",
285                        snapshot.instrument_id
286                    );
287                    return;
288                }
289
290                self.hydrate_pool_profiler(&snapshot);
291            }
292            DefiData::PoolSwap(swap) => {
293                let instrument_id = swap.instrument_id;
294                // Subscribers receive events immediately; keep a copy for profiler hydration
295                // while the snapshot is pending.
296                if self.pool_snapshot_pending.contains_key(&instrument_id) {
297                    log::debug!("Buffering swap event for {instrument_id} (waiting for snapshot)");
298                    self.pool_event_buffers
299                        .entry(instrument_id)
300                        .or_default()
301                        .push(DefiData::PoolSwap(swap.clone()));
302                }
303
304                let topic = defi::switchboard::get_defi_pool_swaps_topic(instrument_id);
305                msgbus::publish_defi_swap(topic, &swap);
306            }
307            DefiData::PoolLiquidityUpdate(update) => {
308                let instrument_id = update.instrument_id;
309                // Subscribers receive events immediately; keep a copy for profiler hydration
310                // while the snapshot is pending.
311                if self.pool_snapshot_pending.contains_key(&instrument_id) {
312                    log::debug!(
313                        "Buffering liquidity update event for {instrument_id} (waiting for snapshot)"
314                    );
315                    self.pool_event_buffers
316                        .entry(instrument_id)
317                        .or_default()
318                        .push(DefiData::PoolLiquidityUpdate(update.clone()));
319                }
320
321                let topic = defi::switchboard::get_defi_liquidity_topic(instrument_id);
322                msgbus::publish_defi_liquidity(topic, &update);
323            }
324            DefiData::PoolFeeCollect(collect) => {
325                let instrument_id = collect.instrument_id;
326                // Subscribers receive events immediately; keep a copy for profiler hydration
327                // while the snapshot is pending.
328                if self.pool_snapshot_pending.contains_key(&instrument_id) {
329                    log::debug!(
330                        "Buffering fee collect event for {instrument_id} (waiting for snapshot)"
331                    );
332                    self.pool_event_buffers
333                        .entry(instrument_id)
334                        .or_default()
335                        .push(DefiData::PoolFeeCollect(collect.clone()));
336                }
337
338                let topic = defi::switchboard::get_defi_collect_topic(instrument_id);
339                msgbus::publish_defi_collect(topic, &collect);
340            }
341            DefiData::PoolFeeProtocolUpdate(update) => {
342                let instrument_id = update.instrument_id;
343                // A protocol-fee change is profiler infrastructure, not a subscriber data stream:
344                // it only needs to keep the profiler's `fee_protocol` consistent for swap and flash
345                // fee splitting. Buffer until the snapshot lands, otherwise apply in place.
346                if self.pool_snapshot_pending.contains_key(&instrument_id) {
347                    log::debug!(
348                        "Buffering fee protocol update for {instrument_id} (waiting for snapshot)"
349                    );
350                    self.pool_event_buffers
351                        .entry(instrument_id)
352                        .or_default()
353                        .push(DefiData::PoolFeeProtocolUpdate(update));
354                } else if let Some(profiler) =
355                    self.cache().borrow_mut().pool_profiler_mut(&instrument_id)
356                    && let Err(e) = profiler.process_fee_protocol_update(&update)
357                {
358                    log::error!("Failed to process pool fee protocol update: {e}");
359                }
360            }
361            DefiData::PoolFeeProtocolCollect(collect) => {
362                let instrument_id = collect.instrument_id;
363                // A protocol-fee withdrawal is profiler infrastructure, not a subscriber data
364                // stream: it only decrements the profiler's accrued protocol-fee balances. Buffer
365                // until the snapshot lands, otherwise apply in place.
366                if self.pool_snapshot_pending.contains_key(&instrument_id) {
367                    log::debug!(
368                        "Buffering fee protocol collect event for {instrument_id} (waiting for snapshot)"
369                    );
370                    self.pool_event_buffers
371                        .entry(instrument_id)
372                        .or_default()
373                        .push(DefiData::PoolFeeProtocolCollect(collect));
374                } else if let Some(profiler) =
375                    self.cache().borrow_mut().pool_profiler_mut(&instrument_id)
376                    && let Err(e) = profiler.process_fee_protocol_collect(&collect)
377                {
378                    log::error!("Failed to process pool fee protocol collect event: {e}");
379                }
380            }
381            DefiData::PoolFlash(flash) => {
382                let instrument_id = flash.instrument_id;
383                // Subscribers receive events immediately; keep a copy for profiler hydration
384                // while the snapshot is pending.
385                if self.pool_snapshot_pending.contains_key(&instrument_id) {
386                    log::debug!("Buffering flash event for {instrument_id} (waiting for snapshot)");
387                    self.pool_event_buffers
388                        .entry(instrument_id)
389                        .or_default()
390                        .push(DefiData::PoolFlash(flash.clone()));
391                }
392
393                let topic = defi::switchboard::get_defi_flash_topic(instrument_id);
394                msgbus::publish_defi_flash(topic, &flash);
395            }
396        }
397    }
398
399    pub(crate) fn handle_pool_snapshot_response(&mut self, response: &PoolSnapshotResponse) {
400        let instrument_id = response.data.instrument_id;
401
402        if self.pool_snapshot_pending.get(&instrument_id) != Some(&response.correlation_id) {
403            log::warn!(
404                "Discarding pool snapshot for {instrument_id} from stale request {}",
405                response.correlation_id
406            );
407            return;
408        }
409
410        self.hydrate_pool_profiler(&response.data);
411    }
412
413    fn hydrate_pool_profiler(&mut self, snapshot: &PoolSnapshot) {
414        let instrument_id = snapshot.instrument_id;
415        log::info!(
416            "Received pool snapshot for {instrument_id} at block {} with {} positions and {} ticks",
417            snapshot.block_position.number,
418            snapshot.positions.len(),
419            snapshot.ticks.len()
420        );
421
422        // Get pool from cache
423        let pool = self
424            .cache()
425            .borrow()
426            .pool(&instrument_id)
427            .map(|pool| Arc::new(pool.clone()));
428
429        let Some(pool) = pool else {
430            log::error!("Pool {instrument_id} not found in cache when processing snapshot");
431            self.abandon_pool_snapshot(&instrument_id);
432            return;
433        };
434
435        // Defensive: refuse stub snapshots that slipped past the bootstrap-side
436        // guards. Installing one would leave Python actors observing an initialized
437        // profiler with zero liquidity; better to leave the pool without a profiler
438        // so the bad state is visible.
439        if snapshot.positions.is_empty()
440            && snapshot.ticks.is_empty()
441            && snapshot.block_position.number == pool.creation_block
442        {
443            log::warn!(
444                "Refusing empty stub snapshot for {instrument_id} at pool creation block {}; pool will remain without profiler",
445                snapshot.block_position.number,
446            );
447            self.abandon_pool_snapshot(&instrument_id);
448            return;
449        }
450
451        // Create profiler and restore from snapshot
452        let mut profiler = PoolProfiler::new(pool);
453        if let Err(e) = profiler.restore_from_snapshot(snapshot.clone()) {
454            log::error!("Failed to restore profiler from snapshot for {instrument_id}: {e}");
455            self.abandon_pool_snapshot(&instrument_id);
456            return;
457        }
458
459        log::debug!("Restored pool profiler for {instrument_id} from snapshot");
460
461        // Process buffered events
462        let buffered_events = self
463            .pool_event_buffers
464            .remove(&instrument_id)
465            .unwrap_or_default();
466
467        if !buffered_events.is_empty() {
468            log::info!(
469                "Processing {} buffered events for {instrument_id}",
470                buffered_events.len()
471            );
472
473            let events_to_apply = convert_and_sort_buffered_events(buffered_events);
474            let applied_count = Self::apply_buffered_events_to_profiler(
475                &mut profiler,
476                events_to_apply,
477                &snapshot.block_position,
478                instrument_id,
479            );
480
481            log::info!("Applied {applied_count} buffered events to profiler for {instrument_id}");
482        }
483
484        // Add profiler to cache
485        let result = self.cache().borrow_mut().add_pool_profiler(profiler);
486        if let Err(e) = result {
487            log::error!("Failed to add pool profiler to cache for {instrument_id}: {e}");
488            self.abandon_pool_snapshot(&instrument_id);
489            return;
490        }
491
492        // Create updater and subscribe to topics
493        self.pool_snapshot_pending.remove(&instrument_id);
494        let updater = Rc::new(PoolUpdater::new(&instrument_id, Rc::clone(self.cache())));
495
496        self.subscribe_pool_updater_topics(instrument_id, Rc::clone(&updater));
497        self.pool_updaters.insert(instrument_id, updater);
498
499        log::info!("Pool profiler setup completed for {instrument_id}, now processing live events");
500    }
501
502    fn abandon_pool_snapshot(&mut self, instrument_id: &InstrumentId) {
503        self.pool_snapshot_pending.remove(instrument_id);
504        self.pool_event_buffers.remove(instrument_id);
505    }
506
507    /// Subscribes a pool updater to all relevant pool data topics using typed handlers.
508    fn subscribe_pool_updater_topics(&self, instrument_id: InstrumentId, updater: Rc<PoolUpdater>) {
509        let priority = Some(self.msgbus_priority());
510
511        // Subscribe swap handler
512        let swap_topic = defi::switchboard::get_defi_pool_swaps_topic(instrument_id);
513        let swap_handler = TypedHandler(Rc::new(PoolSwapHandler::new(Rc::clone(&updater))));
514        msgbus::subscribe_defi_swaps(swap_topic.into(), swap_handler, priority);
515
516        // Subscribe liquidity handler
517        let liq_topic = defi::switchboard::get_defi_liquidity_topic(instrument_id);
518        let liq_handler = TypedHandler(Rc::new(PoolLiquidityHandler::new(Rc::clone(&updater))));
519        msgbus::subscribe_defi_liquidity(liq_topic.into(), liq_handler, priority);
520
521        // Subscribe collect handler
522        let collect_topic = defi::switchboard::get_defi_collect_topic(instrument_id);
523        let collect_handler = TypedHandler(Rc::new(PoolCollectHandler::new(Rc::clone(&updater))));
524        msgbus::subscribe_defi_collects(collect_topic.into(), collect_handler, priority);
525
526        // Subscribe flash handler
527        let flash_topic = defi::switchboard::get_defi_flash_topic(instrument_id);
528        let flash_handler = TypedHandler(Rc::new(PoolFlashHandler::new(updater)));
529        msgbus::subscribe_defi_flash(flash_topic.into(), flash_handler, priority);
530    }
531
532    /// Applies buffered events to a pool profiler, filtering to events after the snapshot.
533    ///
534    /// Returns the count of successfully applied events.
535    fn apply_buffered_events_to_profiler(
536        profiler: &mut PoolProfiler,
537        events: Vec<DexPoolData>,
538        snapshot_block: &BlockPosition,
539        instrument_id: InstrumentId,
540    ) -> usize {
541        let mut applied_count = 0;
542
543        for event in events {
544            let event_block = get_event_block_position(&event);
545
546            // Only apply events that occurred after the snapshot
547            let is_after_snapshot = event_block.0 > snapshot_block.number
548                || (event_block.0 == snapshot_block.number
549                    && event_block.1 > snapshot_block.transaction_index)
550                || (event_block.0 == snapshot_block.number
551                    && event_block.1 == snapshot_block.transaction_index
552                    && event_block.2 > snapshot_block.log_index);
553
554            if is_after_snapshot {
555                if let Err(e) = profiler.process(&event) {
556                    log::error!(
557                        "Failed to apply buffered event to profiler for {instrument_id}: {e}"
558                    );
559                } else {
560                    applied_count += 1;
561                }
562            }
563        }
564
565        applied_count
566    }
567
568    fn setup_pool_updater(&mut self, instrument_id: &InstrumentId, client_id: Option<&ClientId>) {
569        // Early return if updater already exists or we are in the middle of setting it up.
570        if self.pool_updaters.contains_key(instrument_id)
571            || self.pool_snapshot_pending.contains_key(instrument_id)
572        {
573            log::debug!("Pool updater for {instrument_id} already exists");
574            return;
575        }
576
577        log::info!("Setting up pool updater for {instrument_id}");
578
579        // Check cache state and ensure profiler exists
580        {
581            let mut cache = self.cache().borrow_mut();
582
583            if cache.pool_profiler(instrument_id).is_some() {
584                // Profiler already exists, proceed to create updater
585                log::debug!("Pool profiler already exists for {instrument_id}");
586            } else if let Some(pool) = cache.pool(instrument_id) {
587                // Pool exists but no profiler, create profiler from pool
588                let pool = Arc::new(pool.clone());
589                let mut pool_profiler = PoolProfiler::new(Arc::clone(&pool));
590
591                if let Some(initial_sqrt_price_x96) = pool.initial_sqrt_price_x96 {
592                    if let Err(e) = pool_profiler.initialize(initial_sqrt_price_x96) {
593                        log::error!("Failed to initialize pool profiler for {instrument_id}: {e}");
594                        drop(cache);
595                        return;
596                    }
597                    log::debug!(
598                        "Initialized pool profiler for {instrument_id} with sqrt_price {initial_sqrt_price_x96}"
599                    );
600                } else {
601                    log::debug!("Created pool profiler for {instrument_id}");
602                }
603
604                if let Err(e) = cache.add_pool_profiler(pool_profiler) {
605                    log::error!("Failed to add pool profiler for {instrument_id}: {e}");
606                    drop(cache);
607                    return;
608                }
609                drop(cache);
610            } else {
611                // Neither profiler nor pool exists, request snapshot
612                drop(cache);
613
614                let request_id = UUID4::new();
615                let ts_init = self.clock().borrow().timestamp_ns();
616
617                let request = RequestPoolSnapshot::new(
618                    *instrument_id,
619                    client_id.copied(),
620                    request_id,
621                    ts_init,
622                    None,
623                );
624
625                if let Err(e) = self.execute_defi_request(DefiRequestCommand::PoolSnapshot(request))
626                {
627                    log::warn!("Failed to request pool snapshot for {instrument_id}: {e}");
628                } else {
629                    log::debug!("Requested pool snapshot for {instrument_id}");
630                    self.pool_snapshot_pending
631                        .insert(*instrument_id, request_id);
632                    self.pool_event_buffers.entry(*instrument_id).or_default();
633                }
634                return;
635            }
636        }
637
638        // Profiler exists, create updater and subscribe to topics
639        let updater = Rc::new(PoolUpdater::new(instrument_id, Rc::clone(self.cache())));
640
641        self.subscribe_pool_updater_topics(*instrument_id, Rc::clone(&updater));
642        self.pool_updaters.insert(*instrument_id, updater);
643
644        log::debug!("Created PoolUpdater for instrument ID {instrument_id}");
645    }
646
647    fn release_pool_updater(&mut self, instrument_id: &InstrumentId) {
648        if let Some(updater) = self.pool_updaters.remove(instrument_id) {
649            Self::unsubscribe_pool_updater_topics(*instrument_id, updater);
650            log::debug!("Removed PoolUpdater for instrument ID {instrument_id}");
651        }
652
653        self.abandon_pool_snapshot(instrument_id);
654    }
655
656    pub(crate) fn clear_pool_updaters(&mut self) {
657        let updaters: Vec<_> = self.pool_updaters.drain().collect();
658        for (instrument_id, updater) in updaters {
659            Self::unsubscribe_pool_updater_topics(instrument_id, updater);
660        }
661
662        self.abandon_pool_snapshots();
663    }
664
665    pub(crate) fn abandon_pool_snapshots(&mut self) {
666        self.pool_snapshot_pending.clear();
667        self.pool_event_buffers.clear();
668    }
669
670    fn unsubscribe_pool_updater_topics(instrument_id: InstrumentId, updater: Rc<PoolUpdater>) {
671        let swap_topic = defi::switchboard::get_defi_pool_swaps_topic(instrument_id);
672        let swap_handler = TypedHandler(Rc::new(PoolSwapHandler::new(Rc::clone(&updater))));
673        msgbus::unsubscribe_defi_swaps(swap_topic.into(), &swap_handler);
674
675        let liq_topic = defi::switchboard::get_defi_liquidity_topic(instrument_id);
676        let liq_handler = TypedHandler(Rc::new(PoolLiquidityHandler::new(Rc::clone(&updater))));
677        msgbus::unsubscribe_defi_liquidity(liq_topic.into(), &liq_handler);
678
679        let collect_topic = defi::switchboard::get_defi_collect_topic(instrument_id);
680        let collect_handler = TypedHandler(Rc::new(PoolCollectHandler::new(Rc::clone(&updater))));
681        msgbus::unsubscribe_defi_collects(collect_topic.into(), &collect_handler);
682
683        let flash_topic = defi::switchboard::get_defi_flash_topic(instrument_id);
684        let flash_handler = TypedHandler(Rc::new(PoolFlashHandler::new(updater)));
685        msgbus::unsubscribe_defi_flash(flash_topic.into(), &flash_handler);
686    }
687}
688
689#[cfg(test)]
690mod tests {
691    use std::sync::Arc;
692
693    use alloy_primitives::{Address, I256, U160, U256};
694    use nautilus_core::UnixNanos;
695    use nautilus_model::{
696        defi::{
697            Chain, DefiData, PoolFeeCollect, PoolFeeProtocolUpdate, PoolFlash, PoolIdentifier,
698            PoolLiquidityUpdate, PoolLiquidityUpdateType, PoolSwap,
699            chain::chains,
700            data::DexPoolData,
701            dex::{AmmType, Dex, DexType},
702        },
703        identifiers::{InstrumentId, Symbol, Venue},
704    };
705    use rstest::*;
706
707    use super::*;
708
709    #[fixture]
710    fn test_instrument_id() -> InstrumentId {
711        InstrumentId::new(Symbol::from("ETH/USDC"), Venue::from("UNISWAPV3"))
712    }
713
714    #[fixture]
715    fn test_chain() -> Arc<Chain> {
716        Arc::new(chains::ETHEREUM.clone())
717    }
718
719    #[fixture]
720    fn test_dex(test_chain: Arc<Chain>) -> Arc<Dex> {
721        Arc::new(Dex::new(
722            (*test_chain).clone(),
723            DexType::UniswapV3,
724            "0x1F98431c8aD98523631AE4a59f267346ea31F984",
725            12369621,
726            AmmType::CLAMM,
727            "PoolCreated(address,address,uint24,int24,address)",
728            "Swap(address,address,int256,int256,uint160,uint128,int24)",
729            "Mint(address,address,int24,int24,uint128,uint256,uint256)",
730            "Burn(address,int24,int24,uint128,uint256,uint256)",
731            "Collect(address,address,int24,int24,uint128,uint128)",
732        ))
733    }
734
735    fn create_test_swap(
736        test_instrument_id: InstrumentId,
737        test_chain: Arc<Chain>,
738        test_dex: Arc<Dex>,
739        block: u64,
740        tx_index: u32,
741        log_index: u32,
742    ) -> PoolSwap {
743        PoolSwap::new(
744            test_chain,
745            test_dex,
746            test_instrument_id,
747            PoolIdentifier::from_address(Address::ZERO),
748            block,
749            format!("0x{block:064x}"),
750            tx_index,
751            log_index,
752            UnixNanos::default(),
753            UnixNanos::default(),
754            Address::ZERO,
755            Address::ZERO,
756            I256::ZERO,
757            I256::ZERO,
758            U160::ZERO,
759            0,
760            0,
761        )
762    }
763
764    fn create_test_liquidity_update(
765        test_instrument_id: InstrumentId,
766        test_chain: Arc<Chain>,
767        test_dex: Arc<Dex>,
768        block: u64,
769        tx_index: u32,
770        log_index: u32,
771    ) -> PoolLiquidityUpdate {
772        PoolLiquidityUpdate::new(
773            test_chain,
774            test_dex,
775            test_instrument_id,
776            PoolIdentifier::from_address(Address::ZERO),
777            PoolLiquidityUpdateType::Mint,
778            block,
779            format!("0x{block:064x}"),
780            tx_index,
781            log_index,
782            None,
783            Address::ZERO,
784            0,
785            U256::ZERO,
786            U256::ZERO,
787            0,
788            0,
789            UnixNanos::default(),
790            UnixNanos::default(),
791        )
792    }
793
794    fn create_test_fee_collect(
795        test_instrument_id: InstrumentId,
796        test_chain: Arc<Chain>,
797        test_dex: Arc<Dex>,
798        block: u64,
799        tx_index: u32,
800        log_index: u32,
801    ) -> PoolFeeCollect {
802        PoolFeeCollect::new(
803            test_chain,
804            test_dex,
805            test_instrument_id,
806            PoolIdentifier::from_address(Address::ZERO),
807            block,
808            format!("0x{block:064x}"),
809            tx_index,
810            log_index,
811            Address::ZERO,
812            0,
813            0,
814            0,
815            0,
816            UnixNanos::default(),
817            UnixNanos::default(),
818        )
819    }
820
821    fn create_test_flash(
822        test_instrument_id: InstrumentId,
823        test_chain: Arc<Chain>,
824        test_dex: Arc<Dex>,
825        block: u64,
826        tx_index: u32,
827        log_index: u32,
828    ) -> PoolFlash {
829        PoolFlash::new(
830            test_chain,
831            test_dex,
832            test_instrument_id,
833            PoolIdentifier::from_address(Address::ZERO),
834            block,
835            format!("0x{block:064x}"),
836            tx_index,
837            log_index,
838            UnixNanos::default(),
839            UnixNanos::default(),
840            Address::ZERO,
841            Address::ZERO,
842            U256::ZERO,
843            U256::ZERO,
844            U256::ZERO,
845            U256::ZERO,
846        )
847    }
848
849    fn create_test_fee_protocol_update(
850        test_instrument_id: InstrumentId,
851        test_chain: Arc<Chain>,
852        test_dex: Arc<Dex>,
853        block: u64,
854        tx_index: u32,
855        log_index: u32,
856    ) -> PoolFeeProtocolUpdate {
857        PoolFeeProtocolUpdate::new(
858            test_chain,
859            test_dex,
860            test_instrument_id,
861            PoolIdentifier::from_address(Address::ZERO),
862            block,
863            format!("0x{block:064x}"),
864            tx_index,
865            log_index,
866            4,
867            4,
868            UnixNanos::default(),
869            UnixNanos::default(),
870        )
871    }
872
873    #[rstest]
874    fn test_get_event_block_position_swap(
875        test_instrument_id: InstrumentId,
876        test_chain: Arc<Chain>,
877        test_dex: Arc<Dex>,
878    ) {
879        let swap = create_test_swap(test_instrument_id, test_chain, test_dex, 100, 5, 3);
880        let pos = get_event_block_position(&DexPoolData::Swap(swap));
881        assert_eq!(pos, (100, 5, 3));
882    }
883
884    #[rstest]
885    fn test_get_event_block_position_liquidity_update(
886        test_instrument_id: InstrumentId,
887        test_chain: Arc<Chain>,
888        test_dex: Arc<Dex>,
889    ) {
890        let update =
891            create_test_liquidity_update(test_instrument_id, test_chain, test_dex, 200, 10, 7);
892        let pos = get_event_block_position(&DexPoolData::LiquidityUpdate(update));
893        assert_eq!(pos, (200, 10, 7));
894    }
895
896    #[rstest]
897    fn test_get_event_block_position_fee_collect(
898        test_instrument_id: InstrumentId,
899        test_chain: Arc<Chain>,
900        test_dex: Arc<Dex>,
901    ) {
902        let collect = create_test_fee_collect(test_instrument_id, test_chain, test_dex, 300, 15, 2);
903        let pos = get_event_block_position(&DexPoolData::FeeCollect(collect));
904        assert_eq!(pos, (300, 15, 2));
905    }
906
907    #[rstest]
908    fn test_get_event_block_position_flash(
909        test_instrument_id: InstrumentId,
910        test_chain: Arc<Chain>,
911        test_dex: Arc<Dex>,
912    ) {
913        let flash = create_test_flash(test_instrument_id, test_chain, test_dex, 400, 20, 8);
914        let pos = get_event_block_position(&DexPoolData::Flash(flash));
915        assert_eq!(pos, (400, 20, 8));
916    }
917
918    #[rstest]
919    fn test_get_event_block_position_fee_protocol_update(
920        test_instrument_id: InstrumentId,
921        test_chain: Arc<Chain>,
922        test_dex: Arc<Dex>,
923    ) {
924        let update =
925            create_test_fee_protocol_update(test_instrument_id, test_chain, test_dex, 500, 25, 4);
926        let pos = get_event_block_position(&DexPoolData::FeeProtocolUpdate(update));
927        assert_eq!(pos, (500, 25, 4));
928    }
929
930    #[rstest]
931    fn test_convert_and_sort_empty_events() {
932        let events = convert_and_sort_buffered_events(vec![]);
933        assert!(events.is_empty());
934    }
935
936    #[rstest]
937    fn test_convert_and_sort_filters_non_pool_events(
938        test_instrument_id: InstrumentId,
939        test_chain: Arc<Chain>,
940        test_dex: Arc<Dex>,
941    ) {
942        let events = vec![
943            DefiData::PoolSwap(create_test_swap(
944                test_instrument_id,
945                test_chain,
946                test_dex,
947                100,
948                0,
949                0,
950            )),
951            // Block events would be filtered out
952        ];
953        let sorted = convert_and_sort_buffered_events(events);
954        assert_eq!(sorted.len(), 1);
955    }
956
957    #[rstest]
958    fn test_convert_and_sort_single_event(
959        test_instrument_id: InstrumentId,
960        test_chain: Arc<Chain>,
961        test_dex: Arc<Dex>,
962    ) {
963        let swap = create_test_swap(test_instrument_id, test_chain, test_dex, 100, 5, 3);
964        let events = vec![DefiData::PoolSwap(swap)];
965        let sorted = convert_and_sort_buffered_events(events);
966        assert_eq!(sorted.len(), 1);
967        assert_eq!(get_event_block_position(&sorted[0]), (100, 5, 3));
968    }
969
970    #[rstest]
971    fn test_convert_and_sort_already_sorted(
972        test_instrument_id: InstrumentId,
973        test_chain: Arc<Chain>,
974        test_dex: Arc<Dex>,
975    ) {
976        let events = vec![
977            DefiData::PoolSwap(create_test_swap(
978                test_instrument_id,
979                Arc::clone(&test_chain),
980                Arc::clone(&test_dex),
981                100,
982                0,
983                0,
984            )),
985            DefiData::PoolSwap(create_test_swap(
986                test_instrument_id,
987                Arc::clone(&test_chain),
988                Arc::clone(&test_dex),
989                100,
990                0,
991                1,
992            )),
993            DefiData::PoolSwap(create_test_swap(
994                test_instrument_id,
995                test_chain,
996                test_dex,
997                100,
998                1,
999                0,
1000            )),
1001        ];
1002        let sorted = convert_and_sort_buffered_events(events);
1003        assert_eq!(sorted.len(), 3);
1004        assert_eq!(get_event_block_position(&sorted[0]), (100, 0, 0));
1005        assert_eq!(get_event_block_position(&sorted[1]), (100, 0, 1));
1006        assert_eq!(get_event_block_position(&sorted[2]), (100, 1, 0));
1007    }
1008
1009    #[rstest]
1010    fn test_convert_and_sort_reverse_order(
1011        test_instrument_id: InstrumentId,
1012        test_chain: Arc<Chain>,
1013        test_dex: Arc<Dex>,
1014    ) {
1015        let events = vec![
1016            DefiData::PoolSwap(create_test_swap(
1017                test_instrument_id,
1018                Arc::clone(&test_chain),
1019                Arc::clone(&test_dex),
1020                100,
1021                2,
1022                5,
1023            )),
1024            DefiData::PoolSwap(create_test_swap(
1025                test_instrument_id,
1026                Arc::clone(&test_chain),
1027                Arc::clone(&test_dex),
1028                100,
1029                1,
1030                3,
1031            )),
1032            DefiData::PoolSwap(create_test_swap(
1033                test_instrument_id,
1034                test_chain,
1035                test_dex,
1036                100,
1037                0,
1038                1,
1039            )),
1040        ];
1041        let sorted = convert_and_sort_buffered_events(events);
1042        assert_eq!(sorted.len(), 3);
1043        assert_eq!(get_event_block_position(&sorted[0]), (100, 0, 1));
1044        assert_eq!(get_event_block_position(&sorted[1]), (100, 1, 3));
1045        assert_eq!(get_event_block_position(&sorted[2]), (100, 2, 5));
1046    }
1047
1048    #[rstest]
1049    fn test_convert_and_sort_mixed_blocks(
1050        test_instrument_id: InstrumentId,
1051        test_chain: Arc<Chain>,
1052        test_dex: Arc<Dex>,
1053    ) {
1054        let events = vec![
1055            DefiData::PoolSwap(create_test_swap(
1056                test_instrument_id,
1057                Arc::clone(&test_chain),
1058                Arc::clone(&test_dex),
1059                102,
1060                0,
1061                0,
1062            )),
1063            DefiData::PoolSwap(create_test_swap(
1064                test_instrument_id,
1065                Arc::clone(&test_chain),
1066                Arc::clone(&test_dex),
1067                100,
1068                5,
1069                2,
1070            )),
1071            DefiData::PoolSwap(create_test_swap(
1072                test_instrument_id,
1073                test_chain,
1074                test_dex,
1075                101,
1076                3,
1077                1,
1078            )),
1079        ];
1080        let sorted = convert_and_sort_buffered_events(events);
1081        assert_eq!(sorted.len(), 3);
1082        assert_eq!(get_event_block_position(&sorted[0]), (100, 5, 2));
1083        assert_eq!(get_event_block_position(&sorted[1]), (101, 3, 1));
1084        assert_eq!(get_event_block_position(&sorted[2]), (102, 0, 0));
1085    }
1086
1087    #[rstest]
1088    fn test_convert_and_sort_mixed_event_types(
1089        test_instrument_id: InstrumentId,
1090        test_chain: Arc<Chain>,
1091        test_dex: Arc<Dex>,
1092    ) {
1093        let events = vec![
1094            DefiData::PoolSwap(create_test_swap(
1095                test_instrument_id,
1096                Arc::clone(&test_chain),
1097                Arc::clone(&test_dex),
1098                100,
1099                2,
1100                0,
1101            )),
1102            DefiData::PoolLiquidityUpdate(create_test_liquidity_update(
1103                test_instrument_id,
1104                Arc::clone(&test_chain),
1105                Arc::clone(&test_dex),
1106                100,
1107                0,
1108                0,
1109            )),
1110            DefiData::PoolFeeCollect(create_test_fee_collect(
1111                test_instrument_id,
1112                Arc::clone(&test_chain),
1113                Arc::clone(&test_dex),
1114                100,
1115                1,
1116                0,
1117            )),
1118            DefiData::PoolFlash(create_test_flash(
1119                test_instrument_id,
1120                Arc::clone(&test_chain),
1121                Arc::clone(&test_dex),
1122                100,
1123                3,
1124                0,
1125            )),
1126            DefiData::PoolFeeProtocolUpdate(create_test_fee_protocol_update(
1127                test_instrument_id,
1128                test_chain,
1129                test_dex,
1130                100,
1131                4,
1132                0,
1133            )),
1134        ];
1135        let sorted = convert_and_sort_buffered_events(events);
1136        assert_eq!(sorted.len(), 5);
1137        assert_eq!(get_event_block_position(&sorted[0]), (100, 0, 0));
1138        assert_eq!(get_event_block_position(&sorted[1]), (100, 1, 0));
1139        assert_eq!(get_event_block_position(&sorted[2]), (100, 2, 0));
1140        assert_eq!(get_event_block_position(&sorted[3]), (100, 3, 0));
1141        assert_eq!(get_event_block_position(&sorted[4]), (100, 4, 0));
1142        assert!(matches!(sorted[4], DexPoolData::FeeProtocolUpdate(_)));
1143    }
1144
1145    #[rstest]
1146    fn test_convert_and_sort_same_block_and_tx_different_log_index(
1147        test_instrument_id: InstrumentId,
1148        test_chain: Arc<Chain>,
1149        test_dex: Arc<Dex>,
1150    ) {
1151        let events = vec![
1152            DefiData::PoolSwap(create_test_swap(
1153                test_instrument_id,
1154                Arc::clone(&test_chain),
1155                Arc::clone(&test_dex),
1156                100,
1157                5,
1158                10,
1159            )),
1160            DefiData::PoolSwap(create_test_swap(
1161                test_instrument_id,
1162                Arc::clone(&test_chain),
1163                Arc::clone(&test_dex),
1164                100,
1165                5,
1166                5,
1167            )),
1168            DefiData::PoolSwap(create_test_swap(
1169                test_instrument_id,
1170                test_chain,
1171                test_dex,
1172                100,
1173                5,
1174                1,
1175            )),
1176        ];
1177        let sorted = convert_and_sort_buffered_events(events);
1178        assert_eq!(sorted.len(), 3);
1179        assert_eq!(get_event_block_position(&sorted[0]), (100, 5, 1));
1180        assert_eq!(get_event_block_position(&sorted[1]), (100, 5, 5));
1181        assert_eq!(get_event_block_position(&sorted[2]), (100, 5, 10));
1182    }
1183}