nautilus_model/instruments/
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
16use chrono::{TimeZone, Utc};
17use nautilus_core::UnixNanos;
18use rstest::*;
19use rust_decimal::Decimal;
20use rust_decimal_macros::dec;
21use ustr::Ustr;
22
23use super::{
24    CryptoOption, betting::BettingInstrument, binary_option::BinaryOption,
25    futures_spread::FuturesSpread, option_spread::OptionSpread, synthetic::SyntheticInstrument,
26};
27use crate::{
28    enums::{AssetClass, OptionKind},
29    identifiers::{InstrumentId, Symbol, Venue},
30    instruments::{
31        CryptoFuture, CryptoPerpetual, CurrencyPair, Equity, FuturesContract, OptionContract,
32    },
33    types::{Currency, Money, Price, Quantity},
34};
35
36impl Default for SyntheticInstrument {
37    /// Creates a new default [`SyntheticInstrument`] instance for testing.
38    fn default() -> Self {
39        let btc_binance = InstrumentId::from("BTC.BINANCE");
40        let ltc_binance = InstrumentId::from("LTC.BINANCE");
41        let formula = "(BTC.BINANCE + LTC.BINANCE) / 2.0".to_string();
42        SyntheticInstrument::new(
43            Symbol::new("BTC-LTC"),
44            2,
45            vec![btc_binance, ltc_binance],
46            formula.clone(),
47            0.into(),
48            0.into(),
49        )
50    }
51}
52
53////////////////////////////////////////////////////////////////////////////////
54// CryptoFuture
55////////////////////////////////////////////////////////////////////////////////
56
57#[fixture]
58pub fn crypto_future_btcusdt(
59    #[default(2)] price_precision: u8,
60    #[default(6)] size_precision: u8,
61    #[default(Price::from("0.01"))] price_increment: Price,
62    #[default(Quantity::from("0.000001"))] size_increment: Quantity,
63) -> CryptoFuture {
64    let activation = Utc.with_ymd_and_hms(2014, 4, 8, 0, 0, 0).unwrap();
65    let expiration = Utc.with_ymd_and_hms(2014, 7, 8, 0, 0, 0).unwrap();
66    CryptoFuture::new(
67        InstrumentId::from("ETHUSDT-123.BINANCE"),
68        Symbol::from("BTCUSDT"),
69        Currency::from("BTC"),
70        Currency::from("USDT"),
71        Currency::from("USDT"),
72        false,
73        UnixNanos::from(activation.timestamp_nanos_opt().unwrap() as u64),
74        UnixNanos::from(expiration.timestamp_nanos_opt().unwrap() as u64),
75        price_precision,
76        size_precision,
77        price_increment,
78        size_increment,
79        None,
80        None,
81        Some(Quantity::from("9000.0")),
82        Some(Quantity::from("0.000001")),
83        None,
84        Some(Money::new(10.00, Currency::from("USDT"))),
85        Some(Price::from("1000000.00")),
86        Some(Price::from("0.01")),
87        None,
88        None,
89        None,
90        None,
91        0.into(),
92        0.into(),
93    )
94}
95
96////////////////////////////////////////////////////////////////////////////////
97// CryptoOption
98////////////////////////////////////////////////////////////////////////////////
99
100#[fixture]
101pub fn crypto_option_btc_deribit(
102    #[default(3)] price_precision: u8,
103    #[default(1)] size_precision: u8,
104    #[default(Price::from("0.001"))] price_increment: Price,
105    #[default(Quantity::from("0.1"))] size_increment: Quantity,
106) -> CryptoOption {
107    let activation = UnixNanos::from(1_671_696_002_000_000_000);
108    let expiration = UnixNanos::from(1_673_596_800_000_000_000);
109    CryptoOption::new(
110        InstrumentId::from("BTC-13JAN23-16000-P.DERIBIT"),
111        Symbol::from("BTC-13JAN23-16000-P"),
112        Currency::from("BTC"),
113        Currency::from("USD"),
114        Currency::from("BTC"),
115        false,
116        OptionKind::Put,
117        Price::from("16000.000"),
118        activation,
119        expiration,
120        price_precision,
121        size_precision,
122        price_increment,
123        size_increment,
124        Some(Quantity::from(1)),
125        Some(Quantity::from("9000.0")),
126        Some(Quantity::from("0.1")),
127        None,
128        Some(Money::new(10.00, Currency::from("USD"))),
129        None,
130        None,
131        None,
132        None,
133        Some(dec!(0.0003)),
134        Some(dec!(0.0003)),
135        0.into(),
136        0.into(),
137    )
138}
139
140////////////////////////////////////////////////////////////////////////////////
141// CryptoPerpetual
142////////////////////////////////////////////////////////////////////////////////
143
144#[fixture]
145pub fn crypto_perpetual_ethusdt() -> CryptoPerpetual {
146    CryptoPerpetual::new(
147        InstrumentId::from("ETHUSDT-PERP.BINANCE"),
148        Symbol::from("ETHUSDT"),
149        Currency::from("ETH"),
150        Currency::from("USDT"),
151        Currency::from("USDT"),
152        false,
153        2,
154        3,
155        Price::from("0.01"),
156        Quantity::from("0.001"),
157        None,
158        None,
159        Some(Quantity::from("10000.0")),
160        Some(Quantity::from("0.001")),
161        None,
162        Some(Money::new(10.00, Currency::from("USDT"))),
163        Some(Price::from("15000.00")),
164        Some(Price::from("1.0")),
165        Some(dec!(1.0)),
166        Some(dec!(0.35)),
167        Some(dec!(0.0002)),
168        Some(dec!(0.0004)),
169        UnixNanos::default(),
170        UnixNanos::default(),
171    )
172}
173
174#[fixture]
175pub fn xbtusd_bitmex() -> CryptoPerpetual {
176    CryptoPerpetual::new(
177        InstrumentId::from("BTCUSDT.BITMEX"),
178        Symbol::from("XBTUSD"),
179        Currency::BTC(),
180        Currency::USD(),
181        Currency::BTC(),
182        true,
183        1,
184        0,
185        Price::from("0.5"),
186        Quantity::from("1"),
187        None,
188        None,
189        None,
190        None,
191        Some(Money::from("10000000 USD")),
192        Some(Money::from("1 USD")),
193        Some(Price::from("10000000")),
194        Some(Price::from("0.01")),
195        Some(dec!(0.01)),
196        Some(dec!(0.0035)),
197        Some(dec!(-0.00025)),
198        Some(dec!(0.00075)),
199        UnixNanos::default(),
200        UnixNanos::default(),
201    )
202}
203
204#[fixture]
205pub fn ethusdt_bitmex() -> CryptoPerpetual {
206    CryptoPerpetual::new(
207        InstrumentId::from("ETHUSD.BITMEX"),
208        Symbol::from("ETHUSD"),
209        Currency::ETH(),
210        Currency::USD(),
211        Currency::ETH(),
212        true,
213        2,
214        0,
215        Price::from("0.05"),
216        Quantity::from("1"),
217        None,
218        None,
219        None,
220        None,
221        None,
222        None,
223        Some(Price::from("10000000")),
224        Some(Price::from("0.01")),
225        Some(dec!(0.01)),
226        Some(dec!(0.0035)),
227        Some(dec!(-0.00025)),
228        Some(dec!(0.00075)),
229        UnixNanos::default(),
230        UnixNanos::default(),
231    )
232}
233
234////////////////////////////////////////////////////////////////////////////////
235// CurrencyPair
236////////////////////////////////////////////////////////////////////////////////
237
238#[fixture]
239pub fn currency_pair_btcusdt() -> CurrencyPair {
240    CurrencyPair::new(
241        InstrumentId::from("BTCUSDT.BINANCE"),
242        Symbol::from("BTCUSDT"),
243        Currency::from("BTC"),
244        Currency::from("USDT"),
245        2,
246        6,
247        Price::from("0.01"),
248        Quantity::from("0.000001"),
249        None,
250        Some(Quantity::from("9000")),
251        Some(Quantity::from("0.000001")),
252        None,
253        None,
254        Some(Price::from("1000000")),
255        Some(Price::from("0.01")),
256        Some(dec!(0.001)),
257        Some(dec!(0.001)),
258        Some(dec!(0.001)),
259        Some(dec!(0.001)),
260        UnixNanos::default(),
261        UnixNanos::default(),
262    )
263}
264
265#[fixture]
266pub fn currency_pair_ethusdt() -> CurrencyPair {
267    CurrencyPair::new(
268        InstrumentId::from("ETHUSDT.BINANCE"),
269        Symbol::from("ETHUSDT"),
270        Currency::from("ETH"),
271        Currency::from("USDT"),
272        2,
273        5,
274        Price::from("0.01"),
275        Quantity::from("0.00001"),
276        None,
277        Some(Quantity::from("9000")),
278        Some(Quantity::from("0.00001")),
279        None,
280        None,
281        Some(Price::from("1000000")),
282        Some(Price::from("0.01")),
283        Some(dec!(0.01)),
284        Some(dec!(0.0035)),
285        Some(dec!(0.0001)),
286        Some(dec!(0.0001)),
287        UnixNanos::default(),
288        UnixNanos::default(),
289    )
290}
291
292#[must_use]
293pub fn default_fx_ccy(symbol: Symbol, venue: Option<Venue>) -> CurrencyPair {
294    let target_venue = venue.unwrap_or(Venue::from("SIM"));
295    let instrument_id = InstrumentId::new(symbol, target_venue);
296    let base_currency = symbol.as_str().split('/').next().unwrap();
297    let quote_currency = symbol.as_str().split('/').next_back().unwrap();
298    let price_precision = if quote_currency == "JPY" { 3 } else { 5 };
299    let price_increment = Price::new(1.0 / 10.0f64, price_precision);
300    CurrencyPair::new(
301        instrument_id,
302        symbol,
303        Currency::from(base_currency),
304        Currency::from(quote_currency),
305        price_precision,
306        0,
307        price_increment,
308        Quantity::from("1"),
309        Some(Quantity::from("1000")),
310        Some(Quantity::from("1000000")),
311        Some(Quantity::from("100")),
312        None,
313        None,
314        None,
315        None,
316        Some(dec!(0.03)),
317        Some(dec!(0.03)),
318        Some(dec!(0.00002)),
319        Some(dec!(0.00002)),
320        UnixNanos::default(),
321        UnixNanos::default(),
322    )
323}
324
325#[fixture]
326pub fn audusd_sim() -> CurrencyPair {
327    default_fx_ccy(Symbol::from("AUD/USD"), Some(Venue::from("SIM")))
328}
329
330#[fixture]
331pub fn usdjpy_idealpro() -> CurrencyPair {
332    default_fx_ccy(Symbol::from("USD/JPY"), Some(Venue::from("IDEALPRO")))
333}
334
335////////////////////////////////////////////////////////////////////////////////
336// Equity
337////////////////////////////////////////////////////////////////////////////////
338
339#[fixture]
340pub fn equity_aapl() -> Equity {
341    Equity::new(
342        InstrumentId::from("AAPL.XNAS"),
343        Symbol::from("AAPL"),
344        Some(Ustr::from("US0378331005")),
345        Currency::from("USD"),
346        2,
347        Price::from("0.01"),
348        None,
349        None,
350        None,
351        None,
352        None,
353        None,
354        None,
355        None,
356        None,
357        UnixNanos::default(),
358        UnixNanos::default(),
359    )
360}
361
362////////////////////////////////////////////////////////////////////////////////
363// FuturesContract
364////////////////////////////////////////////////////////////////////////////////
365
366pub fn futures_contract_es(
367    activation: Option<UnixNanos>,
368    expiration: Option<UnixNanos>,
369) -> FuturesContract {
370    let activation = activation.unwrap_or(UnixNanos::from(
371        Utc.with_ymd_and_hms(2021, 9, 10, 0, 0, 0)
372            .unwrap()
373            .timestamp_nanos_opt()
374            .unwrap() as u64,
375    ));
376    let expiration = expiration.unwrap_or(UnixNanos::from(
377        Utc.with_ymd_and_hms(2021, 12, 17, 0, 0, 0)
378            .unwrap()
379            .timestamp_nanos_opt()
380            .unwrap() as u64,
381    ));
382    FuturesContract::new(
383        InstrumentId::from("ESZ21.GLBX"),
384        Symbol::from("ESZ21"),
385        AssetClass::Index,
386        Some(Ustr::from("XCME")),
387        Ustr::from("ES"),
388        activation,
389        expiration,
390        Currency::USD(),
391        2,
392        Price::from("0.01"),
393        Quantity::from(1),
394        Quantity::from(1),
395        None,
396        None,
397        None,
398        None,
399        None,
400        None,
401        None,
402        None,
403        UnixNanos::default(),
404        UnixNanos::default(),
405    )
406}
407
408////////////////////////////////////////////////////////////////////////////////
409// FuturesSpread
410////////////////////////////////////////////////////////////////////////////////
411
412#[fixture]
413pub fn futures_spread_es() -> FuturesSpread {
414    let activation = Utc.with_ymd_and_hms(2022, 6, 21, 13, 30, 0).unwrap();
415    let expiration = Utc.with_ymd_and_hms(2024, 6, 21, 13, 30, 0).unwrap();
416    FuturesSpread::new(
417        InstrumentId::from("ESM4-ESU4.GLBX"),
418        Symbol::from("ESM4-ESU4"),
419        AssetClass::Index,
420        Some(Ustr::from("XCME")),
421        Ustr::from("ES"),
422        Ustr::from("EQ"),
423        UnixNanos::from(activation.timestamp_nanos_opt().unwrap() as u64),
424        UnixNanos::from(expiration.timestamp_nanos_opt().unwrap() as u64),
425        Currency::USD(),
426        2,
427        Price::from("0.01"),
428        Quantity::from(1),
429        Quantity::from(1),
430        None,
431        None,
432        None,
433        None,
434        None,
435        None,
436        None,
437        None,
438        UnixNanos::default(),
439        UnixNanos::default(),
440    )
441}
442
443////////////////////////////////////////////////////////////////////////////////
444// OptionContract
445////////////////////////////////////////////////////////////////////////////////
446
447#[fixture]
448pub fn option_contract_appl() -> OptionContract {
449    let activation = Utc.with_ymd_and_hms(2021, 9, 17, 0, 0, 0).unwrap();
450    let expiration = Utc.with_ymd_and_hms(2021, 12, 17, 0, 0, 0).unwrap();
451    OptionContract::new(
452        InstrumentId::from("AAPL211217C00150000.OPRA"),
453        Symbol::from("AAPL211217C00150000"),
454        AssetClass::Equity,
455        Some(Ustr::from("GMNI")), // Nasdaq GEMX
456        Ustr::from("AAPL"),
457        OptionKind::Call,
458        Price::from("149.0"),
459        Currency::USD(),
460        UnixNanos::from(activation.timestamp_nanos_opt().unwrap() as u64),
461        UnixNanos::from(expiration.timestamp_nanos_opt().unwrap() as u64),
462        2,
463        Price::from("0.01"),
464        Quantity::from(1),
465        Quantity::from(1),
466        None,
467        None,
468        None,
469        None,
470        None,
471        None,
472        None,
473        None,
474        UnixNanos::default(),
475        UnixNanos::default(),
476    )
477}
478
479////////////////////////////////////////////////////////////////////////////////
480// OptionSpread
481////////////////////////////////////////////////////////////////////////////////
482
483#[fixture]
484pub fn option_spread() -> OptionSpread {
485    let activation = Utc.with_ymd_and_hms(2023, 11, 6, 20, 54, 7).unwrap();
486    let expiration = Utc.with_ymd_and_hms(2024, 2, 23, 22, 59, 0).unwrap();
487    OptionSpread::new(
488        InstrumentId::from("UD:U$: GN 2534559.GLBX"),
489        Symbol::from("UD:U$: GN 2534559"),
490        AssetClass::FX,
491        Some(Ustr::from("XCME")),
492        Ustr::from("SR3"), // British Pound futures (option on futures)
493        Ustr::from("GN"),
494        UnixNanos::from(activation.timestamp_nanos_opt().unwrap() as u64),
495        UnixNanos::from(expiration.timestamp_nanos_opt().unwrap() as u64),
496        Currency::USD(),
497        2,
498        Price::from("0.01"),
499        Quantity::from(1),
500        Quantity::from(1),
501        None,
502        None,
503        None,
504        None,
505        None,
506        None,
507        None,
508        None,
509        UnixNanos::default(),
510        UnixNanos::default(),
511    )
512}
513
514////////////////////////////////////////////////////////////////////////////////
515// BettingInstrument
516////////////////////////////////////////////////////////////////////////////////
517
518#[fixture]
519pub fn betting() -> BettingInstrument {
520    let raw_symbol = Symbol::new("1-123456789");
521    let id = InstrumentId::from(format!("{raw_symbol}.BETFAIR").as_str());
522    let event_type_id = 6423;
523    let event_type_name = Ustr::from("American Football");
524    let competition_id = 12282733;
525    let competition_name = Ustr::from("NFL");
526    let event_id = 29678534;
527    let event_name = Ustr::from("NFL");
528    let event_country_code = Ustr::from("GB");
529    let event_open_date = UnixNanos::from(
530        Utc.with_ymd_and_hms(2022, 2, 7, 23, 30, 0)
531            .unwrap()
532            .timestamp_nanos_opt()
533            .unwrap() as u64,
534    );
535    let betting_type = Ustr::from("ODDS");
536    let market_id = Ustr::from("1-123456789");
537    let market_name = Ustr::from("AFC Conference Winner");
538    let market_type = Ustr::from("SPECIAL");
539    let market_start_time = UnixNanos::from(
540        Utc.with_ymd_and_hms(2022, 2, 7, 23, 30, 0)
541            .unwrap()
542            .timestamp_nanos_opt()
543            .unwrap() as u64,
544    );
545    let selection_id = 50214;
546    let selection_name = Ustr::from("Kansas City Chiefs");
547    let selection_handicap = 0.0; // As per betting convention, no handicap
548    let currency = Currency::GBP();
549    let price_increment = Price::from("0.01");
550    let size_increment = Quantity::from("0.01");
551    let max_quantity = Some(Quantity::from("1000"));
552    let min_quantity = Some(Quantity::from("1"));
553    let max_notional = Some(Money::from("10000 GBP"));
554    let min_notional = Some(Money::from("10 GBP"));
555    let max_price = Some(Price::from("100.00"));
556    let min_price = Some(Price::from("1.00"));
557    let margin_init = Some(Decimal::from(1));
558    let margin_maint = Some(Decimal::from(1));
559    let maker_fee = Some(Decimal::from(0));
560    let taker_fee = Some(Decimal::from(0));
561    let ts_event = UnixNanos::default(); // For testing purposes
562    let ts_init = UnixNanos::default(); // For testing purposes
563
564    BettingInstrument::new(
565        id,
566        raw_symbol,
567        event_type_id,
568        event_type_name,
569        competition_id,
570        competition_name,
571        event_id,
572        event_name,
573        event_country_code,
574        event_open_date,
575        betting_type,
576        market_id,
577        market_name,
578        market_type,
579        market_start_time,
580        selection_id,
581        selection_name,
582        selection_handicap,
583        currency,
584        price_increment.precision,
585        size_increment.precision,
586        price_increment,
587        size_increment,
588        max_quantity,
589        min_quantity,
590        max_notional,
591        min_notional,
592        max_price,
593        min_price,
594        margin_init,
595        margin_maint,
596        maker_fee,
597        taker_fee,
598        ts_event,
599        ts_init,
600    )
601}
602
603////////////////////////////////////////////////////////////////////////////////
604// BinaryOption
605////////////////////////////////////////////////////////////////////////////////
606
607#[fixture]
608pub fn binary_option() -> BinaryOption {
609    let raw_symbol = Symbol::new(
610        "0x12a0cb60174abc437bf1178367c72d11f069e1a3add20b148fb0ab4279b772b2-92544998123698303655208967887569360731013655782348975589292031774495159624905",
611    );
612    let activation = Utc.with_ymd_and_hms(2023, 11, 6, 20, 54, 7).unwrap();
613    let expiration = Utc.with_ymd_and_hms(2024, 2, 23, 22, 59, 0).unwrap();
614    let price_increment = Price::from("0.001");
615    let size_increment = Quantity::from("0.01");
616    BinaryOption::new(
617        InstrumentId::from("{raw_symbol}.POLYMARKET"),
618        raw_symbol,
619        AssetClass::Alternative,
620        Currency::USDC(),
621        UnixNanos::from(activation.timestamp_nanos_opt().unwrap() as u64),
622        UnixNanos::from(expiration.timestamp_nanos_opt().unwrap() as u64),
623        price_increment.precision,
624        size_increment.precision,
625        price_increment,
626        size_increment,
627        None,
628        None,
629        None,
630        None,
631        None,
632        None,
633        None,
634        None,
635        None,
636        None,
637        None,
638        None,
639        UnixNanos::default(),
640        UnixNanos::default(),
641    )
642}