#![allow(
clippy::multiple_unsafe_ops_per_block,
reason = "each host-command block derefs the host vtable and performs the FFI call as one boundary crossing"
)]
use nautilus_core::{Params, UUID4, UnixNanos};
use nautilus_model::{
data::QuoteTick,
enums::{OrderSide, PositionSide, TimeInForce},
identifiers::{
AccountId, ClientId, ClientOrderId, InstrumentId, PositionId, StrategyId, TraderId,
},
orderbook::OrderBook,
orders::{MarketOrder, OrderAny},
types::Quantity,
};
use nautilus_plugin::{
prelude::*,
surfaces::commands::{
CancelAllOrdersCommand, CancelAllOrdersHandle, CancelOrderCommand, CancelOrderHandle,
CancelOrdersCommand, CancelOrdersHandle, CloseAllPositionsCommand, CloseAllPositionsHandle,
ClosePositionCommand, ClosePositionHandle, ModifyOrderCommand, ModifyOrderHandle,
QueryAccountCommand, QueryAccountHandle, QueryOrderCommand, QueryOrderHandle,
SubmitOrderCommand, SubmitOrderHandle, SubmitOrderListCommand, SubmitOrderListHandle,
},
};
#[derive(Clone, Copy)]
enum ExecAction {
SubmitOrder,
SubmitOrderList,
CancelOrder,
CancelOrders,
CancelAllOrders,
ModifyOrder,
ClosePosition,
CloseAllPositions,
QueryAccount,
QueryOrder,
}
pub struct ExecTestStrategy {
host: *const HostVTable,
ctx: *const HostContext,
action: ExecAction,
strategy_id: StrategyId,
client_order_id: ClientOrderId,
secondary_client_order_id: ClientOrderId,
client_id: Option<ClientId>,
account_id: AccountId,
position_id: PositionId,
expected_instrument_id: InstrumentId,
callback_path: Option<std::path::PathBuf>,
}
unsafe impl Send for ExecTestStrategy {}
impl PluginStrategy for ExecTestStrategy {
const TYPE_NAME: &'static str = "ExecTestStrategy";
fn new(host: *const HostVTable, ctx: *const HostContext, config_json: &str) -> Self {
let config = serde_json::from_str::<serde_json::Value>(config_json)
.unwrap_or_else(|_| serde_json::Value::Object(Default::default()));
let strategy_id = config
.get("strategy_id")
.and_then(serde_json::Value::as_str)
.unwrap_or("PluginExecCdylib-001");
let client_order_id = config
.get("client_order_id")
.and_then(serde_json::Value::as_str)
.unwrap_or("O-CDYLIB-001");
let secondary_client_order_id = config
.get("secondary_client_order_id")
.and_then(serde_json::Value::as_str)
.unwrap_or("O-CDYLIB-002");
let client_id = config
.get("client_id")
.and_then(serde_json::Value::as_str)
.map(ClientId::from);
let account_id = config
.get("account_id")
.and_then(serde_json::Value::as_str)
.unwrap_or("BINANCE-001");
let position_id = config
.get("position_id")
.and_then(serde_json::Value::as_str)
.unwrap_or("P-CDYLIB-001");
let expected_instrument_id = config
.get("instrument_id")
.and_then(serde_json::Value::as_str)
.unwrap_or("ETH-USDT.BINANCE");
let callback_path = config
.get("callback_path")
.and_then(serde_json::Value::as_str)
.map(std::path::PathBuf::from);
let action = match config.get("action").and_then(serde_json::Value::as_str) {
Some("submit_order_list") => ExecAction::SubmitOrderList,
Some("cancel_order") => ExecAction::CancelOrder,
Some("cancel_orders") => ExecAction::CancelOrders,
Some("cancel_all_orders") => ExecAction::CancelAllOrders,
Some("modify_order") => ExecAction::ModifyOrder,
Some("close_position") => ExecAction::ClosePosition,
Some("close_all_positions") => ExecAction::CloseAllPositions,
Some("query_account") => ExecAction::QueryAccount,
Some("query_order") => ExecAction::QueryOrder,
_ => ExecAction::SubmitOrder,
};
Self {
host,
ctx,
action,
strategy_id: StrategyId::from(strategy_id),
client_order_id: ClientOrderId::from(client_order_id),
secondary_client_order_id: ClientOrderId::from(secondary_client_order_id),
client_id,
account_id: AccountId::from(account_id),
position_id: PositionId::from(position_id),
expected_instrument_id: InstrumentId::from(expected_instrument_id),
callback_path,
}
}
fn on_start(&mut self) -> anyhow::Result<()> {
match self.action {
ExecAction::SubmitOrder => self.submit_order(),
ExecAction::SubmitOrderList => self.submit_order_list(),
ExecAction::CancelOrder => self.cancel_order(),
ExecAction::CancelOrders => self.cancel_orders(),
ExecAction::CancelAllOrders => self.cancel_all_orders(),
ExecAction::ModifyOrder => self.modify_order(),
ExecAction::ClosePosition => self.close_position(),
ExecAction::CloseAllPositions => self.close_all_positions(),
ExecAction::QueryAccount => self.query_account(),
ExecAction::QueryOrder => self.query_order(),
}
}
fn on_quote(&mut self, quote: &QuoteTick) -> anyhow::Result<()> {
if quote.instrument_id != self.expected_instrument_id {
anyhow::bail!(
"instrument id mismatch: expected {}, received {}",
self.expected_instrument_id,
quote.instrument_id
);
}
if let Some(path) = &self.callback_path {
std::fs::write(path, quote.instrument_id.to_string())?;
}
Ok(())
}
fn on_book(&mut self, book: &OrderBook) -> anyhow::Result<()> {
if book.instrument_id != self.expected_instrument_id {
anyhow::bail!(
"instrument id mismatch: expected {}, received {}",
self.expected_instrument_id,
book.instrument_id
);
}
if let Some(path) = &self.callback_path {
std::fs::write(path, book.instrument_id.to_string())?;
}
Ok(())
}
}
impl ExecTestStrategy {
fn submit_order(&mut self) -> anyhow::Result<()> {
let order = self.market_order(self.client_order_id, OrderSide::Buy);
let handle = SubmitOrderHandle::new(SubmitOrderCommand::new(
order,
Some(self.position_id),
None,
None,
));
let result = unsafe { ((*self.host).submit_order)(self.ctx, &raw const handle) };
result
.into_result()
.map_err(|e| anyhow::anyhow!(e.message_string()))
}
fn submit_order_list(&mut self) -> anyhow::Result<()> {
let handle = SubmitOrderListHandle::new(SubmitOrderListCommand::new(
vec![
self.market_order(self.client_order_id, OrderSide::Buy),
self.market_order(self.secondary_client_order_id, OrderSide::Sell),
],
Some(self.position_id),
self.client_id,
Some(Self::params("cdylib-submit-order-list")),
));
let result = unsafe { ((*self.host).submit_order_list)(self.ctx, &raw const handle) };
result
.into_result()
.map_err(|e| anyhow::anyhow!(e.message_string()))
}
fn cancel_order(&mut self) -> anyhow::Result<()> {
let handle =
CancelOrderHandle::new(CancelOrderCommand::new(self.client_order_id, None, None));
let result = unsafe { ((*self.host).cancel_order)(self.ctx, &raw const handle) };
result
.into_result()
.map_err(|e| anyhow::anyhow!(e.message_string()))
}
fn cancel_orders(&mut self) -> anyhow::Result<()> {
let handle = CancelOrdersHandle::new(CancelOrdersCommand::new(
vec![self.client_order_id, self.secondary_client_order_id],
self.client_id,
Some(Self::params("cdylib-cancel-orders")),
));
let result = unsafe { ((*self.host).cancel_orders)(self.ctx, &raw const handle) };
result
.into_result()
.map_err(|e| anyhow::anyhow!(e.message_string()))
}
fn cancel_all_orders(&mut self) -> anyhow::Result<()> {
let handle = CancelAllOrdersHandle::new(CancelAllOrdersCommand::new(
self.expected_instrument_id,
Some(OrderSide::Buy),
self.client_id,
Some(Self::params("cdylib-cancel-all-orders")),
));
let result = unsafe { ((*self.host).cancel_all_orders)(self.ctx, &raw const handle) };
result
.into_result()
.map_err(|e| anyhow::anyhow!(e.message_string()))
}
fn modify_order(&mut self) -> anyhow::Result<()> {
let handle = ModifyOrderHandle::new(ModifyOrderCommand::new(
self.client_order_id,
Some(Quantity::from("2.0")),
None,
None,
None,
None,
));
let result = unsafe { ((*self.host).modify_order)(self.ctx, &raw const handle) };
result
.into_result()
.map_err(|e| anyhow::anyhow!(e.message_string()))
}
fn close_position(&mut self) -> anyhow::Result<()> {
let handle = ClosePositionHandle::new(ClosePositionCommand::new(
self.position_id,
None,
Some(vec![ustr::Ustr::from("cdylib-close")]),
Some(TimeInForce::Ioc),
Some(true),
None,
));
let result = unsafe { ((*self.host).close_position)(self.ctx, &raw const handle) };
result
.into_result()
.map_err(|e| anyhow::anyhow!(e.message_string()))
}
fn close_all_positions(&mut self) -> anyhow::Result<()> {
let handle = CloseAllPositionsHandle::new(CloseAllPositionsCommand::new(
self.expected_instrument_id,
Some(PositionSide::Long),
self.client_id,
Some(vec![ustr::Ustr::from("cdylib-flatten")]),
Some(TimeInForce::Ioc),
Some(true),
Some(false),
));
let result = unsafe { ((*self.host).close_all_positions)(self.ctx, &raw const handle) };
result
.into_result()
.map_err(|e| anyhow::anyhow!(e.message_string()))
}
fn query_account(&mut self) -> anyhow::Result<()> {
let handle = QueryAccountHandle::new(QueryAccountCommand::new(
self.account_id,
self.client_id,
Some(Self::params("cdylib-query-account")),
));
let result = unsafe { ((*self.host).query_account)(self.ctx, &raw const handle) };
result
.into_result()
.map_err(|e| anyhow::anyhow!(e.message_string()))
}
fn query_order(&mut self) -> anyhow::Result<()> {
let handle =
QueryOrderHandle::new(QueryOrderCommand::new(self.client_order_id, None, None));
let result = unsafe { ((*self.host).query_order)(self.ctx, &raw const handle) };
result
.into_result()
.map_err(|e| anyhow::anyhow!(e.message_string()))
}
fn market_order(&self, client_order_id: ClientOrderId, order_side: OrderSide) -> OrderAny {
OrderAny::Market(MarketOrder::new(
TraderId::from("TRADER-001"),
self.strategy_id,
self.expected_instrument_id,
client_order_id,
order_side,
Quantity::from("1.0"),
TimeInForce::Gtc,
UUID4::new(),
UnixNanos::default(),
false,
false,
None,
None,
None,
None,
None,
None,
None,
None,
))
}
fn params(marker: &str) -> Params {
let mut params = Params::new();
params.insert(
"marker".to_string(),
serde_json::Value::String(marker.to_string()),
);
params
}
}
nautilus_plugin::nautilus_plugin! {
name: "exec-test-plugin",
vendor: "Nautech",
version: env!("CARGO_PKG_VERSION"),
strategies: [ExecTestStrategy],
}
#[allow(dead_code)]
fn main() {}