nautilus_model/instruments/
stubs.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 Posei Systems Pty Ltd. All rights reserved.
3//  https://poseitrader.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/// # Panics
293///
294/// Panics if `symbol` does not contain a '/' delimiter.
295#[must_use]
296pub fn default_fx_ccy(symbol: Symbol, venue: Option<Venue>) -> CurrencyPair {
297    let target_venue = venue.unwrap_or(Venue::from("SIM"));
298    let instrument_id = InstrumentId::new(symbol, target_venue);
299    let base_currency = symbol.as_str().split('/').next().unwrap();
300    let quote_currency = symbol.as_str().split('/').next_back().unwrap();
301    let price_precision = if quote_currency == "JPY" { 3 } else { 5 };
302    let price_increment = Price::new(1.0 / 10.0f64, price_precision);
303    CurrencyPair::new(
304        instrument_id,
305        symbol,
306        Currency::from(base_currency),
307        Currency::from(quote_currency),
308        price_precision,
309        0,
310        price_increment,
311        Quantity::from("1"),
312        Some(Quantity::from("1000")),
313        Some(Quantity::from("1000000")),
314        Some(Quantity::from("100")),
315        None,
316        None,
317        None,
318        None,
319        Some(dec!(0.03)),
320        Some(dec!(0.03)),
321        Some(dec!(0.00002)),
322        Some(dec!(0.00002)),
323        UnixNanos::default(),
324        UnixNanos::default(),
325    )
326}
327
328#[fixture]
329pub fn audusd_sim() -> CurrencyPair {
330    default_fx_ccy(Symbol::from("AUD/USD"), Some(Venue::from("SIM")))
331}
332
333#[fixture]
334pub fn gbpusd_sim() -> CurrencyPair {
335    default_fx_ccy(Symbol::from("GBP/USD"), Some(Venue::from("SIM")))
336}
337
338#[fixture]
339pub fn usdjpy_idealpro() -> CurrencyPair {
340    default_fx_ccy(Symbol::from("USD/JPY"), Some(Venue::from("IDEALPRO")))
341}
342
343////////////////////////////////////////////////////////////////////////////////
344// Equity
345////////////////////////////////////////////////////////////////////////////////
346
347#[fixture]
348pub fn equity_aapl() -> Equity {
349    Equity::new(
350        InstrumentId::from("AAPL.XNAS"),
351        Symbol::from("AAPL"),
352        Some(Ustr::from("US0378331005")),
353        Currency::from("USD"),
354        2,
355        Price::from("0.01"),
356        None,
357        None,
358        None,
359        None,
360        None,
361        None,
362        None,
363        None,
364        None,
365        UnixNanos::default(),
366        UnixNanos::default(),
367    )
368}
369
370////////////////////////////////////////////////////////////////////////////////
371// FuturesContract
372////////////////////////////////////////////////////////////////////////////////
373
374/// # Panics
375///
376/// Panics if constructing the activation or expiration timestamp fails,
377/// e.g., if the provided dates are invalid or timestamp conversion returns `None`.
378pub fn futures_contract_es(
379    activation: Option<UnixNanos>,
380    expiration: Option<UnixNanos>,
381) -> FuturesContract {
382    let activation = activation.unwrap_or(UnixNanos::from(
383        Utc.with_ymd_and_hms(2021, 9, 10, 0, 0, 0)
384            .unwrap()
385            .timestamp_nanos_opt()
386            .unwrap() as u64,
387    ));
388    let expiration = expiration.unwrap_or(UnixNanos::from(
389        Utc.with_ymd_and_hms(2021, 12, 17, 0, 0, 0)
390            .unwrap()
391            .timestamp_nanos_opt()
392            .unwrap() as u64,
393    ));
394    FuturesContract::new(
395        InstrumentId::from("ESZ21.GLBX"),
396        Symbol::from("ESZ21"),
397        AssetClass::Index,
398        Some(Ustr::from("XCME")),
399        Ustr::from("ES"),
400        activation,
401        expiration,
402        Currency::USD(),
403        2,
404        Price::from("0.01"),
405        Quantity::from(1),
406        Quantity::from(1),
407        None,
408        None,
409        None,
410        None,
411        None,
412        None,
413        None,
414        None,
415        UnixNanos::default(),
416        UnixNanos::default(),
417    )
418}
419
420////////////////////////////////////////////////////////////////////////////////
421// FuturesSpread
422////////////////////////////////////////////////////////////////////////////////
423
424#[fixture]
425pub fn futures_spread_es() -> FuturesSpread {
426    let activation = Utc.with_ymd_and_hms(2022, 6, 21, 13, 30, 0).unwrap();
427    let expiration = Utc.with_ymd_and_hms(2024, 6, 21, 13, 30, 0).unwrap();
428    FuturesSpread::new(
429        InstrumentId::from("ESM4-ESU4.GLBX"),
430        Symbol::from("ESM4-ESU4"),
431        AssetClass::Index,
432        Some(Ustr::from("XCME")),
433        Ustr::from("ES"),
434        Ustr::from("EQ"),
435        UnixNanos::from(activation.timestamp_nanos_opt().unwrap() as u64),
436        UnixNanos::from(expiration.timestamp_nanos_opt().unwrap() as u64),
437        Currency::USD(),
438        2,
439        Price::from("0.01"),
440        Quantity::from(1),
441        Quantity::from(1),
442        None,
443        None,
444        None,
445        None,
446        None,
447        None,
448        None,
449        None,
450        UnixNanos::default(),
451        UnixNanos::default(),
452    )
453}
454
455////////////////////////////////////////////////////////////////////////////////
456// OptionContract
457////////////////////////////////////////////////////////////////////////////////
458
459#[fixture]
460pub fn option_contract_appl() -> OptionContract {
461    let activation = Utc.with_ymd_and_hms(2021, 9, 17, 0, 0, 0).unwrap();
462    let expiration = Utc.with_ymd_and_hms(2021, 12, 17, 0, 0, 0).unwrap();
463    OptionContract::new(
464        InstrumentId::from("AAPL211217C00150000.OPRA"),
465        Symbol::from("AAPL211217C00150000"),
466        AssetClass::Equity,
467        Some(Ustr::from("GMNI")), // Nasdaq GEMX
468        Ustr::from("AAPL"),
469        OptionKind::Call,
470        Price::from("149.0"),
471        Currency::USD(),
472        UnixNanos::from(activation.timestamp_nanos_opt().unwrap() as u64),
473        UnixNanos::from(expiration.timestamp_nanos_opt().unwrap() as u64),
474        2,
475        Price::from("0.01"),
476        Quantity::from(1),
477        Quantity::from(1),
478        None,
479        None,
480        None,
481        None,
482        None,
483        None,
484        None,
485        None,
486        UnixNanos::default(),
487        UnixNanos::default(),
488    )
489}
490
491////////////////////////////////////////////////////////////////////////////////
492// OptionSpread
493////////////////////////////////////////////////////////////////////////////////
494
495#[fixture]
496pub fn option_spread() -> OptionSpread {
497    let activation = Utc.with_ymd_and_hms(2023, 11, 6, 20, 54, 7).unwrap();
498    let expiration = Utc.with_ymd_and_hms(2024, 2, 23, 22, 59, 0).unwrap();
499    OptionSpread::new(
500        InstrumentId::from("UD:U$: GN 2534559.GLBX"),
501        Symbol::from("UD:U$: GN 2534559"),
502        AssetClass::FX,
503        Some(Ustr::from("XCME")),
504        Ustr::from("SR3"), // British Pound futures (option on futures)
505        Ustr::from("GN"),
506        UnixNanos::from(activation.timestamp_nanos_opt().unwrap() as u64),
507        UnixNanos::from(expiration.timestamp_nanos_opt().unwrap() as u64),
508        Currency::USD(),
509        2,
510        Price::from("0.01"),
511        Quantity::from(1),
512        Quantity::from(1),
513        None,
514        None,
515        None,
516        None,
517        None,
518        None,
519        None,
520        None,
521        UnixNanos::default(),
522        UnixNanos::default(),
523    )
524}
525
526////////////////////////////////////////////////////////////////////////////////
527// BettingInstrument
528////////////////////////////////////////////////////////////////////////////////
529
530#[fixture]
531pub fn betting() -> BettingInstrument {
532    let raw_symbol = Symbol::new("1-123456789");
533    let id = InstrumentId::from(format!("{raw_symbol}.BETFAIR").as_str());
534    let event_type_id = 6423;
535    let event_type_name = Ustr::from("American Football");
536    let competition_id = 12282733;
537    let competition_name = Ustr::from("NFL");
538    let event_id = 29678534;
539    let event_name = Ustr::from("NFL");
540    let event_country_code = Ustr::from("GB");
541    let event_open_date = UnixNanos::from(
542        Utc.with_ymd_and_hms(2022, 2, 7, 23, 30, 0)
543            .unwrap()
544            .timestamp_nanos_opt()
545            .unwrap() as u64,
546    );
547    let betting_type = Ustr::from("ODDS");
548    let market_id = Ustr::from("1-123456789");
549    let market_name = Ustr::from("AFC Conference Winner");
550    let market_type = Ustr::from("SPECIAL");
551    let market_start_time = UnixNanos::from(
552        Utc.with_ymd_and_hms(2022, 2, 7, 23, 30, 0)
553            .unwrap()
554            .timestamp_nanos_opt()
555            .unwrap() as u64,
556    );
557    let selection_id = 50214;
558    let selection_name = Ustr::from("Kansas City Chiefs");
559    let selection_handicap = 0.0; // As per betting convention, no handicap
560    let currency = Currency::GBP();
561    let price_increment = Price::from("0.01");
562    let size_increment = Quantity::from("0.01");
563    let max_quantity = Some(Quantity::from("1000"));
564    let min_quantity = Some(Quantity::from("1"));
565    let max_notional = Some(Money::from("10000 GBP"));
566    let min_notional = Some(Money::from("10 GBP"));
567    let max_price = Some(Price::from("100.00"));
568    let min_price = Some(Price::from("1.00"));
569    let margin_init = Some(Decimal::from(1));
570    let margin_maint = Some(Decimal::from(1));
571    let maker_fee = Some(Decimal::from(0));
572    let taker_fee = Some(Decimal::from(0));
573    let ts_event = UnixNanos::default(); // For testing purposes
574    let ts_init = UnixNanos::default(); // For testing purposes
575
576    BettingInstrument::new(
577        id,
578        raw_symbol,
579        event_type_id,
580        event_type_name,
581        competition_id,
582        competition_name,
583        event_id,
584        event_name,
585        event_country_code,
586        event_open_date,
587        betting_type,
588        market_id,
589        market_name,
590        market_type,
591        market_start_time,
592        selection_id,
593        selection_name,
594        selection_handicap,
595        currency,
596        price_increment.precision,
597        size_increment.precision,
598        price_increment,
599        size_increment,
600        max_quantity,
601        min_quantity,
602        max_notional,
603        min_notional,
604        max_price,
605        min_price,
606        margin_init,
607        margin_maint,
608        maker_fee,
609        taker_fee,
610        ts_event,
611        ts_init,
612    )
613}
614
615////////////////////////////////////////////////////////////////////////////////
616// BinaryOption
617////////////////////////////////////////////////////////////////////////////////
618
619#[fixture]
620pub fn binary_option() -> BinaryOption {
621    let raw_symbol = Symbol::new(
622        "0x12a0cb60174abc437bf1178367c72d11f069e1a3add20b148fb0ab4279b772b2-92544998123698303655208967887569360731013655782348975589292031774495159624905",
623    );
624    let activation = Utc.with_ymd_and_hms(2023, 11, 6, 20, 54, 7).unwrap();
625    let expiration = Utc.with_ymd_and_hms(2024, 2, 23, 22, 59, 0).unwrap();
626    let price_increment = Price::from("0.001");
627    let size_increment = Quantity::from("0.01");
628    BinaryOption::new(
629        InstrumentId::from("{raw_symbol}.POLYMARKET"),
630        raw_symbol,
631        AssetClass::Alternative,
632        Currency::USDC(),
633        UnixNanos::from(activation.timestamp_nanos_opt().unwrap() as u64),
634        UnixNanos::from(expiration.timestamp_nanos_opt().unwrap() as u64),
635        price_increment.precision,
636        size_increment.precision,
637        price_increment,
638        size_increment,
639        None,
640        None,
641        None,
642        None,
643        None,
644        None,
645        None,
646        None,
647        None,
648        None,
649        None,
650        None,
651        UnixNanos::default(),
652        UnixNanos::default(),
653    )
654}