1#![allow(unsafe_code)]
30#![allow(
31 clippy::multiple_unsafe_ops_per_block,
32 reason = "vtable deref and FFI call form a single boundary callback; \
33 SAFETY comments cover both ops together"
34)]
35
36use std::{
37 any::Any,
38 fmt::Debug,
39 panic::{AssertUnwindSafe, catch_unwind},
40};
41
42use nautilus_common::{actor::DataActor, signal::Signal, timer::TimeEvent};
43use nautilus_model::{
44 data::{
45 Bar, CustomData, FundingRateUpdate, IndexPriceUpdate, InstrumentClose, InstrumentStatus,
46 MarkPriceUpdate, OptionChainSlice, OptionGreeks, OrderBookDelta, OrderBookDeltas,
47 OrderBookDepth10, QuoteTick, TradeTick,
48 },
49 events::{
50 OrderAccepted, OrderCancelRejected, OrderCanceled, OrderDenied, OrderEmulated,
51 OrderExpired, OrderFilled, OrderInitialized, OrderModifyRejected, OrderPendingCancel,
52 OrderPendingUpdate, OrderRejected, OrderReleased, OrderSubmitted, OrderTriggered,
53 OrderUpdated, PositionChanged, PositionClosed, PositionOpened,
54 },
55 identifiers::ActorId,
56 instruments::InstrumentAny,
57 orderbook::OrderBook,
58};
59use nautilus_trading::{
60 nautilus_strategy,
61 strategy::{Strategy, StrategyConfig, StrategyCore},
62};
63
64use crate::{
65 boundary::{BorrowedStr, PluginResult, Slice},
66 bridge::{
67 custom_data::{try_custom_data_boundary_ref, try_historical_custom_data_boundary_ref},
68 registry::{HostContextInner, drop_host_context, leak_host_context},
69 },
70 host::{HostContext, HostVTable},
71 manifest::ValidatedStrategyVTable,
72 surfaces::{
73 book::{OrderBookDeltasHandle, OrderBookHandle},
74 custom_data::PluginCustomDataRef,
75 instrument::InstrumentAnyHandle,
76 option_chain::OptionChainSliceHandle,
77 strategy::PluginStrategyHandle,
78 },
79};
80
81pub struct PluginStrategyAdapter {
85 core: StrategyCore,
86 plugin_name: String,
87 type_name: String,
88 vtable: ValidatedStrategyVTable,
89 handle: *mut PluginStrategyHandle,
90 ctx: *const HostContext,
91}
92
93unsafe impl Send for PluginStrategyAdapter {}
98
99impl Debug for PluginStrategyAdapter {
100 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
101 f.debug_struct(stringify!(PluginStrategyAdapter))
102 .field("plugin_name", &self.plugin_name)
103 .field("type_name", &self.type_name)
104 .field("actor_id", &self.core.actor_id())
105 .finish()
106 }
107}
108
109impl PluginStrategyAdapter {
110 pub unsafe fn new(
136 strategy_config: StrategyConfig,
137 plugin_name: impl Into<String>,
138 type_name: impl Into<String>,
139 vtable: ValidatedStrategyVTable,
140 host: *const HostVTable,
141 config_json: &str,
142 ) -> anyhow::Result<Self> {
143 if strategy_config.strategy_id.is_none() {
144 anyhow::bail!(
145 "PluginStrategyAdapter requires StrategyConfig::strategy_id to be set so the \
146 host context's actor_id stays stable across Trader::add_strategy"
147 );
148 }
149
150 let plugin_name = plugin_name.into();
151 let type_name = type_name.into();
152 let create = unsafe { validated_slot!(StrategyVTable, vtable.as_ptr(), create) };
154 let core = StrategyCore::new(strategy_config);
155 let actor_id = ActorId::from(core.actor_id().inner().as_str());
156
157 let ctx = leak_host_context(HostContextInner {
158 actor_id,
159 is_strategy: true,
160 });
161
162 let cfg = BorrowedStr::from_str(config_json);
163 let handle = guard_call(&plugin_name, &type_name, "create", || unsafe {
166 create(host, ctx, cfg)
167 })
168 .ok_or_else(|| {
169 unsafe { drop_host_context(ctx) };
171 anyhow::anyhow!("plug-in strategy '{type_name}' panicked in create")
172 })?;
173
174 if handle.is_null() {
175 unsafe { drop_host_context(ctx) };
177 anyhow::bail!("plug-in strategy '{type_name}' returned a null handle from create");
178 }
179
180 Ok(Self {
181 core,
182 plugin_name,
183 type_name,
184 vtable,
185 handle,
186 ctx,
187 })
188 }
189
190 #[must_use]
192 pub fn type_name(&self) -> &str {
193 &self.type_name
194 }
195
196 #[must_use]
198 pub fn plugin_name(&self) -> &str {
199 &self.plugin_name
200 }
201}
202
203impl Drop for PluginStrategyAdapter {
204 fn drop(&mut self) {
205 if !self.handle.is_null() {
206 let _ = catch_unwind(AssertUnwindSafe(|| {
207 unsafe {
209 validated_slot!(StrategyVTable, self.vtable.as_ptr(), drop_handle)(self.handle);
210 };
211 }));
212 self.handle = std::ptr::null_mut();
213 }
214 unsafe { drop_host_context(self.ctx) };
216 self.ctx = std::ptr::null();
217 }
218}
219
220nautilus_strategy!(PluginStrategyAdapter, core, {
221 fn on_order_initialized(&mut self, event: OrderInitialized) {
222 log_strategy_hook_error(
223 "on_order_initialized",
224 self.forward_order_initialized(&event),
225 );
226 }
227
228 fn on_order_submitted(&mut self, event: OrderSubmitted) {
229 log_strategy_hook_error("on_order_submitted", self.forward_order_submitted(&event));
230 }
231
232 fn on_order_accepted(&mut self, event: OrderAccepted) {
233 log_strategy_hook_error("on_order_accepted", self.forward_order_accepted(&event));
234 }
235
236 fn on_order_rejected(&mut self, event: OrderRejected) {
237 log_strategy_hook_error("on_order_rejected", self.forward_order_rejected(&event));
238 }
239
240 fn on_order_expired(&mut self, event: OrderExpired) {
241 log_strategy_hook_error("on_order_expired", self.forward_order_expired(&event));
242 }
243
244 fn on_order_triggered(&mut self, event: OrderTriggered) {
245 log_strategy_hook_error("on_order_triggered", self.forward_order_triggered(&event));
246 }
247
248 fn on_order_denied(&mut self, event: OrderDenied) {
249 log_strategy_hook_error("on_order_denied", self.forward_order_denied(&event));
250 }
251
252 fn on_order_emulated(&mut self, event: OrderEmulated) {
253 log_strategy_hook_error("on_order_emulated", self.forward_order_emulated(&event));
254 }
255
256 fn on_order_released(&mut self, event: OrderReleased) {
257 log_strategy_hook_error("on_order_released", self.forward_order_released(&event));
258 }
259
260 fn on_order_pending_update(&mut self, event: OrderPendingUpdate) {
261 log_strategy_hook_error(
262 "on_order_pending_update",
263 self.forward_order_pending_update(&event),
264 );
265 }
266
267 fn on_order_pending_cancel(&mut self, event: OrderPendingCancel) {
268 log_strategy_hook_error(
269 "on_order_pending_cancel",
270 self.forward_order_pending_cancel(&event),
271 );
272 }
273
274 fn on_order_modify_rejected(&mut self, event: OrderModifyRejected) {
275 log_strategy_hook_error(
276 "on_order_modify_rejected",
277 self.forward_order_modify_rejected(&event),
278 );
279 }
280
281 fn on_order_cancel_rejected(&mut self, event: OrderCancelRejected) {
282 log_strategy_hook_error(
283 "on_order_cancel_rejected",
284 self.forward_order_cancel_rejected(&event),
285 );
286 }
287
288 fn on_order_updated(&mut self, event: OrderUpdated) {
289 log_strategy_hook_error("on_order_updated", self.forward_order_updated(&event));
290 }
291
292 fn on_position_opened(&mut self, event: PositionOpened) {
293 log_strategy_hook_error("on_position_opened", self.forward_position_opened(&event));
294 }
295
296 fn on_position_changed(&mut self, event: PositionChanged) {
297 log_strategy_hook_error("on_position_changed", self.forward_position_changed(&event));
298 }
299
300 fn on_position_closed(&mut self, event: PositionClosed) {
301 log_strategy_hook_error("on_position_closed", self.forward_position_closed(&event));
302 }
303
304 fn on_market_exit(&mut self) {
305 log_strategy_hook_error("on_market_exit", self.forward_market_exit());
306 }
307});
308
309impl DataActor for PluginStrategyAdapter {
310 fn on_start(&mut self) -> anyhow::Result<()> {
311 Strategy::on_start(self)?;
315 invoke_lifecycle(self, "on_start", |adapter| unsafe {
316 validated_slot!(StrategyVTable, adapter.vtable.as_ptr(), on_start)(adapter.handle)
317 })
318 }
319
320 fn on_stop(&mut self) -> anyhow::Result<()> {
321 invoke_lifecycle(self, "on_stop", |adapter| unsafe {
322 validated_slot!(StrategyVTable, adapter.vtable.as_ptr(), on_stop)(adapter.handle)
323 })
324 }
325
326 fn on_resume(&mut self) -> anyhow::Result<()> {
327 invoke_lifecycle(self, "on_resume", |adapter| unsafe {
328 validated_slot!(StrategyVTable, adapter.vtable.as_ptr(), on_resume)(adapter.handle)
329 })
330 }
331
332 fn on_reset(&mut self) -> anyhow::Result<()> {
333 invoke_lifecycle(self, "on_reset", |adapter| unsafe {
334 validated_slot!(StrategyVTable, adapter.vtable.as_ptr(), on_reset)(adapter.handle)
335 })
336 }
337
338 fn on_dispose(&mut self) -> anyhow::Result<()> {
339 invoke_lifecycle(self, "on_dispose", |adapter| unsafe {
340 validated_slot!(StrategyVTable, adapter.vtable.as_ptr(), on_dispose)(adapter.handle)
341 })
342 }
343
344 fn on_degrade(&mut self) -> anyhow::Result<()> {
345 invoke_lifecycle(self, "on_degrade", |adapter| unsafe {
346 validated_slot!(StrategyVTable, adapter.vtable.as_ptr(), on_degrade)(adapter.handle)
347 })
348 }
349
350 fn on_fault(&mut self) -> anyhow::Result<()> {
351 invoke_lifecycle(self, "on_fault", |adapter| unsafe {
352 validated_slot!(StrategyVTable, adapter.vtable.as_ptr(), on_fault)(adapter.handle)
353 })
354 }
355
356 fn on_time_event(&mut self, event: &TimeEvent) -> anyhow::Result<()> {
357 Strategy::on_time_event(self, event)?;
361 invoke_event(self, "on_time_event", event, |adapter, p| unsafe {
362 validated_slot!(StrategyVTable, adapter.vtable.as_ptr(), on_time_event)(
363 adapter.handle,
364 p,
365 )
366 })
367 }
368
369 fn on_data(&mut self, data: &CustomData) -> anyhow::Result<()> {
370 let Some(data_ref) = try_custom_data_boundary_ref(data) else {
371 return Ok(());
372 };
373 invoke_custom_data(self, "on_data", data_ref, |adapter, value| unsafe {
374 validated_slot!(StrategyVTable, adapter.vtable.as_ptr(), on_data)(adapter.handle, value)
375 })
376 }
377
378 fn on_instrument(&mut self, instrument: &InstrumentAny) -> anyhow::Result<()> {
379 let handle = InstrumentAnyHandle::new(instrument.clone());
380 invoke_event(self, "on_instrument", &handle, |adapter, p| unsafe {
381 validated_slot!(StrategyVTable, adapter.vtable.as_ptr(), on_instrument)(
382 adapter.handle,
383 p,
384 )
385 })
386 }
387
388 fn on_book_deltas(&mut self, deltas: &OrderBookDeltas) -> anyhow::Result<()> {
389 let handle = OrderBookDeltasHandle::new(deltas.clone());
390 invoke_event(self, "on_book_deltas", &handle, |adapter, p| unsafe {
391 validated_slot!(StrategyVTable, adapter.vtable.as_ptr(), on_book_deltas)(
392 adapter.handle,
393 p,
394 )
395 })
396 }
397
398 fn on_book(&mut self, book: &OrderBook) -> anyhow::Result<()> {
399 let handle = OrderBookHandle::new(book.clone());
400 invoke_event(self, "on_book", &handle, |adapter, p| unsafe {
401 validated_slot!(StrategyVTable, adapter.vtable.as_ptr(), on_book)(adapter.handle, p)
402 })
403 }
404
405 fn on_quote(&mut self, quote: &QuoteTick) -> anyhow::Result<()> {
406 invoke_event(self, "on_quote", quote, |adapter, p| unsafe {
407 validated_slot!(StrategyVTable, adapter.vtable.as_ptr(), on_quote)(adapter.handle, p)
408 })
409 }
410
411 fn on_trade(&mut self, trade: &TradeTick) -> anyhow::Result<()> {
412 invoke_event(self, "on_trade", trade, |adapter, p| unsafe {
413 validated_slot!(StrategyVTable, adapter.vtable.as_ptr(), on_trade)(adapter.handle, p)
414 })
415 }
416
417 fn on_bar(&mut self, bar: &Bar) -> anyhow::Result<()> {
418 invoke_event(self, "on_bar", bar, |adapter, p| unsafe {
419 validated_slot!(StrategyVTable, adapter.vtable.as_ptr(), on_bar)(adapter.handle, p)
420 })
421 }
422
423 fn on_mark_price(&mut self, mark_price: &MarkPriceUpdate) -> anyhow::Result<()> {
424 invoke_event(self, "on_mark_price", mark_price, |adapter, p| unsafe {
425 validated_slot!(StrategyVTable, adapter.vtable.as_ptr(), on_mark_price)(
426 adapter.handle,
427 p,
428 )
429 })
430 }
431
432 fn on_index_price(&mut self, index_price: &IndexPriceUpdate) -> anyhow::Result<()> {
433 invoke_event(self, "on_index_price", index_price, |adapter, p| unsafe {
434 validated_slot!(StrategyVTable, adapter.vtable.as_ptr(), on_index_price)(
435 adapter.handle,
436 p,
437 )
438 })
439 }
440
441 fn on_funding_rate(&mut self, funding_rate: &FundingRateUpdate) -> anyhow::Result<()> {
442 invoke_event(self, "on_funding_rate", funding_rate, |adapter, p| unsafe {
443 validated_slot!(StrategyVTable, adapter.vtable.as_ptr(), on_funding_rate)(
444 adapter.handle,
445 p,
446 )
447 })
448 }
449
450 fn on_option_greeks(&mut self, greeks: &OptionGreeks) -> anyhow::Result<()> {
451 invoke_event(self, "on_option_greeks", greeks, |adapter, p| unsafe {
452 validated_slot!(StrategyVTable, adapter.vtable.as_ptr(), on_option_greeks)(
453 adapter.handle,
454 p,
455 )
456 })
457 }
458
459 fn on_option_chain(&mut self, chain: &OptionChainSlice) -> anyhow::Result<()> {
460 let handle = OptionChainSliceHandle::new(chain.clone());
461 invoke_event(self, "on_option_chain", &handle, |adapter, p| unsafe {
462 validated_slot!(StrategyVTable, adapter.vtable.as_ptr(), on_option_chain)(
463 adapter.handle,
464 p,
465 )
466 })
467 }
468
469 fn on_instrument_status(&mut self, data: &InstrumentStatus) -> anyhow::Result<()> {
470 invoke_event(self, "on_instrument_status", data, |adapter, p| unsafe {
471 validated_slot!(
472 StrategyVTable,
473 adapter.vtable.as_ptr(),
474 on_instrument_status
475 )(adapter.handle, p)
476 })
477 }
478
479 fn on_instrument_close(&mut self, update: &InstrumentClose) -> anyhow::Result<()> {
480 invoke_event(self, "on_instrument_close", update, |adapter, p| unsafe {
481 validated_slot!(StrategyVTable, adapter.vtable.as_ptr(), on_instrument_close)(
482 adapter.handle,
483 p,
484 )
485 })
486 }
487
488 fn on_order_filled(&mut self, event: &OrderFilled) -> anyhow::Result<()> {
489 invoke_event(self, "on_order_filled", event, |adapter, p| unsafe {
490 validated_slot!(StrategyVTable, adapter.vtable.as_ptr(), on_order_filled)(
491 adapter.handle,
492 p,
493 )
494 })
495 }
496
497 fn on_order_canceled(&mut self, event: &OrderCanceled) -> anyhow::Result<()> {
498 invoke_event(self, "on_order_canceled", event, |adapter, p| unsafe {
499 validated_slot!(StrategyVTable, adapter.vtable.as_ptr(), on_order_canceled)(
500 adapter.handle,
501 p,
502 )
503 })
504 }
505
506 fn on_signal(&mut self, signal: &Signal) -> anyhow::Result<()> {
507 invoke_event(self, "on_signal", signal, |adapter, p| unsafe {
508 validated_slot!(StrategyVTable, adapter.vtable.as_ptr(), on_signal)(adapter.handle, p)
509 })
510 }
511
512 fn on_historical_data(&mut self, data: &dyn Any) -> anyhow::Result<()> {
513 let Some(data_ref) = try_historical_custom_data_boundary_ref(data) else {
514 return Ok(());
515 };
516 invoke_custom_data(
517 self,
518 "on_historical_data",
519 data_ref,
520 |adapter, value| unsafe {
521 validated_slot!(StrategyVTable, adapter.vtable.as_ptr(), on_data)(
522 adapter.handle,
523 value,
524 )
525 },
526 )
527 }
528
529 fn on_historical_book_deltas(&mut self, deltas: &[OrderBookDelta]) -> anyhow::Result<()> {
530 invoke_slice(
531 self,
532 "on_historical_book_deltas",
533 deltas,
534 |adapter, s| unsafe {
535 validated_slot!(
536 StrategyVTable,
537 adapter.vtable.as_ptr(),
538 on_historical_book_deltas
539 )(adapter.handle, s)
540 },
541 )
542 }
543
544 fn on_historical_book_depth(&mut self, depths: &[OrderBookDepth10]) -> anyhow::Result<()> {
545 invoke_slice(
546 self,
547 "on_historical_book_depth",
548 depths,
549 |adapter, s| unsafe {
550 validated_slot!(
551 StrategyVTable,
552 adapter.vtable.as_ptr(),
553 on_historical_book_depth
554 )(adapter.handle, s)
555 },
556 )
557 }
558
559 fn on_historical_quotes(&mut self, quotes: &[QuoteTick]) -> anyhow::Result<()> {
560 invoke_slice(self, "on_historical_quotes", quotes, |adapter, s| unsafe {
561 validated_slot!(
562 StrategyVTable,
563 adapter.vtable.as_ptr(),
564 on_historical_quotes
565 )(adapter.handle, s)
566 })
567 }
568
569 fn on_historical_trades(&mut self, trades: &[TradeTick]) -> anyhow::Result<()> {
570 invoke_slice(self, "on_historical_trades", trades, |adapter, s| unsafe {
571 validated_slot!(
572 StrategyVTable,
573 adapter.vtable.as_ptr(),
574 on_historical_trades
575 )(adapter.handle, s)
576 })
577 }
578
579 fn on_historical_bars(&mut self, bars: &[Bar]) -> anyhow::Result<()> {
580 invoke_slice(self, "on_historical_bars", bars, |adapter, s| unsafe {
581 validated_slot!(StrategyVTable, adapter.vtable.as_ptr(), on_historical_bars)(
582 adapter.handle,
583 s,
584 )
585 })
586 }
587
588 fn on_historical_mark_prices(&mut self, mark_prices: &[MarkPriceUpdate]) -> anyhow::Result<()> {
589 invoke_slice(
590 self,
591 "on_historical_mark_prices",
592 mark_prices,
593 |adapter, s| unsafe {
594 validated_slot!(
595 StrategyVTable,
596 adapter.vtable.as_ptr(),
597 on_historical_mark_prices
598 )(adapter.handle, s)
599 },
600 )
601 }
602
603 fn on_historical_index_prices(
604 &mut self,
605 index_prices: &[IndexPriceUpdate],
606 ) -> anyhow::Result<()> {
607 invoke_slice(
608 self,
609 "on_historical_index_prices",
610 index_prices,
611 |adapter, s| unsafe {
612 validated_slot!(
613 StrategyVTable,
614 adapter.vtable.as_ptr(),
615 on_historical_index_prices
616 )(adapter.handle, s)
617 },
618 )
619 }
620
621 fn on_historical_funding_rates(
622 &mut self,
623 funding_rates: &[FundingRateUpdate],
624 ) -> anyhow::Result<()> {
625 invoke_slice(
626 self,
627 "on_historical_funding_rates",
628 funding_rates,
629 |adapter, s| unsafe {
630 validated_slot!(
631 StrategyVTable,
632 adapter.vtable.as_ptr(),
633 on_historical_funding_rates
634 )(adapter.handle, s)
635 },
636 )
637 }
638}
639
640impl PluginStrategyAdapter {
645 fn forward_order_initialized(&self, event: &OrderInitialized) -> anyhow::Result<()> {
646 invoke_event(self, "on_order_initialized", event, |adapter, p| unsafe {
647 validated_slot!(
648 StrategyVTable,
649 adapter.vtable.as_ptr(),
650 on_order_initialized
651 )(adapter.handle, p)
652 })
653 }
654
655 fn forward_order_submitted(&self, event: &OrderSubmitted) -> anyhow::Result<()> {
656 invoke_event(self, "on_order_submitted", event, |adapter, p| unsafe {
657 validated_slot!(StrategyVTable, adapter.vtable.as_ptr(), on_order_submitted)(
658 adapter.handle,
659 p,
660 )
661 })
662 }
663
664 fn forward_order_accepted(&self, event: &OrderAccepted) -> anyhow::Result<()> {
665 invoke_event(self, "on_order_accepted", event, |adapter, p| unsafe {
666 validated_slot!(StrategyVTable, adapter.vtable.as_ptr(), on_order_accepted)(
667 adapter.handle,
668 p,
669 )
670 })
671 }
672
673 fn forward_order_rejected(&self, event: &OrderRejected) -> anyhow::Result<()> {
674 invoke_event(self, "on_order_rejected", event, |adapter, p| unsafe {
675 validated_slot!(StrategyVTable, adapter.vtable.as_ptr(), on_order_rejected)(
676 adapter.handle,
677 p,
678 )
679 })
680 }
681
682 fn forward_order_expired(&self, event: &OrderExpired) -> anyhow::Result<()> {
683 invoke_event(self, "on_order_expired", event, |adapter, p| unsafe {
684 validated_slot!(StrategyVTable, adapter.vtable.as_ptr(), on_order_expired)(
685 adapter.handle,
686 p,
687 )
688 })
689 }
690
691 fn forward_order_triggered(&self, event: &OrderTriggered) -> anyhow::Result<()> {
692 invoke_event(self, "on_order_triggered", event, |adapter, p| unsafe {
693 validated_slot!(StrategyVTable, adapter.vtable.as_ptr(), on_order_triggered)(
694 adapter.handle,
695 p,
696 )
697 })
698 }
699
700 fn forward_order_denied(&self, event: &OrderDenied) -> anyhow::Result<()> {
701 invoke_event(self, "on_order_denied", event, |adapter, p| unsafe {
702 validated_slot!(StrategyVTable, adapter.vtable.as_ptr(), on_order_denied)(
703 adapter.handle,
704 p,
705 )
706 })
707 }
708
709 fn forward_order_emulated(&self, event: &OrderEmulated) -> anyhow::Result<()> {
710 invoke_event(self, "on_order_emulated", event, |adapter, p| unsafe {
711 validated_slot!(StrategyVTable, adapter.vtable.as_ptr(), on_order_emulated)(
712 adapter.handle,
713 p,
714 )
715 })
716 }
717
718 fn forward_order_released(&self, event: &OrderReleased) -> anyhow::Result<()> {
719 invoke_event(self, "on_order_released", event, |adapter, p| unsafe {
720 validated_slot!(StrategyVTable, adapter.vtable.as_ptr(), on_order_released)(
721 adapter.handle,
722 p,
723 )
724 })
725 }
726
727 fn forward_order_pending_update(&self, event: &OrderPendingUpdate) -> anyhow::Result<()> {
728 invoke_event(
729 self,
730 "on_order_pending_update",
731 event,
732 |adapter, p| unsafe {
733 validated_slot!(
734 StrategyVTable,
735 adapter.vtable.as_ptr(),
736 on_order_pending_update
737 )(adapter.handle, p)
738 },
739 )
740 }
741
742 fn forward_order_pending_cancel(&self, event: &OrderPendingCancel) -> anyhow::Result<()> {
743 invoke_event(
744 self,
745 "on_order_pending_cancel",
746 event,
747 |adapter, p| unsafe {
748 validated_slot!(
749 StrategyVTable,
750 adapter.vtable.as_ptr(),
751 on_order_pending_cancel
752 )(adapter.handle, p)
753 },
754 )
755 }
756
757 fn forward_order_modify_rejected(&self, event: &OrderModifyRejected) -> anyhow::Result<()> {
758 invoke_event(
759 self,
760 "on_order_modify_rejected",
761 event,
762 |adapter, p| unsafe {
763 validated_slot!(
764 StrategyVTable,
765 adapter.vtable.as_ptr(),
766 on_order_modify_rejected
767 )(adapter.handle, p)
768 },
769 )
770 }
771
772 fn forward_order_cancel_rejected(&self, event: &OrderCancelRejected) -> anyhow::Result<()> {
773 invoke_event(
774 self,
775 "on_order_cancel_rejected",
776 event,
777 |adapter, p| unsafe {
778 validated_slot!(
779 StrategyVTable,
780 adapter.vtable.as_ptr(),
781 on_order_cancel_rejected
782 )(adapter.handle, p)
783 },
784 )
785 }
786
787 fn forward_order_updated(&self, event: &OrderUpdated) -> anyhow::Result<()> {
788 invoke_event(self, "on_order_updated", event, |adapter, p| unsafe {
789 validated_slot!(StrategyVTable, adapter.vtable.as_ptr(), on_order_updated)(
790 adapter.handle,
791 p,
792 )
793 })
794 }
795
796 fn forward_position_opened(&self, event: &PositionOpened) -> anyhow::Result<()> {
797 invoke_event(self, "on_position_opened", event, |adapter, p| unsafe {
798 validated_slot!(StrategyVTable, adapter.vtable.as_ptr(), on_position_opened)(
799 adapter.handle,
800 p,
801 )
802 })
803 }
804
805 fn forward_position_changed(&self, event: &PositionChanged) -> anyhow::Result<()> {
806 invoke_event(self, "on_position_changed", event, |adapter, p| unsafe {
807 validated_slot!(StrategyVTable, adapter.vtable.as_ptr(), on_position_changed)(
808 adapter.handle,
809 p,
810 )
811 })
812 }
813
814 fn forward_position_closed(&self, event: &PositionClosed) -> anyhow::Result<()> {
815 invoke_event(self, "on_position_closed", event, |adapter, p| unsafe {
816 validated_slot!(StrategyVTable, adapter.vtable.as_ptr(), on_position_closed)(
817 adapter.handle,
818 p,
819 )
820 })
821 }
822
823 fn forward_market_exit(&self) -> anyhow::Result<()> {
824 invoke_lifecycle(self, "on_market_exit", |adapter| unsafe {
825 validated_slot!(StrategyVTable, adapter.vtable.as_ptr(), on_market_exit)(adapter.handle)
826 })
827 }
828}
829
830fn log_strategy_hook_error(method: &str, r: anyhow::Result<()>) {
831 if let Err(e) = r {
832 log::error!(target: "nautilus_plugin", "{method}: {e}");
833 }
834}
835
836fn guard_call<R>(plugin: &str, type_name: &str, method: &str, f: impl FnOnce() -> R) -> Option<R> {
837 match catch_unwind(AssertUnwindSafe(f)) {
838 Ok(r) => Some(r),
839 Err(_payload) => {
840 log::error!(
841 target: "nautilus_plugin",
842 "plug-in '{plugin}' ({type_name}) panicked in {method}",
843 );
844 None
845 }
846 }
847}
848
849fn invoke_lifecycle(
850 adapter: &PluginStrategyAdapter,
851 method: &str,
852 f: impl FnOnce(&PluginStrategyAdapter) -> PluginResult<()>,
853) -> anyhow::Result<()> {
854 let plugin_name = adapter.plugin_name.clone();
855 let type_name = adapter.type_name.clone();
856 let result = guard_call(&plugin_name, &type_name, method, || f(adapter));
857 finish(result, &plugin_name, &type_name, method)
858}
859
860fn invoke_event<T>(
861 adapter: &PluginStrategyAdapter,
862 method: &str,
863 payload: &T,
864 f: impl FnOnce(&PluginStrategyAdapter, *const T) -> PluginResult<()>,
865) -> anyhow::Result<()> {
866 let plugin_name = adapter.plugin_name.clone();
867 let type_name = adapter.type_name.clone();
868 let ptr: *const T = payload;
869 let result = guard_call(&plugin_name, &type_name, method, || f(adapter, ptr));
870 finish(result, &plugin_name, &type_name, method)
871}
872
873fn invoke_custom_data(
874 adapter: &PluginStrategyAdapter,
875 method: &str,
876 payload: PluginCustomDataRef,
877 f: impl FnOnce(&PluginStrategyAdapter, PluginCustomDataRef) -> PluginResult<()>,
878) -> anyhow::Result<()> {
879 let plugin_name = adapter.plugin_name.clone();
880 let type_name = adapter.type_name.clone();
881 let result = guard_call(&plugin_name, &type_name, method, || f(adapter, payload));
882 finish(result, &plugin_name, &type_name, method)
883}
884
885fn invoke_slice<T>(
886 adapter: &PluginStrategyAdapter,
887 method: &str,
888 payload: &[T],
889 f: impl FnOnce(&PluginStrategyAdapter, Slice<'_, T>) -> PluginResult<()>,
890) -> anyhow::Result<()> {
891 let plugin_name = adapter.plugin_name.clone();
892 let type_name = adapter.type_name.clone();
893 let slice = Slice::from_slice(payload);
894 let result = guard_call(&plugin_name, &type_name, method, || f(adapter, slice));
895 finish(result, &plugin_name, &type_name, method)
896}
897
898fn finish(
899 result: Option<PluginResult<()>>,
900 plugin_name: &str,
901 type_name: &str,
902 method: &str,
903) -> anyhow::Result<()> {
904 match result {
905 Some(r) => r.into_result().map_err(|e| {
906 anyhow::anyhow!(
907 "plug-in '{plugin_name}' ({type_name}) {method} returned error: {}",
908 e.message_string()
909 )
910 }),
911 None => anyhow::bail!("plug-in '{plugin_name}' ({type_name}) panicked in {method}"),
912 }
913}
914
915#[cfg(test)]
916mod tests {
917 use nautilus_model::identifiers::StrategyId;
918 use rstest::rstest;
919
920 use super::*;
921 use crate::{
922 bridge::{
923 host::host_vtable,
924 registry::{host_context_live_count, host_context_test_lock},
925 },
926 surfaces::strategy::{PluginStrategy, strategy_vtable},
927 };
928
929 struct DropTestStrategy;
930 unsafe impl Send for DropTestStrategy {}
932
933 impl PluginStrategy for DropTestStrategy {
934 const TYPE_NAME: &'static str = "DropTestStrategy";
935
936 fn new(_host: *const HostVTable, _ctx: *const HostContext, _config_json: &str) -> Self {
937 Self
938 }
939 }
940
941 fn test_strategy_config(strategy_id: &str) -> StrategyConfig {
942 StrategyConfig::builder()
943 .strategy_id(StrategyId::from(strategy_id))
944 .order_id_tag("001".to_string())
945 .build()
946 }
947
948 fn drop_test_strategy_vtable() -> ValidatedStrategyVTable {
949 unsafe {
952 ValidatedStrategyVTable::from_raw_unchecked(strategy_vtable::<DropTestStrategy>())
953 }
954 }
955
956 #[rstest]
957 fn new_rejects_config_without_strategy_id() {
958 let config = StrategyConfig::default();
963 assert!(
964 config.strategy_id.is_none(),
965 "fixture assumes default has no strategy_id"
966 );
967
968 let r = unsafe {
970 PluginStrategyAdapter::new(
971 config,
972 "plug-in",
973 DropTestStrategy::TYPE_NAME,
974 drop_test_strategy_vtable(),
975 host_vtable(),
976 "{}",
977 )
978 };
979 let err = r.unwrap_err();
980 assert!(
981 err.to_string().contains("strategy_id"),
982 "expected strategy_id error, was: {err}",
983 );
984 }
985
986 #[rstest]
987 fn drop_frees_host_context() {
988 let _guard = host_context_test_lock();
989 let before = host_context_live_count();
990 let config = test_strategy_config("PluginStrategyAdapter-Drop");
991 let adapter = unsafe {
993 PluginStrategyAdapter::new(
994 config,
995 "plug-in",
996 DropTestStrategy::TYPE_NAME,
997 drop_test_strategy_vtable(),
998 host_vtable(),
999 "{}",
1000 )
1001 }
1002 .expect("adapter construction");
1003 assert_eq!(host_context_live_count(), before + 1);
1004
1005 drop(adapter);
1006 assert_eq!(host_context_live_count(), before);
1007 }
1008}