nautilus_plugin/surfaces/commands/submit.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//! Submit commands and their boundary-owned handles.
17//!
18//! The plug-in constructs a [`SubmitOrderCommand`] or
19//! [`SubmitOrderListCommand`], wraps it in the matching `*Handle`, and
20//! hands the host a pointer via
21//! [`HostVTable::submit_order`](crate::host::HostVTable::submit_order) or
22//! [`HostVTable::submit_order_list`](crate::host::HostVTable::submit_order_list).
23//! The host derefs the handle once and routes the borrowed command into
24//! the calling strategy's submit path. The plug-in owns the box and frees
25//! it when the call returns.
26
27#![allow(unsafe_code)]
28
29use std::ops::Deref;
30
31use nautilus_core::Params;
32use nautilus_model::{
33 identifiers::{ClientId, PositionId},
34 orders::OrderAny,
35};
36
37/// Submit-order command. Mirrors the arguments to `Strategy::submit_order`.
38#[repr(C)]
39#[derive(Debug, Clone)]
40pub struct SubmitOrderCommand {
41 /// The order to submit.
42 pub order: OrderAny,
43
44 /// Optional position the order is associated with.
45 pub position_id: Option<PositionId>,
46
47 /// Optional client routing identifier.
48 pub client_id: Option<ClientId>,
49
50 /// Optional venue-specific parameters.
51 pub params: Option<Params>,
52}
53
54impl SubmitOrderCommand {
55 /// Creates a new [`SubmitOrderCommand`] instance.
56 #[must_use]
57 pub const fn new(
58 order: OrderAny,
59 position_id: Option<PositionId>,
60 client_id: Option<ClientId>,
61 params: Option<Params>,
62 ) -> Self {
63 Self {
64 order,
65 position_id,
66 client_id,
67 params,
68 }
69 }
70}
71
72/// Boundary-owned wrapper that lets [`SubmitOrderCommand`] cross the cdylib
73/// FFI boundary by reference.
74///
75/// `SubmitOrderCommand` carries an `OrderAny` whose variant payloads are
76/// heap-owned (e.g. tag vectors, exec-algorithm params), so the plug-in
77/// wraps the whole command in this `#[repr(C)]` handle and passes a
78/// borrowed pointer to the host. Equivalent layout on both sides relies
79/// on operator-side pinning (plug-in cdylibs rebuilt to match each
80/// Nautilus version); `PluginBuildId` is recorded for load diagnostics,
81/// and precision mode is enforced during manifest validation.
82#[repr(C)]
83#[derive(Debug, Clone)]
84pub struct SubmitOrderHandle(Box<SubmitOrderCommand>);
85
86impl SubmitOrderHandle {
87 /// Wraps `command` in a boundary-owned handle.
88 #[must_use]
89 pub fn new(command: SubmitOrderCommand) -> Self {
90 Self(Box::new(command))
91 }
92
93 /// Returns a reference to the wrapped command.
94 #[must_use]
95 pub fn command(&self) -> &SubmitOrderCommand {
96 &self.0
97 }
98
99 /// Consumes the wrapper and returns the inner command.
100 #[must_use]
101 pub fn into_inner(self) -> SubmitOrderCommand {
102 *self.0
103 }
104}
105
106impl Deref for SubmitOrderHandle {
107 type Target = SubmitOrderCommand;
108
109 fn deref(&self) -> &Self::Target {
110 &self.0
111 }
112}
113
114/// Submit-order-list command. Mirrors the arguments to `Strategy::submit_order_list`.
115#[repr(C)]
116#[derive(Debug, Clone)]
117pub struct SubmitOrderListCommand {
118 /// The orders to submit as a batched list.
119 pub orders: Vec<OrderAny>,
120
121 /// Optional position the orders are associated with.
122 pub position_id: Option<PositionId>,
123
124 /// Optional client routing identifier.
125 pub client_id: Option<ClientId>,
126
127 /// Optional venue-specific parameters.
128 pub params: Option<Params>,
129}
130
131impl SubmitOrderListCommand {
132 /// Creates a new [`SubmitOrderListCommand`] instance.
133 #[must_use]
134 pub const fn new(
135 orders: Vec<OrderAny>,
136 position_id: Option<PositionId>,
137 client_id: Option<ClientId>,
138 params: Option<Params>,
139 ) -> Self {
140 Self {
141 orders,
142 position_id,
143 client_id,
144 params,
145 }
146 }
147}
148
149/// Boundary-owned wrapper that lets [`SubmitOrderListCommand`] cross the
150/// cdylib FFI boundary by reference.
151///
152/// The `Vec<OrderAny>` payload is the largest of any execution command;
153/// the outer `Box` pins the Vec header and the heap allocation stays on
154/// the plug-in side for the duration of the call.
155#[repr(C)]
156#[derive(Debug, Clone)]
157pub struct SubmitOrderListHandle(Box<SubmitOrderListCommand>);
158
159impl SubmitOrderListHandle {
160 /// Wraps `command` in a boundary-owned handle.
161 #[must_use]
162 pub fn new(command: SubmitOrderListCommand) -> Self {
163 Self(Box::new(command))
164 }
165
166 /// Returns a reference to the wrapped command.
167 #[must_use]
168 pub fn command(&self) -> &SubmitOrderListCommand {
169 &self.0
170 }
171
172 /// Consumes the wrapper and returns the inner command.
173 #[must_use]
174 pub fn into_inner(self) -> SubmitOrderListCommand {
175 *self.0
176 }
177}
178
179impl Deref for SubmitOrderListHandle {
180 type Target = SubmitOrderListCommand;
181
182 fn deref(&self) -> &Self::Target {
183 &self.0
184 }
185}
186
187#[cfg(test)]
188mod tests {
189 use nautilus_core::{UUID4, UnixNanos};
190 use nautilus_model::{
191 enums::{OrderSide, TimeInForce},
192 identifiers::{ClientOrderId, InstrumentId, StrategyId, TraderId},
193 orders::{MarketOrder, Order},
194 types::Quantity,
195 };
196 use rstest::rstest;
197
198 use super::*;
199
200 fn market_order(client_order_id: &str) -> OrderAny {
201 OrderAny::Market(MarketOrder::new(
202 TraderId::from("TRADER-001"),
203 StrategyId::from("S-001"),
204 InstrumentId::from("ETH-USDT.BINANCE"),
205 ClientOrderId::from(client_order_id),
206 OrderSide::Buy,
207 Quantity::from("1.0"),
208 TimeInForce::Gtc,
209 UUID4::new(),
210 UnixNanos::default(),
211 false,
212 false,
213 None,
214 None,
215 None,
216 None,
217 None,
218 None,
219 None,
220 None,
221 ))
222 }
223
224 // SubmitOrderCommand carries an `OrderAny` which is not `Eq`, so identity is
225 // asserted via the underlying client_order_id rather than via `==` on the
226 // full struct.
227 #[rstest]
228 fn submit_order_handle_round_trips_command() {
229 let order = market_order("O-1");
230 let order_id = order.client_order_id();
231 let handle = SubmitOrderHandle::new(SubmitOrderCommand::new(order, None, None, None));
232 assert_eq!(handle.command().order.client_order_id(), order_id);
233 // Exercise the Deref impl explicitly so a regression in the Deref
234 // target binding (e.g. returning a default) fails the test.
235 assert_eq!(Deref::deref(&handle).order.client_order_id(), order_id,);
236 let recovered = handle.into_inner();
237 assert_eq!(recovered.order.client_order_id(), order_id);
238 }
239
240 #[rstest]
241 fn submit_order_list_handle_round_trips_command() {
242 let handle = SubmitOrderListHandle::new(SubmitOrderListCommand::new(
243 vec![market_order("O-1"), market_order("O-2")],
244 None,
245 None,
246 None,
247 ));
248 assert_eq!(handle.command().orders.len(), 2);
249 // Exercise the Deref impl explicitly.
250 assert_eq!(Deref::deref(&handle).orders.len(), 2);
251 let recovered = handle.into_inner();
252 assert_eq!(recovered.orders.len(), 2);
253 assert_eq!(
254 recovered.orders[0].client_order_id(),
255 ClientOrderId::from("O-1")
256 );
257 }
258}