nautilus_model/
stubs.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 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//! Type stubs to facilitate testing.
17
18use rstest::fixture;
19use rust_decimal::prelude::ToPrimitive;
20
21use crate::{
22    data::order::BookOrder,
23    enums::{BookType, LiquiditySide, OrderSide, OrderType},
24    identifiers::InstrumentId,
25    instruments::{CurrencyPair, Instrument, InstrumentAny, stubs::audusd_sim},
26    orderbook::OrderBook,
27    orders::{builder::OrderTestBuilder, stubs::TestOrderEventStubs},
28    position::Position,
29    types::{Money, Price, Quantity},
30};
31
32/// Calculate commission for testing.
33///
34/// # Panics
35///
36/// This function panics if:
37/// - The liquidity side is `NoLiquiditySide`.
38/// - `instrument.maker_fee()` or `instrument.taker_fee()` cannot be converted to `f64`.
39pub fn calculate_commission(
40    instrument: &InstrumentAny,
41    last_qty: Quantity,
42    last_px: Price,
43    use_quote_for_inverse: Option<bool>,
44) -> Money {
45    let liquidity_side = LiquiditySide::Taker;
46    assert_ne!(
47        liquidity_side,
48        LiquiditySide::NoLiquiditySide,
49        "Invalid liquidity side"
50    );
51    let notional = instrument
52        .calculate_notional_value(last_qty, last_px, use_quote_for_inverse)
53        .as_f64();
54    let commission = if liquidity_side == LiquiditySide::Maker {
55        notional * instrument.maker_fee().to_f64().unwrap()
56    } else if liquidity_side == LiquiditySide::Taker {
57        notional * instrument.taker_fee().to_f64().unwrap()
58    } else {
59        panic!("Invalid liquidity side {liquidity_side}")
60    };
61    if instrument.is_inverse() && !use_quote_for_inverse.unwrap_or(false) {
62        Money::new(commission, instrument.base_currency().unwrap())
63    } else {
64        Money::new(commission, instrument.quote_currency())
65    }
66}
67
68#[fixture]
69pub fn stub_position_long(audusd_sim: CurrencyPair) -> Position {
70    let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
71    let order = OrderTestBuilder::new(OrderType::Market)
72        .instrument_id(audusd_sim.id())
73        .side(OrderSide::Buy)
74        .quantity(Quantity::from(1))
75        .build();
76    let filled = TestOrderEventStubs::filled(
77        &order,
78        &audusd_sim,
79        None,
80        None,
81        Some(Price::from("1.0002")),
82        None,
83        None,
84        None,
85        None,
86        None,
87    );
88    Position::new(&audusd_sim, filled.into())
89}
90
91#[fixture]
92pub fn stub_position_short(audusd_sim: CurrencyPair) -> Position {
93    let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
94    let order = OrderTestBuilder::new(OrderType::Market)
95        .instrument_id(audusd_sim.id())
96        .side(OrderSide::Sell)
97        .quantity(Quantity::from(1))
98        .build();
99    let filled = TestOrderEventStubs::filled(
100        &order,
101        &audusd_sim,
102        None,
103        None,
104        Some(Price::from("22000.0")),
105        None,
106        None,
107        None,
108        None,
109        None,
110    );
111    Position::new(&audusd_sim, filled.into())
112}
113
114#[must_use]
115pub fn stub_order_book_mbp_appl_xnas() -> OrderBook {
116    stub_order_book_mbp(
117        InstrumentId::from("AAPL.XNAS"),
118        101.0,
119        100.0,
120        100.0,
121        100.0,
122        2,
123        0.01,
124        0,
125        100.0,
126        10,
127    )
128}
129
130#[allow(clippy::too_many_arguments)]
131#[must_use]
132pub fn stub_order_book_mbp(
133    instrument_id: InstrumentId,
134    top_ask_price: f64,
135    top_bid_price: f64,
136    top_ask_size: f64,
137    top_bid_size: f64,
138    price_precision: u8,
139    price_increment: f64,
140    size_precision: u8,
141    size_increment: f64,
142    num_levels: usize,
143) -> OrderBook {
144    let mut book = OrderBook::new(instrument_id, BookType::L2_MBP);
145
146    // Generate bids
147    for i in 0..num_levels {
148        let price = Price::new(
149            price_increment.mul_add(-(i as f64), top_bid_price),
150            price_precision,
151        );
152        let size = Quantity::new(
153            size_increment.mul_add(i as f64, top_bid_size),
154            size_precision,
155        );
156        let order = BookOrder::new(
157            OrderSide::Buy,
158            price,
159            size,
160            0, // order_id not applicable for MBP (market by price) books
161        );
162        book.add(order, 0, 1, 2.into());
163    }
164
165    // Generate asks
166    for i in 0..num_levels {
167        let price = Price::new(
168            price_increment.mul_add(i as f64, top_ask_price),
169            price_precision,
170        );
171        let size = Quantity::new(
172            size_increment.mul_add(i as f64, top_ask_size),
173            size_precision,
174        );
175        let order = BookOrder::new(
176            OrderSide::Sell,
177            price,
178            size,
179            0, // order_id not applicable for MBP (market by price) books
180        );
181        book.add(order, 0, 1, 2.into());
182    }
183
184    book
185}