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