Skip to main content

nautilus_execution/engine/
settlement.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//! Binary-option settlement from authoritative contract-expiration closes.
17//!
18//! The [`ExecutionEngine`](super::ExecutionEngine) applies these rules to closes it receives, and
19//! event-store cache replay applies the same rules to captured closes.
20
21use nautilus_common::cache::Cache;
22use nautilus_model::{
23    data::InstrumentClose,
24    enums::InstrumentCloseType,
25    identifiers::{InstrumentId, PositionId},
26    instruments::InstrumentAny,
27    position::Position,
28    types::Quantity,
29};
30
31/// Settles every open position in the binary option named by a contract-expiration `close`.
32///
33/// The first contract-expiration close cached for the instrument is authoritative. A later close
34/// settles any position still open on the cached terms, and a conflicting price is logged. Returns
35/// a transient copy of each settled position, without stored history, with the quantity the
36/// settlement closed. The list is empty when `close` is not a contract expiration for a cached
37/// binary option. A position that cannot be settled is logged and skipped.
38///
39/// # Errors
40///
41/// Returns an error if caching the close fails.
42pub fn settle_instrument_close(
43    cache: &mut Cache,
44    close: InstrumentClose,
45) -> anyhow::Result<Vec<(Position, Quantity)>> {
46    let instrument_id = close.instrument_id;
47
48    if close.close_type != InstrumentCloseType::ContractExpired
49        || !matches!(
50            cache.instrument(&instrument_id),
51            Some(InstrumentAny::BinaryOption(_))
52        )
53    {
54        return Ok(Vec::new());
55    }
56
57    let close = if let Some(settled) = settlement_close(cache, &instrument_id) {
58        if settled.close_price != close.close_price {
59            log::error!(
60                "Ignoring conflicting instrument close for {instrument_id}: settled at {}, received {}",
61                settled.close_price,
62                close.close_price,
63            );
64        }
65
66        settled
67    } else {
68        cache.add_instrument_close(close)?;
69        close
70    };
71
72    let positions: Vec<(PositionId, Quantity)> = cache
73        .positions_open(None, Some(&instrument_id), None, None, None)
74        .iter()
75        .map(|position| (position.id, position.quantity))
76        .collect();
77    let mut settled = Vec::with_capacity(positions.len());
78
79    for (position_id, last_qty) in positions {
80        match cache.update_position_from_instrument_close(position_id, close) {
81            Ok(position) => settled.push((position, last_qty)),
82            Err(e) => log::error!("Cannot settle position {position_id}: {e}"),
83        }
84    }
85
86    Ok(settled)
87}
88
89/// Returns the contract-expiration close that settled binary option `instrument_id`, if any.
90#[must_use]
91pub fn settlement_close(cache: &Cache, instrument_id: &InstrumentId) -> Option<InstrumentClose> {
92    if !matches!(
93        cache.instrument(instrument_id),
94        Some(InstrumentAny::BinaryOption(_))
95    ) {
96        return None;
97    }
98
99    cache
100        .instrument_close(instrument_id)
101        .copied()
102        .filter(|close| close.close_type == InstrumentCloseType::ContractExpired)
103}