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