1#![allow(unsafe_code)]
25#![allow(
26 clippy::multiple_unsafe_ops_per_block,
27 reason = "vtable deref and FFI call form a single boundary callback; \
28 SAFETY comments cover both ops together"
29)]
30
31use std::{num::NonZeroUsize, str::FromStr, sync::OnceLock};
32
33use nautilus_common::{
34 actor::{DataActor, registry::try_get_actor_unchecked},
35 cache::Cache,
36 msgbus,
37};
38use nautilus_core::{Params, UnixNanos, time::duration_since_unix_epoch};
39use nautilus_model::{
40 data::BarType,
41 enums::{BookType, FromU8},
42 identifiers::{AccountId, ClientId, ClientOrderId, InstrumentId, PositionId, StrategyId},
43};
44use nautilus_trading::strategy::Strategy;
45use serde::Serialize;
46
47use crate::{
48 NAUTILUS_PLUGIN_ABI_VERSION,
49 boundary::{BorrowedStr, OwnedBytes, PluginError, PluginErrorCode, PluginResult, Slice},
50 bridge::{
51 actor::PluginActorAdapter,
52 registry::{HostContextInner, controller_host_context_inner, host_context_inner},
53 strategy::PluginStrategyAdapter,
54 },
55 host::{ControllerHostContext, ControllerHostVTable, HostContext, HostLogLevel, HostVTable},
56 loader::PluginLoader,
57 normalize::BoundaryCommandHandle,
58 surfaces::commands::{
59 CancelAllOrdersHandle, CancelOrderHandle, CancelOrdersHandle, CloseAllPositionsHandle,
60 ClosePositionHandle, ModifyOrderHandle, QueryAccountHandle, QueryOrderHandle,
61 SubmitOrderHandle, SubmitOrderListHandle,
62 },
63};
64
65#[must_use]
71pub fn host_vtable() -> *const HostVTable {
72 static HOST: OnceLock<HostVTable> = OnceLock::new();
73 std::ptr::from_ref(HOST.get_or_init(|| HostVTable {
74 abi_version: NAUTILUS_PLUGIN_ABI_VERSION,
75 clock_now_ns: host_clock_now_ns,
76 log: host_log,
77 cache_instrument: host_cache_instrument,
78 cache_account: host_cache_account,
79 cache_order: host_cache_order,
80 cache_position: host_cache_position,
81 cache_orders_for_strategy: host_cache_orders_for_strategy,
82 cache_positions_for_strategy: host_cache_positions_for_strategy,
83 subscribe_quotes: host_subscribe_quotes,
84 unsubscribe_quotes: host_unsubscribe_quotes,
85 subscribe_trades: host_subscribe_trades,
86 unsubscribe_trades: host_unsubscribe_trades,
87 subscribe_bars: host_subscribe_bars,
88 unsubscribe_bars: host_unsubscribe_bars,
89 subscribe_book_deltas: host_subscribe_book_deltas,
90 unsubscribe_book_deltas: host_unsubscribe_book_deltas,
91 subscribe_book_at_interval: host_subscribe_book_at_interval,
92 unsubscribe_book_at_interval: host_unsubscribe_book_at_interval,
93 msgbus_publish: host_msgbus_publish,
94 set_time_alert: host_set_time_alert,
95 set_timer: host_set_timer,
96 cancel_timer: host_cancel_timer,
97 submit_order: host_submit_order,
98 cancel_order: host_cancel_order,
99 modify_order: host_modify_order,
100 submit_order_list: host_submit_order_list,
101 cancel_orders: host_cancel_orders,
102 cancel_all_orders: host_cancel_all_orders,
103 close_position: host_close_position,
104 close_all_positions: host_close_all_positions,
105 query_account: host_query_account,
106 query_order: host_query_order,
107 }))
108}
109
110#[must_use]
112pub fn controller_host_vtable() -> *const ControllerHostVTable {
113 static HOST: OnceLock<ControllerHostVTable> = OnceLock::new();
114 std::ptr::from_ref(HOST.get_or_init(|| ControllerHostVTable {
115 abi_version: NAUTILUS_PLUGIN_ABI_VERSION,
116 create_plugin_strategy: controller_host_not_implemented,
117 start_strategy: controller_host_not_implemented,
118 stop_strategy: controller_host_not_implemented,
119 exit_market: controller_host_not_implemented,
120 remove_strategy: controller_host_not_implemented,
121 instrument_exists: controller_host_not_implemented,
122 log: controller_host_log,
123 clock_now_ns: controller_host_clock_now_ns,
124 }))
125}
126
127#[must_use]
134pub fn plugin_loader() -> PluginLoader {
135 PluginLoader::with_host(host_vtable())
136}
137
138unsafe extern "C" fn host_clock_now_ns() -> u64 {
139 u64::try_from(duration_since_unix_epoch().as_nanos()).unwrap_or(u64::MAX)
140}
141
142unsafe extern "C" fn controller_host_not_implemented(
143 ctx: *const ControllerHostContext,
144 _request_json: BorrowedStr<'_>,
145) -> PluginResult<OwnedBytes> {
146 let context = controller_context_label(ctx);
147 PluginResult::Err(PluginError::new(
148 PluginErrorCode::NotImplemented,
149 format!("{context} controller host service is not implemented"),
150 ))
151}
152
153unsafe extern "C" fn controller_host_log(
154 ctx: *const ControllerHostContext,
155 request_json: BorrowedStr<'_>,
156) -> PluginResult<OwnedBytes> {
157 let context = controller_context_label(ctx);
158 let request = unsafe { request_json.as_str() };
160 log::info!(target: "nautilus_plugin", "[{context}] {request}");
161 PluginResult::Ok(OwnedBytes::empty())
162}
163
164unsafe extern "C" fn controller_host_clock_now_ns(
165 _ctx: *const ControllerHostContext,
166 _request_json: BorrowedStr<'_>,
167) -> PluginResult<OwnedBytes> {
168 json_bytes(&serde_json::json!({
169 "unix_nanos": u64::try_from(duration_since_unix_epoch().as_nanos()).unwrap_or(u64::MAX),
170 }))
171}
172
173fn controller_context_label(ctx: *const ControllerHostContext) -> String {
174 let Some(inner) = (unsafe { controller_host_context_inner(ctx) }) else {
176 return "unknown-controller".to_string();
177 };
178 format!("{}:{}", inner.plugin_name, inner.type_name)
179}
180
181unsafe extern "C" fn host_log(
182 level: HostLogLevel,
183 target: BorrowedStr<'_>,
184 message: BorrowedStr<'_>,
185) {
186 let target = unsafe { target.as_str() };
188 let message = unsafe { message.as_str() };
190 match level {
191 HostLogLevel::Error => log::error!(target: "nautilus_plugin", "[{target}] {message}"),
192 HostLogLevel::Warn => log::warn!(target: "nautilus_plugin", "[{target}] {message}"),
193 HostLogLevel::Info => log::info!(target: "nautilus_plugin", "[{target}] {message}"),
194 HostLogLevel::Debug => log::debug!(target: "nautilus_plugin", "[{target}] {message}"),
195 HostLogLevel::Trace => log::trace!(target: "nautilus_plugin", "[{target}] {message}"),
196 }
197}
198
199unsafe extern "C" fn host_cache_instrument(
200 ctx: *const HostContext,
201 instrument_id: BorrowedStr<'_>,
202) -> PluginResult<OwnedBytes> {
203 let instrument_id = match parse_instrument_id(instrument_id, "instrument_id") {
204 Ok(id) => id,
205 Err(e) => return PluginResult::Err(e),
206 };
207
208 dispatch_cache_query(ctx, "cache_instrument", |cache, _| {
209 json_optional(cache.instrument(&instrument_id))
210 })
211}
212
213unsafe extern "C" fn host_cache_account(
214 ctx: *const HostContext,
215 account_id: BorrowedStr<'_>,
216) -> PluginResult<OwnedBytes> {
217 let account_id = match parse_account_id(account_id, "account_id") {
218 Ok(id) => id,
219 Err(e) => return PluginResult::Err(e),
220 };
221
222 dispatch_cache_query(ctx, "cache_account", |cache, _| {
223 let account = cache.account(&account_id).map(|account| account.cloned());
224 json_optional(account.as_ref())
225 })
226}
227
228unsafe extern "C" fn host_cache_order(
229 ctx: *const HostContext,
230 client_order_id: BorrowedStr<'_>,
231) -> PluginResult<OwnedBytes> {
232 let client_order_id = match parse_client_order_id(client_order_id, "client_order_id") {
233 Ok(id) => id,
234 Err(e) => return PluginResult::Err(e),
235 };
236
237 dispatch_cache_query(ctx, "cache_order", |cache, _| {
238 let order = cache.order(&client_order_id).map(|order| order.cloned());
239 json_optional(order.as_ref())
240 })
241}
242
243unsafe extern "C" fn host_cache_position(
244 ctx: *const HostContext,
245 position_id: BorrowedStr<'_>,
246) -> PluginResult<OwnedBytes> {
247 let position_id = match parse_position_id(position_id, "position_id") {
248 Ok(id) => id,
249 Err(e) => return PluginResult::Err(e),
250 };
251
252 dispatch_cache_query(ctx, "cache_position", |cache, _| {
253 let position = cache
254 .position(&position_id)
255 .map(|position| position.cloned());
256 json_optional(position.as_ref())
257 })
258}
259
260unsafe extern "C" fn host_cache_orders_for_strategy(
261 ctx: *const HostContext,
262 strategy_id: BorrowedStr<'_>,
263) -> PluginResult<OwnedBytes> {
264 dispatch_cache_query(ctx, "cache_orders_for_strategy", |cache, inner| {
265 let strategy_id = match parse_strategy_id_for_context(strategy_id, inner) {
266 Ok(id) => id,
267 Err(e) => return PluginResult::Err(e),
268 };
269 let orders = cache
270 .orders(None, None, Some(&strategy_id), None, None)
271 .into_iter()
272 .map(|order| order.cloned())
273 .collect::<Vec<_>>();
274 json_bytes(&orders)
275 })
276}
277
278unsafe extern "C" fn host_cache_positions_for_strategy(
279 ctx: *const HostContext,
280 strategy_id: BorrowedStr<'_>,
281) -> PluginResult<OwnedBytes> {
282 dispatch_cache_query(ctx, "cache_positions_for_strategy", |cache, inner| {
283 let strategy_id = match parse_strategy_id_for_context(strategy_id, inner) {
284 Ok(id) => id,
285 Err(e) => return PluginResult::Err(e),
286 };
287 let positions = cache
288 .positions(None, None, Some(&strategy_id), None, None)
289 .into_iter()
290 .map(|position| position.cloned())
291 .collect::<Vec<_>>();
292 json_bytes(&positions)
293 })
294}
295
296unsafe extern "C" fn host_subscribe_quotes(
297 ctx: *const HostContext,
298 instrument_id: BorrowedStr<'_>,
299 client_id: BorrowedStr<'_>,
300 params_json: BorrowedStr<'_>,
301) -> PluginResult<()> {
302 let args = match parse_instrument_subscription(instrument_id, client_id, params_json) {
303 Ok(args) => args,
304 Err(e) => return PluginResult::Err(e),
305 };
306 let actor_args = args.clone();
307 let strategy_args = args;
308
309 dispatch_actor_action(
310 ctx,
311 "subscribe_quotes",
312 |actor| {
313 DataActor::subscribe_quotes(
314 actor,
315 actor_args.instrument_id,
316 actor_args.client_id,
317 actor_args.params,
318 );
319 Ok(())
320 },
321 |strategy| {
322 DataActor::subscribe_quotes(
323 strategy,
324 strategy_args.instrument_id,
325 strategy_args.client_id,
326 strategy_args.params,
327 );
328 Ok(())
329 },
330 )
331}
332
333unsafe extern "C" fn host_unsubscribe_quotes(
334 ctx: *const HostContext,
335 instrument_id: BorrowedStr<'_>,
336 client_id: BorrowedStr<'_>,
337 params_json: BorrowedStr<'_>,
338) -> PluginResult<()> {
339 let args = match parse_instrument_subscription(instrument_id, client_id, params_json) {
340 Ok(args) => args,
341 Err(e) => return PluginResult::Err(e),
342 };
343 let actor_args = args.clone();
344 let strategy_args = args;
345
346 dispatch_actor_action(
347 ctx,
348 "unsubscribe_quotes",
349 |actor| {
350 DataActor::unsubscribe_quotes(
351 actor,
352 actor_args.instrument_id,
353 actor_args.client_id,
354 actor_args.params,
355 );
356 Ok(())
357 },
358 |strategy| {
359 DataActor::unsubscribe_quotes(
360 strategy,
361 strategy_args.instrument_id,
362 strategy_args.client_id,
363 strategy_args.params,
364 );
365 Ok(())
366 },
367 )
368}
369
370unsafe extern "C" fn host_subscribe_trades(
371 ctx: *const HostContext,
372 instrument_id: BorrowedStr<'_>,
373 client_id: BorrowedStr<'_>,
374 params_json: BorrowedStr<'_>,
375) -> PluginResult<()> {
376 let args = match parse_instrument_subscription(instrument_id, client_id, params_json) {
377 Ok(args) => args,
378 Err(e) => return PluginResult::Err(e),
379 };
380 let actor_args = args.clone();
381 let strategy_args = args;
382
383 dispatch_actor_action(
384 ctx,
385 "subscribe_trades",
386 |actor| {
387 DataActor::subscribe_trades(
388 actor,
389 actor_args.instrument_id,
390 actor_args.client_id,
391 actor_args.params,
392 );
393 Ok(())
394 },
395 |strategy| {
396 DataActor::subscribe_trades(
397 strategy,
398 strategy_args.instrument_id,
399 strategy_args.client_id,
400 strategy_args.params,
401 );
402 Ok(())
403 },
404 )
405}
406
407unsafe extern "C" fn host_unsubscribe_trades(
408 ctx: *const HostContext,
409 instrument_id: BorrowedStr<'_>,
410 client_id: BorrowedStr<'_>,
411 params_json: BorrowedStr<'_>,
412) -> PluginResult<()> {
413 let args = match parse_instrument_subscription(instrument_id, client_id, params_json) {
414 Ok(args) => args,
415 Err(e) => return PluginResult::Err(e),
416 };
417 let actor_args = args.clone();
418 let strategy_args = args;
419
420 dispatch_actor_action(
421 ctx,
422 "unsubscribe_trades",
423 |actor| {
424 DataActor::unsubscribe_trades(
425 actor,
426 actor_args.instrument_id,
427 actor_args.client_id,
428 actor_args.params,
429 );
430 Ok(())
431 },
432 |strategy| {
433 DataActor::unsubscribe_trades(
434 strategy,
435 strategy_args.instrument_id,
436 strategy_args.client_id,
437 strategy_args.params,
438 );
439 Ok(())
440 },
441 )
442}
443
444unsafe extern "C" fn host_subscribe_bars(
445 ctx: *const HostContext,
446 bar_type: BorrowedStr<'_>,
447 client_id: BorrowedStr<'_>,
448 params_json: BorrowedStr<'_>,
449) -> PluginResult<()> {
450 let args = match parse_bar_subscription(bar_type, client_id, params_json) {
451 Ok(args) => args,
452 Err(e) => return PluginResult::Err(e),
453 };
454 let actor_args = args.clone();
455 let strategy_args = args;
456
457 dispatch_actor_action(
458 ctx,
459 "subscribe_bars",
460 |actor| {
461 DataActor::subscribe_bars(
462 actor,
463 actor_args.bar_type,
464 actor_args.client_id,
465 actor_args.params,
466 );
467 Ok(())
468 },
469 |strategy| {
470 DataActor::subscribe_bars(
471 strategy,
472 strategy_args.bar_type,
473 strategy_args.client_id,
474 strategy_args.params,
475 );
476 Ok(())
477 },
478 )
479}
480
481unsafe extern "C" fn host_unsubscribe_bars(
482 ctx: *const HostContext,
483 bar_type: BorrowedStr<'_>,
484 client_id: BorrowedStr<'_>,
485 params_json: BorrowedStr<'_>,
486) -> PluginResult<()> {
487 let args = match parse_bar_subscription(bar_type, client_id, params_json) {
488 Ok(args) => args,
489 Err(e) => return PluginResult::Err(e),
490 };
491 let actor_args = args.clone();
492 let strategy_args = args;
493
494 dispatch_actor_action(
495 ctx,
496 "unsubscribe_bars",
497 |actor| {
498 DataActor::unsubscribe_bars(
499 actor,
500 actor_args.bar_type,
501 actor_args.client_id,
502 actor_args.params,
503 );
504 Ok(())
505 },
506 |strategy| {
507 DataActor::unsubscribe_bars(
508 strategy,
509 strategy_args.bar_type,
510 strategy_args.client_id,
511 strategy_args.params,
512 );
513 Ok(())
514 },
515 )
516}
517
518unsafe extern "C" fn host_subscribe_book_deltas(
519 ctx: *const HostContext,
520 instrument_id: BorrowedStr<'_>,
521 book_type: u8,
522 depth: usize,
523 client_id: BorrowedStr<'_>,
524 managed: u8,
525 params_json: BorrowedStr<'_>,
526) -> PluginResult<()> {
527 let args =
528 match parse_book_subscription(instrument_id, book_type, depth, client_id, params_json) {
529 Ok(args) => args,
530 Err(e) => return PluginResult::Err(e),
531 };
532 let actor_args = args.clone();
533 let strategy_args = args;
534 let managed = managed != 0;
535
536 dispatch_actor_action(
537 ctx,
538 "subscribe_book_deltas",
539 |actor| {
540 DataActor::subscribe_book_deltas(
541 actor,
542 actor_args.instrument_id,
543 actor_args.book_type,
544 actor_args.depth,
545 actor_args.client_id,
546 managed,
547 actor_args.params,
548 );
549 Ok(())
550 },
551 |strategy| {
552 DataActor::subscribe_book_deltas(
553 strategy,
554 strategy_args.instrument_id,
555 strategy_args.book_type,
556 strategy_args.depth,
557 strategy_args.client_id,
558 managed,
559 strategy_args.params,
560 );
561 Ok(())
562 },
563 )
564}
565
566unsafe extern "C" fn host_unsubscribe_book_deltas(
567 ctx: *const HostContext,
568 instrument_id: BorrowedStr<'_>,
569 client_id: BorrowedStr<'_>,
570 params_json: BorrowedStr<'_>,
571) -> PluginResult<()> {
572 let args = match parse_instrument_subscription(instrument_id, client_id, params_json) {
573 Ok(args) => args,
574 Err(e) => return PluginResult::Err(e),
575 };
576 let actor_args = args.clone();
577 let strategy_args = args;
578
579 dispatch_actor_action(
580 ctx,
581 "unsubscribe_book_deltas",
582 |actor| {
583 DataActor::unsubscribe_book_deltas(
584 actor,
585 actor_args.instrument_id,
586 actor_args.client_id,
587 actor_args.params,
588 );
589 Ok(())
590 },
591 |strategy| {
592 DataActor::unsubscribe_book_deltas(
593 strategy,
594 strategy_args.instrument_id,
595 strategy_args.client_id,
596 strategy_args.params,
597 );
598 Ok(())
599 },
600 )
601}
602
603unsafe extern "C" fn host_subscribe_book_at_interval(
604 ctx: *const HostContext,
605 instrument_id: BorrowedStr<'_>,
606 book_type: u8,
607 depth: usize,
608 interval_ms: usize,
609 client_id: BorrowedStr<'_>,
610 params_json: BorrowedStr<'_>,
611) -> PluginResult<()> {
612 let args =
613 match parse_book_subscription(instrument_id, book_type, depth, client_id, params_json) {
614 Ok(args) => args,
615 Err(e) => return PluginResult::Err(e),
616 };
617 let actor_args = args.clone();
618 let strategy_args = args;
619 let interval_ms = match NonZeroUsize::new(interval_ms) {
620 Some(value) => value,
621 None => {
622 return PluginResult::Err(PluginError::new(
623 PluginErrorCode::InvalidArgument,
624 "interval_ms must be greater than zero",
625 ));
626 }
627 };
628
629 dispatch_actor_action(
630 ctx,
631 "subscribe_book_at_interval",
632 |actor| {
633 DataActor::subscribe_book_at_interval(
634 actor,
635 actor_args.instrument_id,
636 actor_args.book_type,
637 actor_args.depth,
638 interval_ms,
639 actor_args.client_id,
640 actor_args.params,
641 );
642 Ok(())
643 },
644 |strategy| {
645 DataActor::subscribe_book_at_interval(
646 strategy,
647 strategy_args.instrument_id,
648 strategy_args.book_type,
649 strategy_args.depth,
650 interval_ms,
651 strategy_args.client_id,
652 strategy_args.params,
653 );
654 Ok(())
655 },
656 )
657}
658
659unsafe extern "C" fn host_unsubscribe_book_at_interval(
660 ctx: *const HostContext,
661 instrument_id: BorrowedStr<'_>,
662 interval_ms: usize,
663 client_id: BorrowedStr<'_>,
664 params_json: BorrowedStr<'_>,
665) -> PluginResult<()> {
666 let args = match parse_instrument_subscription(instrument_id, client_id, params_json) {
667 Ok(args) => args,
668 Err(e) => return PluginResult::Err(e),
669 };
670 let actor_args = args.clone();
671 let strategy_args = args;
672 let interval_ms = match NonZeroUsize::new(interval_ms) {
673 Some(value) => value,
674 None => {
675 return PluginResult::Err(PluginError::new(
676 PluginErrorCode::InvalidArgument,
677 "interval_ms must be greater than zero",
678 ));
679 }
680 };
681
682 dispatch_actor_action(
683 ctx,
684 "unsubscribe_book_at_interval",
685 |actor| {
686 DataActor::unsubscribe_book_at_interval(
687 actor,
688 actor_args.instrument_id,
689 interval_ms,
690 actor_args.client_id,
691 actor_args.params,
692 );
693 Ok(())
694 },
695 |strategy| {
696 DataActor::unsubscribe_book_at_interval(
697 strategy,
698 strategy_args.instrument_id,
699 interval_ms,
700 strategy_args.client_id,
701 strategy_args.params,
702 );
703 Ok(())
704 },
705 )
706}
707
708unsafe extern "C" fn host_msgbus_publish(
709 ctx: *const HostContext,
710 topic: BorrowedStr<'_>,
711 payload: Slice<'_, u8>,
712) -> PluginResult<()> {
713 let inner = match resolve_context(ctx, "msgbus_publish") {
714 Ok(inner) => inner,
715 Err(e) => return PluginResult::Err(e),
716 };
717
718 if let Err(e) = ensure_adapter_registered("msgbus_publish", inner) {
719 return PluginResult::Err(e);
720 }
721
722 let topic = unsafe { topic.as_str() };
724 let payload = unsafe { payload.as_slice() }.to_vec();
726 msgbus::publish_any(topic.into(), &payload);
727 PluginResult::Ok(())
728}
729
730unsafe extern "C" fn host_set_time_alert(
731 ctx: *const HostContext,
732 name: BorrowedStr<'_>,
733 alert_time_ns: u64,
734 allow_past: u8,
735) -> PluginResult<()> {
736 let name = unsafe { name.as_str() }.to_string();
738 dispatch_actor_action(
739 ctx,
740 "set_time_alert",
741 |actor| {
742 actor.clock().set_time_alert_ns(
743 &name,
744 UnixNanos::from(alert_time_ns),
745 None,
746 Some(allow_past != 0),
747 )
748 },
749 |strategy| {
750 strategy.clock().set_time_alert_ns(
751 &name,
752 UnixNanos::from(alert_time_ns),
753 None,
754 Some(allow_past != 0),
755 )
756 },
757 )
758}
759
760unsafe extern "C" fn host_set_timer(
761 ctx: *const HostContext,
762 name: BorrowedStr<'_>,
763 interval_ns: u64,
764 start_time_ns: u64,
765 stop_time_ns: u64,
766 allow_past: u8,
767 fire_immediately: u8,
768) -> PluginResult<()> {
769 let name = unsafe { name.as_str() }.to_string();
771 let start_time_ns = nonzero_unix_nanos(start_time_ns);
772 let stop_time_ns = nonzero_unix_nanos(stop_time_ns);
773
774 dispatch_actor_action(
775 ctx,
776 "set_timer",
777 |actor| {
778 actor.clock().set_timer_ns(
779 &name,
780 interval_ns,
781 start_time_ns,
782 stop_time_ns,
783 None,
784 Some(allow_past != 0),
785 Some(fire_immediately != 0),
786 )
787 },
788 |strategy| {
789 strategy.clock().set_timer_ns(
790 &name,
791 interval_ns,
792 start_time_ns,
793 stop_time_ns,
794 None,
795 Some(allow_past != 0),
796 Some(fire_immediately != 0),
797 )
798 },
799 )
800}
801
802unsafe extern "C" fn host_cancel_timer(
803 ctx: *const HostContext,
804 name: BorrowedStr<'_>,
805) -> PluginResult<()> {
806 let name = unsafe { name.as_str() }.to_string();
808 dispatch_actor_action(
809 ctx,
810 "cancel_timer",
811 |actor| {
812 actor.clock().cancel_timer(&name);
813 Ok(())
814 },
815 |strategy| {
816 strategy.clock().cancel_timer(&name);
817 Ok(())
818 },
819 )
820}
821
822unsafe extern "C" fn host_submit_order(
823 ctx: *const HostContext,
824 command: *const SubmitOrderHandle,
825) -> PluginResult<()> {
826 unsafe {
828 dispatch_handle(ctx, command, "submit_order", |adapter, cmd| {
829 Strategy::submit_order(
830 adapter,
831 cmd.order,
832 cmd.position_id,
833 cmd.client_id,
834 cmd.params,
835 )
836 })
837 }
838}
839
840unsafe extern "C" fn host_cancel_order(
841 ctx: *const HostContext,
842 command: *const CancelOrderHandle,
843) -> PluginResult<()> {
844 unsafe {
846 dispatch_handle(ctx, command, "cancel_order", |adapter, cmd| {
847 Strategy::cancel_order(adapter, cmd.client_order_id, cmd.client_id, cmd.params)
848 })
849 }
850}
851
852unsafe extern "C" fn host_modify_order(
853 ctx: *const HostContext,
854 command: *const ModifyOrderHandle,
855) -> PluginResult<()> {
856 unsafe {
858 dispatch_handle(ctx, command, "modify_order", |adapter, cmd| {
859 Strategy::modify_order(
860 adapter,
861 cmd.client_order_id,
862 cmd.quantity,
863 cmd.price,
864 cmd.trigger_price,
865 cmd.client_id,
866 cmd.params,
867 )
868 })
869 }
870}
871
872unsafe extern "C" fn host_submit_order_list(
873 ctx: *const HostContext,
874 command: *const SubmitOrderListHandle,
875) -> PluginResult<()> {
876 unsafe {
878 dispatch_handle(ctx, command, "submit_order_list", |adapter, cmd| {
879 Strategy::submit_order_list(
880 adapter,
881 cmd.orders,
882 cmd.position_id,
883 cmd.client_id,
884 cmd.params,
885 )
886 })
887 }
888}
889
890unsafe extern "C" fn host_cancel_orders(
891 ctx: *const HostContext,
892 command: *const CancelOrdersHandle,
893) -> PluginResult<()> {
894 unsafe {
896 dispatch_handle(ctx, command, "cancel_orders", |adapter, cmd| {
897 Strategy::cancel_orders(adapter, cmd.client_order_ids, cmd.client_id, cmd.params)
898 })
899 }
900}
901
902unsafe extern "C" fn host_cancel_all_orders(
903 ctx: *const HostContext,
904 command: *const CancelAllOrdersHandle,
905) -> PluginResult<()> {
906 unsafe {
908 dispatch_handle(ctx, command, "cancel_all_orders", |adapter, cmd| {
909 Strategy::cancel_all_orders(
910 adapter,
911 cmd.instrument_id,
912 cmd.order_side,
913 cmd.client_id,
914 cmd.params,
915 )
916 })
917 }
918}
919
920unsafe extern "C" fn host_close_position(
921 ctx: *const HostContext,
922 command: *const ClosePositionHandle,
923) -> PluginResult<()> {
924 unsafe {
926 dispatch_handle(ctx, command, "close_position", |adapter, cmd| {
927 let position = {
928 let cache = adapter.cache();
929 cache.position(&cmd.position_id).map(|p| p.cloned())
930 };
931 let position = position.ok_or_else(|| {
932 anyhow::anyhow!("position '{}' not found in cache", cmd.position_id)
933 })?;
934 Strategy::close_position(
935 adapter,
936 &position,
937 cmd.client_id,
938 cmd.tags,
939 cmd.time_in_force,
940 cmd.reduce_only,
941 cmd.quote_quantity,
942 )
943 })
944 }
945}
946
947unsafe extern "C" fn host_close_all_positions(
948 ctx: *const HostContext,
949 command: *const CloseAllPositionsHandle,
950) -> PluginResult<()> {
951 unsafe {
953 dispatch_handle(ctx, command, "close_all_positions", |adapter, cmd| {
954 Strategy::close_all_positions(
955 adapter,
956 cmd.instrument_id,
957 cmd.position_side,
958 cmd.client_id,
959 cmd.tags,
960 cmd.time_in_force,
961 cmd.reduce_only,
962 cmd.quote_quantity,
963 )
964 })
965 }
966}
967
968unsafe extern "C" fn host_query_account(
969 ctx: *const HostContext,
970 command: *const QueryAccountHandle,
971) -> PluginResult<()> {
972 unsafe {
974 dispatch_handle(ctx, command, "query_account", |adapter, cmd| {
975 Strategy::query_account(adapter, cmd.account_id, cmd.client_id, cmd.params)
976 })
977 }
978}
979
980unsafe extern "C" fn host_query_order(
981 ctx: *const HostContext,
982 command: *const QueryOrderHandle,
983) -> PluginResult<()> {
984 unsafe {
986 dispatch_handle(ctx, command, "query_order", |adapter, cmd| {
987 let order = {
988 let cache = adapter.cache();
989 cache.order(&cmd.client_order_id).map(|o| o.cloned())
990 };
991 let order = order.ok_or_else(|| {
992 anyhow::anyhow!("order '{}' not found in cache", cmd.client_order_id)
993 })?;
994 Strategy::query_order(adapter, &order, cmd.client_id, cmd.params)
995 })
996 }
997}
998
999unsafe fn dispatch_handle<H>(
1014 ctx: *const HostContext,
1015 command: *const H,
1016 method: &'static str,
1017 f: impl FnOnce(&mut PluginStrategyAdapter, H::Command) -> anyhow::Result<()>,
1018) -> PluginResult<()>
1019where
1020 H: BoundaryCommandHandle,
1021{
1022 if command.is_null() {
1023 return PluginResult::Err(PluginError::new(
1024 PluginErrorCode::InvalidArgument,
1025 format!("{method} called with null command handle"),
1026 ));
1027 }
1028
1029 let inner = match unsafe { host_context_inner(ctx) } {
1032 Some(inner) => inner,
1033 None => {
1034 return PluginResult::Err(PluginError::new(
1035 PluginErrorCode::InvalidArgument,
1036 format!("{method} called with null HostContext"),
1037 ));
1038 }
1039 };
1040
1041 if !inner.is_strategy {
1042 return PluginResult::Err(PluginError::new(
1043 PluginErrorCode::InvalidArgument,
1044 format!(
1045 "{method} called from a non-strategy plug-in context (actor_id={})",
1046 inner.actor_id
1047 ),
1048 ));
1049 }
1050
1051 let actor_id = inner.actor_id.inner();
1052 let Some(mut adapter_ref) = try_get_actor_unchecked::<PluginStrategyAdapter>(&actor_id) else {
1053 return PluginResult::Err(PluginError::new(
1054 PluginErrorCode::Generic,
1055 format!(
1056 "{method} could not resolve strategy adapter for actor_id={}",
1057 inner.actor_id
1058 ),
1059 ));
1060 };
1061
1062 let handle = unsafe { &*command };
1065 let command = handle.boundary_normalized_command();
1066 match f(&mut adapter_ref, command) {
1067 Ok(()) => PluginResult::Ok(()),
1068 Err(e) => PluginResult::Err(PluginError::new(PluginErrorCode::Generic, e.to_string())),
1069 }
1070}
1071
1072fn dispatch_actor_action(
1073 ctx: *const HostContext,
1074 method: &'static str,
1075 actor_fn: impl FnOnce(&mut PluginActorAdapter) -> anyhow::Result<()>,
1076 strategy_fn: impl FnOnce(&mut PluginStrategyAdapter) -> anyhow::Result<()>,
1077) -> PluginResult<()> {
1078 let inner = match resolve_context(ctx, method) {
1079 Ok(inner) => inner,
1080 Err(e) => return PluginResult::Err(e),
1081 };
1082
1083 let actor_id = inner.actor_id.inner();
1084 let result = if inner.is_strategy {
1085 let Some(mut adapter_ref) = try_get_actor_unchecked::<PluginStrategyAdapter>(&actor_id)
1086 else {
1087 return PluginResult::Err(resolve_adapter_error(method, inner));
1088 };
1089 strategy_fn(&mut adapter_ref)
1090 } else {
1091 let Some(mut adapter_ref) = try_get_actor_unchecked::<PluginActorAdapter>(&actor_id) else {
1092 return PluginResult::Err(resolve_adapter_error(method, inner));
1093 };
1094 actor_fn(&mut adapter_ref)
1095 };
1096
1097 match result {
1098 Ok(()) => PluginResult::Ok(()),
1099 Err(e) => PluginResult::Err(PluginError::new(PluginErrorCode::Generic, e.to_string())),
1100 }
1101}
1102
1103fn dispatch_cache_query(
1104 ctx: *const HostContext,
1105 method: &'static str,
1106 f: impl FnOnce(&Cache, &HostContextInner) -> PluginResult<OwnedBytes>,
1107) -> PluginResult<OwnedBytes> {
1108 let inner = match resolve_context(ctx, method) {
1109 Ok(inner) => inner,
1110 Err(e) => return PluginResult::Err(e),
1111 };
1112
1113 let actor_id = inner.actor_id.inner();
1114 if inner.is_strategy {
1115 let Some(adapter_ref) = try_get_actor_unchecked::<PluginStrategyAdapter>(&actor_id) else {
1116 return PluginResult::Err(resolve_adapter_error(method, inner));
1117 };
1118 let cache = adapter_ref.cache();
1119 f(&cache, inner)
1120 } else {
1121 let Some(adapter_ref) = try_get_actor_unchecked::<PluginActorAdapter>(&actor_id) else {
1122 return PluginResult::Err(resolve_adapter_error(method, inner));
1123 };
1124 let cache = adapter_ref.cache();
1125 f(&cache, inner)
1126 }
1127}
1128
1129fn resolve_context(
1130 ctx: *const HostContext,
1131 method: &'static str,
1132) -> Result<&'static HostContextInner, PluginError> {
1133 unsafe { host_context_inner(ctx) }.ok_or_else(|| {
1135 PluginError::new(
1136 PluginErrorCode::InvalidArgument,
1137 format!("{method} called with null HostContext"),
1138 )
1139 })
1140}
1141
1142fn resolve_adapter_error(method: &str, inner: &HostContextInner) -> PluginError {
1143 let kind = if inner.is_strategy {
1144 "strategy"
1145 } else {
1146 "actor"
1147 };
1148 PluginError::new(
1149 PluginErrorCode::Generic,
1150 format!(
1151 "{method} could not resolve {kind} adapter for actor_id={}",
1152 inner.actor_id
1153 ),
1154 )
1155}
1156
1157fn ensure_adapter_registered(method: &str, inner: &HostContextInner) -> Result<(), PluginError> {
1158 let actor_id = inner.actor_id.inner();
1159 let found = if inner.is_strategy {
1160 try_get_actor_unchecked::<PluginStrategyAdapter>(&actor_id).is_some()
1161 } else {
1162 try_get_actor_unchecked::<PluginActorAdapter>(&actor_id).is_some()
1163 };
1164
1165 if found {
1166 Ok(())
1167 } else {
1168 Err(resolve_adapter_error(method, inner))
1169 }
1170}
1171
1172fn json_optional<T>(value: Option<&T>) -> PluginResult<OwnedBytes>
1173where
1174 T: Serialize,
1175{
1176 match value {
1177 Some(value) => json_bytes(value),
1178 None => PluginResult::Ok(OwnedBytes::empty()),
1179 }
1180}
1181
1182fn json_bytes<T>(value: &T) -> PluginResult<OwnedBytes>
1183where
1184 T: Serialize,
1185{
1186 match serde_json::to_vec(value) {
1187 Ok(bytes) => PluginResult::Ok(OwnedBytes::from_vec(bytes)),
1188 Err(e) => PluginResult::Err(PluginError::new(
1189 PluginErrorCode::SerializationFailed,
1190 e.to_string(),
1191 )),
1192 }
1193}
1194
1195#[derive(Clone)]
1196struct InstrumentSubscriptionArgs {
1197 instrument_id: InstrumentId,
1198 client_id: Option<ClientId>,
1199 params: Option<Params>,
1200}
1201
1202#[derive(Clone)]
1203struct BarSubscriptionArgs {
1204 bar_type: BarType,
1205 client_id: Option<ClientId>,
1206 params: Option<Params>,
1207}
1208
1209#[derive(Clone)]
1210struct BookSubscriptionArgs {
1211 instrument_id: InstrumentId,
1212 book_type: BookType,
1213 depth: Option<NonZeroUsize>,
1214 client_id: Option<ClientId>,
1215 params: Option<Params>,
1216}
1217
1218fn parse_instrument_subscription(
1219 instrument_id: BorrowedStr<'_>,
1220 client_id: BorrowedStr<'_>,
1221 params_json: BorrowedStr<'_>,
1222) -> Result<InstrumentSubscriptionArgs, PluginError> {
1223 Ok(InstrumentSubscriptionArgs {
1224 instrument_id: parse_instrument_id(instrument_id, "instrument_id")?,
1225 client_id: parse_optional_client_id(client_id)?,
1226 params: parse_optional_params(params_json)?,
1227 })
1228}
1229
1230fn parse_bar_subscription(
1231 bar_type: BorrowedStr<'_>,
1232 client_id: BorrowedStr<'_>,
1233 params_json: BorrowedStr<'_>,
1234) -> Result<BarSubscriptionArgs, PluginError> {
1235 let raw = unsafe { bar_type.as_str() };
1237 let bar_type = BarType::from_str(raw).map_err(|e| {
1238 PluginError::new(
1239 PluginErrorCode::InvalidArgument,
1240 format!("invalid bar_type '{raw}': {e}"),
1241 )
1242 })?;
1243 Ok(BarSubscriptionArgs {
1244 bar_type,
1245 client_id: parse_optional_client_id(client_id)?,
1246 params: parse_optional_params(params_json)?,
1247 })
1248}
1249
1250fn parse_book_subscription(
1251 instrument_id: BorrowedStr<'_>,
1252 book_type: u8,
1253 depth: usize,
1254 client_id: BorrowedStr<'_>,
1255 params_json: BorrowedStr<'_>,
1256) -> Result<BookSubscriptionArgs, PluginError> {
1257 let book_type = BookType::from_u8(book_type).ok_or_else(|| {
1258 PluginError::new(
1259 PluginErrorCode::InvalidArgument,
1260 format!("invalid book_type discriminant {book_type}"),
1261 )
1262 })?;
1263 Ok(BookSubscriptionArgs {
1264 instrument_id: parse_instrument_id(instrument_id, "instrument_id")?,
1265 book_type,
1266 depth: NonZeroUsize::new(depth),
1267 client_id: parse_optional_client_id(client_id)?,
1268 params: parse_optional_params(params_json)?,
1269 })
1270}
1271
1272fn parse_instrument_id(
1273 value: BorrowedStr<'_>,
1274 label: &'static str,
1275) -> Result<InstrumentId, PluginError> {
1276 let raw = unsafe { value.as_str() };
1278 InstrumentId::from_str(raw).map_err(|e| {
1279 PluginError::new(
1280 PluginErrorCode::InvalidArgument,
1281 format!("invalid {label} '{raw}': {e}"),
1282 )
1283 })
1284}
1285
1286fn parse_account_id(value: BorrowedStr<'_>, label: &'static str) -> Result<AccountId, PluginError> {
1287 let raw = unsafe { value.as_str() };
1289 AccountId::new_checked(raw).map_err(|e| {
1290 PluginError::new(
1291 PluginErrorCode::InvalidArgument,
1292 format!("invalid {label} '{raw}': {e}"),
1293 )
1294 })
1295}
1296
1297fn parse_client_order_id(
1298 value: BorrowedStr<'_>,
1299 label: &'static str,
1300) -> Result<ClientOrderId, PluginError> {
1301 let raw = unsafe { value.as_str() };
1303 ClientOrderId::new_checked(raw).map_err(|e| {
1304 PluginError::new(
1305 PluginErrorCode::InvalidArgument,
1306 format!("invalid {label} '{raw}': {e}"),
1307 )
1308 })
1309}
1310
1311fn parse_position_id(
1312 value: BorrowedStr<'_>,
1313 label: &'static str,
1314) -> Result<PositionId, PluginError> {
1315 let raw = unsafe { value.as_str() };
1317 PositionId::new_checked(raw).map_err(|e| {
1318 PluginError::new(
1319 PluginErrorCode::InvalidArgument,
1320 format!("invalid {label} '{raw}': {e}"),
1321 )
1322 })
1323}
1324
1325fn parse_strategy_id_for_context(
1326 value: BorrowedStr<'_>,
1327 inner: &HostContextInner,
1328) -> Result<StrategyId, PluginError> {
1329 let raw = unsafe { value.as_str() };
1331 if !raw.is_empty() {
1332 return StrategyId::new_checked(raw).map_err(|e| {
1333 PluginError::new(
1334 PluginErrorCode::InvalidArgument,
1335 format!("invalid strategy_id '{raw}': {e}"),
1336 )
1337 });
1338 }
1339
1340 if !inner.is_strategy {
1341 return Err(PluginError::new(
1342 PluginErrorCode::InvalidArgument,
1343 "empty strategy_id is only valid for strategy plug-in contexts",
1344 ));
1345 }
1346
1347 StrategyId::new_checked(inner.actor_id.inner().as_str()).map_err(|e| {
1348 PluginError::new(
1349 PluginErrorCode::InvalidArgument,
1350 format!("invalid calling strategy_id '{}': {e}", inner.actor_id),
1351 )
1352 })
1353}
1354
1355fn parse_optional_client_id(value: BorrowedStr<'_>) -> Result<Option<ClientId>, PluginError> {
1356 let raw = unsafe { value.as_str() };
1358 if raw.is_empty() {
1359 return Ok(None);
1360 }
1361 ClientId::new_checked(raw)
1362 .map(Some)
1363 .map_err(|e| PluginError::new(PluginErrorCode::InvalidArgument, e.to_string()))
1364}
1365
1366fn parse_optional_params(value: BorrowedStr<'_>) -> Result<Option<Params>, PluginError> {
1367 let raw = unsafe { value.as_str() };
1369 if raw.trim().is_empty() {
1370 return Ok(None);
1371 }
1372 serde_json::from_str(raw).map(Some).map_err(|e| {
1373 PluginError::new(
1374 PluginErrorCode::InvalidArgument,
1375 format!("invalid params_json: {e}"),
1376 )
1377 })
1378}
1379
1380fn nonzero_unix_nanos(value: u64) -> Option<UnixNanos> {
1381 (value != 0).then_some(UnixNanos::from(value))
1382}
1383
1384#[cfg(test)]
1385mod tests {
1386 use nautilus_core::{UUID4, UnixNanos};
1387 use nautilus_model::{
1388 enums::{OrderSide, TimeInForce},
1389 identifiers::{
1390 ClientOrderId as TestClientOrderId, InstrumentId as TestInstrumentId, StrategyId,
1391 TraderId,
1392 },
1393 orders::{MarketOrder, OrderAny},
1394 types::Quantity,
1395 };
1396 use rstest::rstest;
1397
1398 use super::*;
1399 use crate::surfaces::commands::{CancelOrderCommand, ModifyOrderCommand, SubmitOrderCommand};
1400
1401 fn make_market_submit_command() -> SubmitOrderCommand {
1402 let order = OrderAny::Market(MarketOrder::new(
1403 TraderId::from("TRADER-001"),
1404 StrategyId::from("S-001"),
1405 TestInstrumentId::from("ETH-USDT.BINANCE"),
1406 TestClientOrderId::from("O-1"),
1407 OrderSide::Buy,
1408 Quantity::from("1.0"),
1409 TimeInForce::Gtc,
1410 UUID4::new(),
1411 UnixNanos::default(),
1412 false,
1413 false,
1414 None,
1415 None,
1416 None,
1417 None,
1418 None,
1419 None,
1420 None,
1421 None,
1422 ));
1423 SubmitOrderCommand::new(order, None, None, None)
1424 }
1425
1426 #[rstest]
1427 fn host_vtable_carries_compiled_abi() {
1428 let p = host_vtable();
1429 assert!(!p.is_null());
1430 let v = unsafe { &*p };
1432 assert_eq!(v.abi_version, NAUTILUS_PLUGIN_ABI_VERSION);
1433 }
1434
1435 #[rstest]
1436 fn host_vtable_binds_live_node_callbacks() {
1437 let p = host_vtable();
1440 let v = unsafe { &*p };
1442 assert_eq!(
1443 v.cache_order as *const () as usize,
1444 host_cache_order as *const () as usize,
1445 );
1446 assert_eq!(
1447 v.subscribe_quotes as *const () as usize,
1448 host_subscribe_quotes as *const () as usize,
1449 );
1450 assert_eq!(
1451 v.msgbus_publish as *const () as usize,
1452 host_msgbus_publish as *const () as usize,
1453 );
1454 assert_eq!(
1455 v.set_timer as *const () as usize,
1456 host_set_timer as *const () as usize,
1457 );
1458 assert_eq!(
1459 v.submit_order as *const () as usize,
1460 host_submit_order as *const () as usize,
1461 );
1462 assert_eq!(
1463 v.cancel_order as *const () as usize,
1464 host_cancel_order as *const () as usize,
1465 );
1466 assert_eq!(
1467 v.modify_order as *const () as usize,
1468 host_modify_order as *const () as usize,
1469 );
1470 assert_eq!(
1471 v.clock_now_ns as *const () as usize,
1472 host_clock_now_ns as *const () as usize,
1473 );
1474 assert_eq!(v.log as *const () as usize, host_log as *const () as usize);
1475 }
1476
1477 #[rstest]
1478 fn host_clock_now_ns_returns_unix_nanos_after_2020() {
1479 let p = host_vtable();
1480 let v = unsafe { &*p };
1482 let now = unsafe { (v.clock_now_ns)() };
1484 assert!(now > 1_577_836_800_000_000_000u64);
1486 }
1487
1488 #[rstest]
1489 fn host_submit_order_rejects_null_ctx() {
1490 let p = host_vtable();
1491 let v = unsafe { &*p };
1493 let handle = SubmitOrderHandle::new(make_market_submit_command());
1494 let r = unsafe { (v.submit_order)(std::ptr::null(), &raw const handle) };
1496 let err = r.into_result().unwrap_err();
1497 assert_eq!(err.code, PluginErrorCode::InvalidArgument);
1498 assert!(err.message_string().contains("null HostContext"));
1499 }
1500
1501 #[rstest]
1502 fn host_cancel_order_rejects_null_ctx() {
1503 use nautilus_model::identifiers::ClientOrderId;
1504
1505 let p = host_vtable();
1506 let v = unsafe { &*p };
1508 let handle = CancelOrderHandle::new(CancelOrderCommand::new(
1509 ClientOrderId::from("O-1"),
1510 None,
1511 None,
1512 ));
1513 let r = unsafe { (v.cancel_order)(std::ptr::null(), &raw const handle) };
1515 let err = r.into_result().unwrap_err();
1516 assert_eq!(err.code, PluginErrorCode::InvalidArgument);
1517 }
1518
1519 #[rstest]
1520 fn host_modify_order_rejects_null_ctx() {
1521 use nautilus_model::identifiers::ClientOrderId;
1522
1523 let p = host_vtable();
1524 let v = unsafe { &*p };
1526 let handle = ModifyOrderHandle::new(ModifyOrderCommand::new(
1527 ClientOrderId::from("O-1"),
1528 None,
1529 None,
1530 None,
1531 None,
1532 None,
1533 ));
1534 let r = unsafe { (v.modify_order)(std::ptr::null(), &raw const handle) };
1536 let err = r.into_result().unwrap_err();
1537 assert_eq!(err.code, PluginErrorCode::InvalidArgument);
1538 }
1539
1540 #[rstest]
1541 fn host_cancel_order_rejects_null_command() {
1542 let p = host_vtable();
1543 let v = unsafe { &*p };
1545 let r = unsafe { (v.cancel_order)(std::ptr::null(), std::ptr::null()) };
1547 let err = r.into_result().unwrap_err();
1548 assert_eq!(err.code, PluginErrorCode::InvalidArgument);
1549 assert!(err.message_string().contains("null command handle"));
1550 }
1551
1552 #[rstest]
1553 fn host_modify_order_rejects_null_command() {
1554 let p = host_vtable();
1555 let v = unsafe { &*p };
1557 let r = unsafe { (v.modify_order)(std::ptr::null(), std::ptr::null()) };
1559 let err = r.into_result().unwrap_err();
1560 assert_eq!(err.code, PluginErrorCode::InvalidArgument);
1561 assert!(err.message_string().contains("null command handle"));
1562 }
1563
1564 #[rstest]
1565 fn host_submit_order_rejects_non_strategy_context() {
1566 use nautilus_model::identifiers::ActorId;
1570
1571 use crate::bridge::registry::{
1572 HostContextInner, drop_host_context, host_context_test_lock, leak_host_context,
1573 };
1574
1575 let _guard = host_context_test_lock();
1576 let ctx = leak_host_context(HostContextInner {
1577 actor_id: ActorId::from("ActorContextProbe"),
1578 is_strategy: false,
1579 });
1580 let p = host_vtable();
1581 let v = unsafe { &*p };
1583 let handle = SubmitOrderHandle::new(make_market_submit_command());
1584 let r = unsafe { (v.submit_order)(ctx, &raw const handle) };
1586 let err = r.into_result().unwrap_err();
1587 assert_eq!(err.code, PluginErrorCode::InvalidArgument);
1588 assert!(
1589 err.message_string().contains("non-strategy"),
1590 "expected non-strategy rejection, was: {}",
1591 err.message_string(),
1592 );
1593 unsafe { drop_host_context(ctx) };
1595 }
1596
1597 #[rstest]
1598 fn host_submit_order_rejects_unregistered_actor_id() {
1599 use nautilus_model::identifiers::ActorId;
1603
1604 use crate::bridge::registry::{
1605 HostContextInner, drop_host_context, host_context_test_lock, leak_host_context,
1606 };
1607
1608 let _guard = host_context_test_lock();
1609 let ctx = leak_host_context(HostContextInner {
1610 actor_id: ActorId::from("UnregisteredStrategyAdapter"),
1611 is_strategy: true,
1612 });
1613 let p = host_vtable();
1614 let v = unsafe { &*p };
1616 let handle = SubmitOrderHandle::new(make_market_submit_command());
1617 let r = unsafe { (v.submit_order)(ctx, &raw const handle) };
1619 let err = r.into_result().unwrap_err();
1620 assert_eq!(err.code, PluginErrorCode::Generic);
1621 assert!(
1622 err.message_string().contains("could not resolve"),
1623 "expected unresolved-adapter rejection, was: {}",
1624 err.message_string(),
1625 );
1626 unsafe { drop_host_context(ctx) };
1628 }
1629}