use std::{hint::spin_loop, path::PathBuf, range::RangeInclusive};
use crate::{
backend::{DataBackend, OrderIdGenerator, OrdersBackend},
instrument::{InstrumentData, InstrumentSpec},
order::{ActiveOrderGoals, ActiveStateGoal, ApplyOrderStateUpdate, DeferredOrderActions},
order_manager::{OrderManager, OrdersCapacitySpec},
strategy::Strategy,
timestamp::{TickTimestamp, Timestamp},
volume::DirectionalExposure,
};
use bevy::log::{Level, debug, error, info, tracing, tracing_subscriber::FmtSubscriber};
use clap::Parser;
use time::{Date, Duration, Time, UtcDateTime, format_description::well_known::Iso8601};
use tokio::{fs::File, io::BufReader};
mod cli;
use cli::*;
pub async fn run<
IS: InstrumentSpec + Send,
DB: DataBackend<IS>,
OB: OrdersBackend<IS>,
OrdersCS: OrdersCapacitySpec,
S: Strategy<IS, OB, OrdersCS>,
>(
strategy: S
) {
tracing::subscriber::set_global_default(
FmtSubscriber::builder()
.with_max_level(Level::DEBUG)
.finish(),
)
.unwrap();
let CLI { key, command } = CLI::parse();
match command {
Commands::Backtest { start, end: last } => {
let Ok(start_date) = Date::parse(&start, &Iso8601::DATE) else {
error!(
"Invalid start date: \"{}\"",
start
);
return;
};
let Ok(last_date) = Date::parse(&last, &Iso8601::DATE) else {
error!(
"Invalid last date (inclusive): \"{}\"",
last
);
return;
};
let date_range_inclusive = RangeInclusive {
start: start_date,
last: last_date,
};
backtest::<IS, DB, OB, OrdersCS, S>(
strategy,
key.as_ref()
.map(|string| string.as_str()),
date_range_inclusive,
)
.await;
}
}
}
async fn backtest<
IS: InstrumentSpec + Send,
DB: DataBackend<IS>,
OB: OrdersBackend<IS>,
OrdersCS: OrdersCapacitySpec,
S: Strategy<IS, OB, OrdersCS>,
>(
mut strategy: S,
key: Option<&str>,
date_range_inclusive: RangeInclusive<Date>,
) {
let walk_start_timestamp = Timestamp::new(
UtcDateTime::new(
date_range_inclusive.start,
Time::MIDNIGHT,
)
.unix_timestamp_nanos(),
);
let walk_last_timestamp = Timestamp::new(
UtcDateTime::new(
date_range_inclusive.last + Duration::days(1),
Time::MIDNIGHT,
)
.unix_timestamp_nanos(),
);
let mut instrument_data = InstrumentData::<IS>::default();
let mut data_backend = DB::new(key);
let (mut order_state_update_receiver, mut orders_backend) = OB::new().await;
let mut order_manager = OrderManager::default();
let mut order_id_generator = OrderIdGenerator::new();
let mut deferred_order_actions = DeferredOrderActions::<IS>::default();
let mut directional_exposure = DirectionalExposure::<IS>::default();
let mut active_order_goals = ActiveOrderGoals::default();
let mut active_state_goal = ActiveStateGoal::default();
let start_julian_day = date_range_inclusive
.start
.to_julian_day();
let last_julian_day = date_range_inclusive
.last
.to_julian_day();
if start_julian_day > last_julian_day {
error!(
"Invalid start date \"{}\" (inclusive date, inclusive time at midnight) and/or last date \"{}\" (inclusive date, exclusive time at midnight).",
date_range_inclusive.start, date_range_inclusive.last,
);
return;
}
for julian_day in start_julian_day..=last_julian_day {
let Ok(date) = Date::from_julian_day(julian_day) else {
error!(
"Invalid start date \"{}\" or end date \"{}\".",
date_range_inclusive.start, date_range_inclusive.last,
);
return;
};
let mut iso_string_date = date
.format(&Iso8601::DATE)
.unwrap();
iso_string_date += DB::file_name_postpend();
let mut path_buf = PathBuf::new();
path_buf.push("market-data");
path_buf.push(IS::INSTRUMENT_KIND.as_str());
path_buf.push(IS::BARE_SYMBOL);
path_buf.push("trades");
path_buf.push(iso_string_date);
let Ok(file) = File::open(&path_buf).await else {
error!(
"Unable to open market data file: \"{:?}\".",
path_buf,
);
return;
};
let trades = data_backend
.stream_in(BufReader::new(file))
.await;
info!(
"Adding {} binned trades from {:?}",
trades.len(),
path_buf,
);
instrument_data.new_trades(trades);
}
instrument_data.initialize_walk(walk_start_timestamp);
let mut tick_timestamp = TickTimestamp::new(walk_start_timestamp);
strategy
.initialize(
&tick_timestamp,
&instrument_data,
&mut order_manager,
&directional_exposure,
&active_order_goals,
&active_state_goal,
&mut deferred_order_actions,
&mut order_id_generator,
)
.await;
loop {
while let Ok(order_state_update) = order_state_update_receiver.try_recv() {
order_state_update
.apply(
&tick_timestamp,
&mut order_manager,
&mut orders_backend,
&mut order_id_generator,
&mut directional_exposure,
&mut deferred_order_actions,
&mut active_order_goals,
&mut active_state_goal,
)
.await
.unwrap();
}
deferred_order_actions
.trigger(
&tick_timestamp,
&mut order_manager,
&mut orders_backend,
&mut order_id_generator,
&mut directional_exposure,
&mut active_order_goals,
&mut active_state_goal,
)
.await
.unwrap();
let Some(walk_timestamp) = instrument_data
.walk::<100_000_000>(&walk_last_timestamp)
.await
.cloned()
else {
error!("Walk loop terminating due to walk rejection.");
break;
};
orders_backend
.trades_stream(instrument_data.iter_recent_latest_trades_forward())
.await;
order_manager
.pending_client_orders
.tick_client_orders(
&mut deferred_order_actions,
&mut directional_exposure,
instrument_data.iter_recent_latest_trades_forward(),
);
deferred_order_actions
.trigger(
&tick_timestamp,
&mut order_manager,
&mut orders_backend,
&mut order_id_generator,
&mut directional_exposure,
&mut active_order_goals,
&mut active_state_goal,
)
.await
.unwrap();
tick_timestamp = TickTimestamp::new(walk_timestamp);
strategy
.data_update(
&tick_timestamp,
&instrument_data,
&order_manager,
&directional_exposure,
&active_order_goals,
&active_state_goal,
&mut deferred_order_actions,
&mut order_id_generator,
)
.await;
deferred_order_actions
.trigger(
&tick_timestamp,
&mut order_manager,
&mut orders_backend,
&mut order_id_generator,
&mut directional_exposure,
&mut active_order_goals,
&mut active_state_goal,
)
.await
.unwrap();
if walk_timestamp == walk_last_timestamp {
info!("Walk loop successfully terminating.");
break;
}
spin_loop();
}
}