#![allow(clippy::unwrap_used, clippy::expect_used)]
use chrono::{DateTime, Days, Utc};
use rust_decimal::Decimal;
use rust_decimal_macros::dec;
use rustrade::{
engine::state::{
EngineState, global::DefaultGlobalData, instrument::data::DefaultInstrumentMarketData,
position::PositionExited, trading::TradingState,
},
statistic::{summary::TradingSummaryGenerator, time::Annual365},
};
use rustrade_execution::{
balance::{AssetBalance, Balance},
order::id::PositionId,
trade::{AssetFees, TradeId},
};
use rustrade_instrument::{
Side, Underlying,
asset::{AssetIndex, QuoteAsset},
exchange::ExchangeId,
index::IndexedInstruments,
instrument::{Instrument, InstrumentIndex},
};
use rustrade_integration::collection::snapshot::Snapshot;
use smol_str::SmolStr;
const RISK_FREE_RETURN: Decimal = dec!(0.05);
const EXCHANGE: ExchangeId = ExchangeId::BinanceSpot;
const STARTING_BALANCE_USDT: Balance = Balance::new(dec!(10_000.0), dec!(10_000.0));
const STARTING_BALANCE_BTC: Balance = Balance::new(dec!(0.1), dec!(0.1));
const STARTING_BALANCE_ETH: Balance = Balance::new(dec!(1.0), dec!(1.0));
pub enum ContrivedEvents {
Balance(Snapshot<AssetBalance<AssetIndex>>),
Position(PositionExited<QuoteAsset, InstrumentIndex>),
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let instruments = indexed_instruments();
let time_now = Utc::now();
let state = EngineState::builder(&instruments, DefaultGlobalData, |_| {
DefaultInstrumentMarketData::default()
})
.time_engine_start(time_now)
.trading_state(TradingState::Enabled)
.balances([
(EXCHANGE, "usdt", STARTING_BALANCE_USDT),
(EXCHANGE, "btc", STARTING_BALANCE_BTC),
(EXCHANGE, "eth", STARTING_BALANCE_ETH),
])
.build();
let mut summary_generator = TradingSummaryGenerator::init(
RISK_FREE_RETURN,
time_now, time_now,
&state.instruments,
&state.assets,
);
for update in generate_synthetic_updates(time_now) {
match update {
ContrivedEvents::Balance(balance) => {
summary_generator.update_from_balance(balance.as_ref());
}
ContrivedEvents::Position(position) => {
summary_generator.update_from_position(&position);
}
}
}
let summary = summary_generator.generate(Annual365);
summary.print_summary();
Ok(())
}
fn indexed_instruments() -> IndexedInstruments {
IndexedInstruments::builder()
.add_instrument(Instrument::spot(
ExchangeId::BinanceSpot,
"binance_spot_btc_usdt",
"BTCUSDT",
Underlying::new("btc", "usdt"),
None,
))
.add_instrument(Instrument::spot(
ExchangeId::BinanceSpot,
"binance_spot_eth_usdt",
"ETHUSDT",
Underlying::new("eth", "usdt"),
None,
))
.build()
}
fn generate_synthetic_updates(base_time: DateTime<Utc>) -> Vec<ContrivedEvents> {
vec![
ContrivedEvents::Balance(Snapshot::new(AssetBalance {
asset: AssetIndex(2), balance: Balance::new(dec!(9000.0), dec!(9000.0)),
time_exchange: base_time.checked_add_days(Days::new(1)).unwrap(),
})),
ContrivedEvents::Balance(Snapshot::new(AssetBalance {
asset: AssetIndex(2), balance: Balance::new(dec!(12_000.0), dec!(12_000.0)),
time_exchange: base_time.checked_add_days(Days::new(2)).unwrap(),
})),
ContrivedEvents::Position(PositionExited {
position_id: PositionId::NETTING,
instrument: InstrumentIndex(0), side: Side::Buy,
price_entry_average: dec!(1.0),
quantity_abs_max: dec!(1000.0),
pnl_realised: dec!(2000.0), fees_enter: AssetFees {
asset: QuoteAsset,
fees: dec!(0.0),
fees_quote: Some(dec!(0.0)),
},
fees_exit: AssetFees {
asset: QuoteAsset,
fees: dec!(0.0),
fees_quote: Some(dec!(0.0)),
},
time_enter: base_time.checked_add_days(Days::new(1)).unwrap(),
time_exit: base_time.checked_add_days(Days::new(2)).unwrap(),
trades: vec![TradeId(SmolStr::new("1")), TradeId(SmolStr::new("2"))],
}),
ContrivedEvents::Balance(Snapshot::new(AssetBalance {
asset: AssetIndex(2), balance: Balance::new(dec!(10_000.0), dec!(10_000.0)),
time_exchange: base_time.checked_add_days(Days::new(2)).unwrap(),
})),
ContrivedEvents::Balance(Snapshot::new(AssetBalance {
asset: AssetIndex(2), balance: Balance::new(dec!(13_000.0), dec!(13_000.0)),
time_exchange: base_time.checked_add_days(Days::new(3)).unwrap(),
})),
ContrivedEvents::Position(PositionExited {
position_id: PositionId::NETTING,
instrument: InstrumentIndex(0), side: Side::Buy,
price_entry_average: dec!(1.0),
quantity_abs_max: dec!(2000.0),
pnl_realised: dec!(1000.0), fees_enter: AssetFees::default(),
fees_exit: AssetFees::default(),
time_enter: base_time.checked_add_days(Days::new(2)).unwrap(),
time_exit: base_time.checked_add_days(Days::new(3)).unwrap(),
trades: vec![TradeId(SmolStr::new("3")), TradeId(SmolStr::new("4"))],
}),
ContrivedEvents::Balance(Snapshot::new(AssetBalance {
asset: AssetIndex(2), balance: Balance::new(dec!(8000.0), dec!(8000.0)),
time_exchange: base_time.checked_add_days(Days::new(4)).unwrap(),
})),
ContrivedEvents::Balance(Snapshot::new(AssetBalance {
asset: AssetIndex(2), balance: Balance::new(dec!(11_000.0), dec!(11_000.0)),
time_exchange: base_time.checked_add_days(Days::new(5)).unwrap(),
})),
ContrivedEvents::Position(PositionExited {
position_id: PositionId::NETTING,
instrument: InstrumentIndex(0), side: Side::Buy,
price_entry_average: dec!(1.0),
quantity_abs_max: dec!(2000.0),
pnl_realised: dec!(-2000.0), fees_enter: AssetFees::default(),
fees_exit: AssetFees::default(),
time_enter: base_time.checked_add_days(Days::new(4)).unwrap(),
time_exit: base_time.checked_add_days(Days::new(5)).unwrap(),
trades: vec![TradeId(SmolStr::new("5")), TradeId(SmolStr::new("6"))],
}),
ContrivedEvents::Balance(Snapshot::new(AssetBalance {
asset: AssetIndex(2), balance: Balance::new(dec!(6000.0), dec!(6000.0)),
time_exchange: base_time.checked_add_days(Days::new(6)).unwrap(),
})),
ContrivedEvents::Balance(Snapshot::new(AssetBalance {
asset: AssetIndex(2), balance: Balance::new(dec!(5000.0), dec!(5000.0)),
time_exchange: base_time.checked_add_days(Days::new(7)).unwrap(),
})),
ContrivedEvents::Balance(Snapshot::new(AssetBalance {
asset: AssetIndex(2), balance: Balance::new(dec!(10_000.0), dec!(10_000.0)),
time_exchange: base_time.checked_add_days(Days::new(8)).unwrap(),
})),
ContrivedEvents::Position(PositionExited {
position_id: PositionId::NETTING,
instrument: InstrumentIndex(1), side: Side::Buy,
price_entry_average: dec!(1.0),
quantity_abs_max: dec!(6000.0),
pnl_realised: dec!(-1000.0), fees_enter: AssetFees::default(),
fees_exit: AssetFees::default(),
time_enter: base_time.checked_add_days(Days::new(6)).unwrap(),
time_exit: base_time.checked_add_days(Days::new(8)).unwrap(),
trades: vec![
TradeId(SmolStr::new("7")),
TradeId(SmolStr::new("8")),
TradeId(SmolStr::new("9")),
],
}),
ContrivedEvents::Balance(Snapshot::new(AssetBalance {
asset: AssetIndex(2), balance: Balance::new(dec!(7000.0), dec!(7000.0)),
time_exchange: base_time.checked_add_days(Days::new(10)).unwrap(),
})),
ContrivedEvents::Balance(Snapshot::new(AssetBalance {
asset: AssetIndex(2), balance: Balance::new(dec!(10_500.0), dec!(10_500.0)),
time_exchange: base_time.checked_add_days(Days::new(11)).unwrap(),
})),
ContrivedEvents::Position(PositionExited {
position_id: PositionId::NETTING,
instrument: InstrumentIndex(1), side: Side::Buy,
price_entry_average: dec!(1.0),
quantity_abs_max: dec!(6000.0),
pnl_realised: dec!(500.0), fees_enter: AssetFees::default(),
fees_exit: AssetFees::default(),
time_enter: base_time.checked_add_days(Days::new(10)).unwrap(),
time_exit: base_time.checked_add_days(Days::new(11)).unwrap(),
trades: vec![TradeId(SmolStr::new("10")), TradeId(SmolStr::new("11"))],
}),
]
}