#![allow(clippy::unwrap_used, clippy::expect_used)]
use chrono::{DateTime, Duration, Utc};
use rust_decimal::Decimal;
use rustrade::{
backtest::{
BacktestArgsConstant, BacktestArgsDynamic, backtest,
market_data::{BacktestMarketData, MarketDataInMemory},
},
engine::{
Processor,
state::{
EngineState, builder::EngineStateBuilder, global::DefaultGlobalData,
instrument::data::InstrumentDataState,
order::in_flight_recorder::InFlightRequestRecorder, trading::TradingState,
},
},
risk::DefaultRiskManager,
statistic::time::Daily,
strategy::DefaultStrategy,
system::config::SystemConfig,
};
use rustrade_data::{
event::{DataKind, MarketEvent},
streams::consumer::MarketStreamEvent,
subscription::candle::{Candle, IntervalStep, close_time_from_open},
};
use rustrade_execution::{
AccountEvent,
order::request::{OrderRequestCancel, OrderRequestOpen},
};
use rustrade_instrument::{
exchange::ExchangeId, index::IndexedInstruments, instrument::InstrumentIndex,
};
use serde::Deserialize;
use std::{fs::File, io::BufReader, sync::Arc};
const CONFIG_PATH: &str = "rustrade/examples/config/backtest_config.json";
#[derive(Deserialize)]
pub struct Config {
pub risk_free_return: Decimal,
pub system: SystemConfig,
}
#[tokio::main]
async fn main() {
rustrade::logging::init_logging();
let Config {
risk_free_return,
system: SystemConfig {
instruments,
executions,
},
} = load_config();
let instruments = IndexedInstruments::new(instruments);
let market_events = candle_market_data();
let market_data = MarketDataInMemory::new(Arc::new(market_events));
let time_engine_start = market_data.time_first_event().await.unwrap();
let engine_state = EngineStateBuilder::new(&instruments, DefaultGlobalData, |_| {
CandleInstrumentData::default()
})
.time_engine_start(time_engine_start)
.trading_state(TradingState::Enabled)
.build();
let args_constant = Arc::new(BacktestArgsConstant {
instruments,
executions,
market_data,
summary_interval: Daily,
engine_state,
});
let args_dynamic = BacktestArgsDynamic {
id: "candle-backtest-demo".into(),
risk_free_return,
strategy: DefaultStrategy::<EngineState<DefaultGlobalData, CandleInstrumentData>>::default(
),
risk: DefaultRiskManager::<EngineState<DefaultGlobalData, CandleInstrumentData>>::default(),
};
let summary = backtest(args_constant, args_dynamic).await.unwrap();
println!("\nBacktest complete (BacktestId = {})", summary.id);
summary.trading_summary.print_summary();
}
fn candle_market_data() -> Vec<MarketStreamEvent<InstrumentIndex, DataKind>> {
let first_open: DateTime<Utc> = "2025-03-24T22:00:00Z".parse().unwrap();
let step = IntervalStep::Fixed(Duration::hours(1));
(0..48)
.map(|i| {
let open_time = first_open + Duration::hours(i);
let close_time = close_time_from_open(open_time, step)
.expect("1h candle boundary is well within DateTime<Utc> range");
let close = Decimal::from(60_000) + Decimal::from(i * 25);
let candle = Candle {
close_time,
open: close - Decimal::from(10),
high: close + Decimal::from(20),
low: close - Decimal::from(20),
close,
volume: Decimal::from(5),
trade_count: 100,
};
MarketStreamEvent::Item(MarketEvent {
time_exchange: candle.close_time,
time_received: candle.close_time,
exchange: ExchangeId::BinanceSpot,
instrument: InstrumentIndex::new(0),
kind: DataKind::Candle(candle),
})
})
.collect()
}
pub fn load_config() -> Config {
let file = File::open(CONFIG_PATH).expect("Failed to open config file");
let reader = BufReader::new(file);
serde_json::from_reader(reader).expect("Failed to parse config file")
}
#[derive(Debug, Clone, Default)]
pub struct CandleInstrumentData {
pub last_candle: Option<Candle>,
}
impl InstrumentDataState for CandleInstrumentData {
type MarketEventKind = DataKind;
fn price(&self) -> Option<Decimal> {
self.last_candle.as_ref().map(|candle| candle.close)
}
}
impl<InstrumentKey> Processor<&MarketEvent<InstrumentKey, DataKind>> for CandleInstrumentData {
type Audit = ();
fn process(&mut self, event: &MarketEvent<InstrumentKey, DataKind>) -> Self::Audit {
if let DataKind::Candle(candle) = &event.kind {
self.last_candle = Some(*candle);
}
}
}
impl<ExchangeKey, AssetKey, InstrumentKey>
Processor<&AccountEvent<ExchangeKey, AssetKey, InstrumentKey>> for CandleInstrumentData
{
type Audit = ();
fn process(&mut self, _: &AccountEvent<ExchangeKey, AssetKey, InstrumentKey>) -> Self::Audit {}
}
impl<ExchangeKey, InstrumentKey> InFlightRequestRecorder<ExchangeKey, InstrumentKey>
for CandleInstrumentData
{
fn record_in_flight_cancel(&mut self, _: &OrderRequestCancel<ExchangeKey, InstrumentKey>) {}
fn record_in_flight_open(&mut self, _: &OrderRequestOpen<ExchangeKey, InstrumentKey>) {}
}