nautilus_model/
stubs.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//! 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/// A trait for providing test-only default values.
33///
34/// This trait is intentionally separate from [`Default`] to make it clear
35/// that these default values are only meaningful in testing contexts and should
36/// not be used in production code.
37pub trait TestDefault {
38    /// Creates a new instance with test-appropriate default values.
39    fn test_default() -> Self;
40}
41
42/// Calculate commission for testing.
43///
44/// # Panics
45///
46/// This function panics if:
47/// - The liquidity side is `NoLiquiditySide`.
48/// - `instrument.maker_fee()` or `instrument.taker_fee()` cannot be converted to `f64`.
49pub fn calculate_commission(
50    instrument: &InstrumentAny,
51    last_qty: Quantity,
52    last_px: Price,
53    use_quote_for_inverse: Option<bool>,
54) -> Money {
55    let liquidity_side = LiquiditySide::Taker;
56    assert_ne!(
57        liquidity_side,
58        LiquiditySide::NoLiquiditySide,
59        "Invalid liquidity side"
60    );
61    let notional = instrument
62        .calculate_notional_value(last_qty, last_px, use_quote_for_inverse)
63        .as_f64();
64    let commission = if liquidity_side == LiquiditySide::Maker {
65        notional * instrument.maker_fee().to_f64().unwrap()
66    } else if liquidity_side == LiquiditySide::Taker {
67        notional * instrument.taker_fee().to_f64().unwrap()
68    } else {
69        panic!("Invalid liquidity side {liquidity_side}")
70    };
71    if instrument.is_inverse() && !use_quote_for_inverse.unwrap_or(false) {
72        Money::new(commission, instrument.base_currency().unwrap())
73    } else {
74        Money::new(commission, instrument.quote_currency())
75    }
76}
77
78#[fixture]
79pub fn stub_position_long(audusd_sim: CurrencyPair) -> Position {
80    let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
81    let order = OrderTestBuilder::new(OrderType::Market)
82        .instrument_id(audusd_sim.id())
83        .side(OrderSide::Buy)
84        .quantity(Quantity::from(1))
85        .build();
86    let filled = TestOrderEventStubs::filled(
87        &order,
88        &audusd_sim,
89        None,
90        None,
91        Some(Price::from("1.0002")),
92        None,
93        None,
94        None,
95        None,
96        None,
97    );
98    Position::new(&audusd_sim, filled.into())
99}
100
101#[fixture]
102pub fn stub_position_short(audusd_sim: CurrencyPair) -> Position {
103    let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
104    let order = OrderTestBuilder::new(OrderType::Market)
105        .instrument_id(audusd_sim.id())
106        .side(OrderSide::Sell)
107        .quantity(Quantity::from(1))
108        .build();
109    let filled = TestOrderEventStubs::filled(
110        &order,
111        &audusd_sim,
112        None,
113        None,
114        Some(Price::from("22000.0")),
115        None,
116        None,
117        None,
118        None,
119        None,
120    );
121    Position::new(&audusd_sim, filled.into())
122}
123
124#[must_use]
125pub fn stub_order_book_mbp_appl_xnas() -> OrderBook {
126    stub_order_book_mbp(
127        InstrumentId::from("AAPL.XNAS"),
128        101.0,
129        100.0,
130        100.0,
131        100.0,
132        2,
133        0.01,
134        0,
135        100.0,
136        10,
137    )
138}
139
140#[allow(clippy::too_many_arguments)]
141#[must_use]
142pub fn stub_order_book_mbp(
143    instrument_id: InstrumentId,
144    top_ask_price: f64,
145    top_bid_price: f64,
146    top_ask_size: f64,
147    top_bid_size: f64,
148    price_precision: u8,
149    price_increment: f64,
150    size_precision: u8,
151    size_increment: f64,
152    num_levels: usize,
153) -> OrderBook {
154    let mut book = OrderBook::new(instrument_id, BookType::L2_MBP);
155
156    // Generate bids
157    for i in 0..num_levels {
158        let price = Price::new(
159            price_increment.mul_add(-(i as f64), top_bid_price),
160            price_precision,
161        );
162        let size = Quantity::new(
163            size_increment.mul_add(i as f64, top_bid_size),
164            size_precision,
165        );
166        let order = BookOrder::new(
167            OrderSide::Buy,
168            price,
169            size,
170            0, // order_id not applicable for MBP (market by price) books
171        );
172        book.add(order, 0, 1, 2.into());
173    }
174
175    // Generate asks
176    for i in 0..num_levels {
177        let price = Price::new(
178            price_increment.mul_add(i as f64, top_ask_price),
179            price_precision,
180        );
181        let size = Quantity::new(
182            size_increment.mul_add(i as f64, top_ask_size),
183            size_precision,
184        );
185        let order = BookOrder::new(
186            OrderSide::Sell,
187            price,
188            size,
189            0, // order_id not applicable for MBP (market by price) books
190        );
191        book.add(order, 0, 1, 2.into());
192    }
193
194    book
195}