Skip to main content

nautilus_plugin/surfaces/commands/
close.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//! Close-position and close-all-positions commands and their boundary-owned
17//! handles.
18//!
19//! The plug-in constructs a [`ClosePositionCommand`] or
20//! [`CloseAllPositionsCommand`], wraps it in the matching `*Handle`, and
21//! hands the host a pointer via
22//! [`HostVTable::close_position`](crate::host::HostVTable::close_position) or
23//! [`HostVTable::close_all_positions`](crate::host::HostVTable::close_all_positions).
24//! The host derefs the handle once and routes the borrowed command into
25//! the calling strategy's close path. The plug-in owns the box and frees
26//! it when the call returns.
27
28#![allow(unsafe_code)]
29
30use std::ops::Deref;
31
32use nautilus_model::{
33    enums::{PositionSide, TimeInForce},
34    identifiers::{ClientId, InstrumentId, PositionId},
35};
36use ustr::Ustr;
37
38/// Close-position command. Mirrors the arguments to `Strategy::close_position`.
39///
40/// The host resolves `position_id` against the live cache to materialise the
41/// `&Position` reference the trait method requires.
42#[repr(C)]
43#[derive(Debug, Clone, PartialEq, Eq)]
44pub struct ClosePositionCommand {
45    /// The identifier of the position to close.
46    pub position_id: PositionId,
47
48    /// Optional client routing identifier.
49    pub client_id: Option<ClientId>,
50
51    /// Optional tags to attach to the closing order.
52    pub tags: Option<Vec<Ustr>>,
53
54    /// Optional time-in-force override.
55    pub time_in_force: Option<TimeInForce>,
56
57    /// Optional reduce-only flag override.
58    pub reduce_only: Option<bool>,
59
60    /// Optional quote-quantity flag override.
61    pub quote_quantity: Option<bool>,
62}
63
64impl ClosePositionCommand {
65    /// Creates a new [`ClosePositionCommand`] instance.
66    #[must_use]
67    pub const fn new(
68        position_id: PositionId,
69        client_id: Option<ClientId>,
70        tags: Option<Vec<Ustr>>,
71        time_in_force: Option<TimeInForce>,
72        reduce_only: Option<bool>,
73        quote_quantity: Option<bool>,
74    ) -> Self {
75        Self {
76            position_id,
77            client_id,
78            tags,
79            time_in_force,
80            reduce_only,
81            quote_quantity,
82        }
83    }
84}
85
86/// Boundary-owned wrapper that lets [`ClosePositionCommand`] cross the cdylib
87/// FFI boundary by reference.
88#[repr(C)]
89#[derive(Debug, Clone)]
90pub struct ClosePositionHandle(Box<ClosePositionCommand>);
91
92impl ClosePositionHandle {
93    /// Wraps `command` in a boundary-owned handle.
94    #[must_use]
95    pub fn new(command: ClosePositionCommand) -> Self {
96        Self(Box::new(command))
97    }
98
99    /// Returns a reference to the wrapped command.
100    #[must_use]
101    pub fn command(&self) -> &ClosePositionCommand {
102        &self.0
103    }
104
105    /// Consumes the wrapper and returns the inner command.
106    #[must_use]
107    pub fn into_inner(self) -> ClosePositionCommand {
108        *self.0
109    }
110}
111
112impl Deref for ClosePositionHandle {
113    type Target = ClosePositionCommand;
114
115    fn deref(&self) -> &Self::Target {
116        &self.0
117    }
118}
119
120/// Close-all-positions command. Mirrors the arguments to `Strategy::close_all_positions`.
121#[repr(C)]
122#[derive(Debug, Clone, PartialEq, Eq)]
123pub struct CloseAllPositionsCommand {
124    /// The instrument identifier filtering which positions to close.
125    pub instrument_id: InstrumentId,
126
127    /// Optional position side filter.
128    pub position_side: Option<PositionSide>,
129
130    /// Optional client routing identifier.
131    pub client_id: Option<ClientId>,
132
133    /// Optional tags to attach to the closing orders.
134    pub tags: Option<Vec<Ustr>>,
135
136    /// Optional time-in-force override.
137    pub time_in_force: Option<TimeInForce>,
138
139    /// Optional reduce-only flag override.
140    pub reduce_only: Option<bool>,
141
142    /// Optional quote-quantity flag override.
143    pub quote_quantity: Option<bool>,
144}
145
146impl CloseAllPositionsCommand {
147    /// Creates a new [`CloseAllPositionsCommand`] instance.
148    #[allow(clippy::too_many_arguments)]
149    #[must_use]
150    pub const fn new(
151        instrument_id: InstrumentId,
152        position_side: Option<PositionSide>,
153        client_id: Option<ClientId>,
154        tags: Option<Vec<Ustr>>,
155        time_in_force: Option<TimeInForce>,
156        reduce_only: Option<bool>,
157        quote_quantity: Option<bool>,
158    ) -> Self {
159        Self {
160            instrument_id,
161            position_side,
162            client_id,
163            tags,
164            time_in_force,
165            reduce_only,
166            quote_quantity,
167        }
168    }
169}
170
171/// Boundary-owned wrapper that lets [`CloseAllPositionsCommand`] cross the
172/// cdylib FFI boundary by reference.
173#[repr(C)]
174#[derive(Debug, Clone)]
175pub struct CloseAllPositionsHandle(Box<CloseAllPositionsCommand>);
176
177impl CloseAllPositionsHandle {
178    /// Wraps `command` in a boundary-owned handle.
179    #[must_use]
180    pub fn new(command: CloseAllPositionsCommand) -> Self {
181        Self(Box::new(command))
182    }
183
184    /// Returns a reference to the wrapped command.
185    #[must_use]
186    pub fn command(&self) -> &CloseAllPositionsCommand {
187        &self.0
188    }
189
190    /// Consumes the wrapper and returns the inner command.
191    #[must_use]
192    pub fn into_inner(self) -> CloseAllPositionsCommand {
193        *self.0
194    }
195}
196
197impl Deref for CloseAllPositionsHandle {
198    type Target = CloseAllPositionsCommand;
199
200    fn deref(&self) -> &Self::Target {
201        &self.0
202    }
203}
204
205#[cfg(test)]
206mod tests {
207    use rstest::rstest;
208
209    use super::*;
210
211    #[rstest]
212    fn close_position_handle_round_trips_command() {
213        let cmd = ClosePositionCommand::new(
214            PositionId::from("P-001"),
215            None,
216            Some(vec![Ustr::from("exit")]),
217            Some(TimeInForce::Ioc),
218            None,
219            None,
220        );
221        let handle = ClosePositionHandle::new(cmd.clone());
222        assert_eq!(handle.command(), &cmd);
223        assert_eq!(&*handle, &cmd);
224        assert_eq!(handle.into_inner(), cmd);
225    }
226
227    #[rstest]
228    fn close_all_positions_handle_round_trips_command() {
229        let cmd = CloseAllPositionsCommand::new(
230            InstrumentId::from("ETH-USDT.BINANCE"),
231            Some(PositionSide::Long),
232            None,
233            None,
234            None,
235            None,
236            None,
237        );
238        let handle = CloseAllPositionsHandle::new(cmd.clone());
239        assert_eq!(handle.command(), &cmd);
240        assert_eq!(&*handle, &cmd);
241        assert_eq!(handle.into_inner(), cmd);
242    }
243}