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}